14
14
import container
15
15
import lifecycle
16
16
import logrotate
17
+ import machine_upgrade
17
18
import relations .database_provides
18
19
import relations .database_requires
20
+ import upgrade
19
21
import workload
20
22
21
23
logger = logging .getLogger (__name__ )
@@ -35,11 +37,26 @@ def __init__(self, *args) -> None:
35
37
self ._authenticated_workload_type = workload .AuthenticatedWorkload
36
38
self ._database_requires = relations .database_requires .RelationEndpoint (self )
37
39
self ._database_provides = relations .database_provides .RelationEndpoint (self )
38
- self .framework .observe (self .on .update_status , self .reconcile_database_relations )
39
- # Set status on first start if no relations active
40
- self .framework .observe (self .on .start , self .reconcile_database_relations )
40
+ self .framework .observe (self .on .update_status , self .reconcile )
41
+ self .framework .observe (
42
+ self .on [upgrade .PEER_RELATION_ENDPOINT_NAME ].relation_changed , self .reconcile
43
+ )
44
+ self .framework .observe (
45
+ self .on [upgrade .RESUME_ACTION_NAME ].action , self ._on_resume_upgrade_action
46
+ )
47
+ # (For Kubernetes) Reset partition after scale down
48
+ self .framework .observe (
49
+ self .on [upgrade .PEER_RELATION_ENDPOINT_NAME ].relation_departed , self .reconcile
50
+ )
51
+ # Handle upgrade & set status on first start if no relations active
52
+ self .framework .observe (self .on .start , self .reconcile )
41
53
# Update app status
42
- self .framework .observe (self .on .leader_elected , self .reconcile_database_relations )
54
+ self .framework .observe (self .on .leader_elected , self .reconcile )
55
+ # Set versions in upgrade peer relation app databag
56
+ self .framework .observe (
57
+ self .on [upgrade .PEER_RELATION_ENDPOINT_NAME ].relation_created ,
58
+ self ._upgrade_relation_created ,
59
+ )
43
60
44
61
@property
45
62
@abc .abstractmethod
@@ -60,6 +77,11 @@ def _tls_certificate_saved(self) -> bool:
60
77
def _container (self ) -> container .Container :
61
78
"""Workload container (snap or ROCK)"""
62
79
80
+ @property
81
+ @abc .abstractmethod
82
+ def _upgrade (self ) -> typing .Optional [upgrade .Upgrade ]:
83
+ pass
84
+
63
85
@property
64
86
@abc .abstractmethod
65
87
def _logrotate (self ) -> logrotate .LogRotate :
@@ -95,8 +117,8 @@ def _prioritize_statuses(statuses: typing.List[ops.StatusBase]) -> ops.StatusBas
95
117
"""
96
118
status_priority = (
97
119
ops .BlockedStatus ,
98
- ops .WaitingStatus ,
99
120
ops .MaintenanceStatus ,
121
+ ops .WaitingStatus ,
100
122
# Catch any unknown status type
101
123
ops .StatusBase ,
102
124
)
@@ -108,6 +130,11 @@ def _prioritize_statuses(statuses: typing.List[ops.StatusBase]) -> ops.StatusBas
108
130
109
131
def _determine_app_status (self , * , event ) -> ops .StatusBase :
110
132
"""Report app status."""
133
+ if self ._upgrade and (upgrade_status := self ._upgrade .app_status ):
134
+ # Upgrade status should take priority over relation status—even if the status level is
135
+ # normally lower priority.
136
+ # (Relations should not be modified during upgrade.)
137
+ return upgrade_status
111
138
statuses = []
112
139
for endpoint in (self ._database_requires , self ._database_provides ):
113
140
if status := endpoint .get_status (event ):
@@ -117,25 +144,28 @@ def _determine_app_status(self, *, event) -> ops.StatusBase:
117
144
def _determine_unit_status (self , * , event ) -> ops .StatusBase :
118
145
"""Report unit status."""
119
146
statuses = []
120
- workload_ = self .get_workload (event = event )
121
- statuses .append (workload_ .get_status (event ))
147
+ workload_status = self .get_workload (event = event ).status
148
+ if self ._upgrade :
149
+ statuses .append (self ._upgrade .get_unit_juju_status (workload_status = workload_status ))
150
+ statuses .append (workload_status )
122
151
return self ._prioritize_statuses (statuses )
123
152
124
- def set_status (self , * , event ) -> None :
153
+ def set_status (self , * , event , app = True , unit = True ) -> None :
125
154
"""Set charm status."""
126
- if self ._unit_lifecycle .authorized_leader :
155
+ if app and self ._unit_lifecycle .authorized_leader :
127
156
self .app .status = self ._determine_app_status (event = event )
128
157
logger .debug (f"Set app status to { self .app .status } " )
129
- self .unit .status = self ._determine_unit_status (event = event )
130
- logger .debug (f"Set unit status to { self .unit .status } " )
158
+ if unit :
159
+ self .unit .status = self ._determine_unit_status (event = event )
160
+ logger .debug (f"Set unit status to { self .unit .status } " )
131
161
132
162
def wait_until_mysql_router_ready (self ) -> None :
133
163
"""Wait until a connection to MySQL Router is possible.
134
164
135
165
Retry every 5 seconds for up to 30 seconds.
136
166
"""
137
167
logger .debug ("Waiting until MySQL Router is ready" )
138
- self .unit .status = ops .WaitingStatus ("MySQL Router starting" )
168
+ self .unit .status = ops .MaintenanceStatus ("MySQL Router starting" )
139
169
try :
140
170
for attempt in tenacity .Retrying (
141
171
reraise = True ,
@@ -156,21 +186,63 @@ def wait_until_mysql_router_ready(self) -> None:
156
186
# Handlers
157
187
# =======================
158
188
159
- def reconcile_database_relations (self , event = None ) -> None :
160
- """Handle database requires/provides events."""
189
+ def _upgrade_relation_created (self , _ ) -> None :
190
+ if self ._unit_lifecycle .authorized_leader :
191
+ # `self._upgrade.is_compatible` should return `True` during first charm
192
+ # installation/setup
193
+ self ._upgrade .set_versions_in_app_databag ()
194
+
195
+ def reconcile (self , event = None ) -> None : # noqa: C901
196
+ """Handle most events."""
197
+ if not self ._upgrade :
198
+ logger .debug ("Peer relation not available" )
199
+ return
200
+ if not self ._upgrade .versions_set :
201
+ logger .debug ("Peer relation not ready" )
202
+ return
161
203
workload_ = self .get_workload (event = event )
204
+ if self ._upgrade .unit_state == "restarting" : # Kubernetes only
205
+ if not self ._upgrade .is_compatible :
206
+ logger .info (
207
+ "Upgrade incompatible. If you accept potential *data loss* and *downtime*, you can continue with `resume-upgrade force=true`"
208
+ )
209
+ self .unit .status = ops .BlockedStatus (
210
+ "Upgrade incompatible. Rollback to previous revision with `juju refresh`"
211
+ )
212
+ self .set_status (event = event , unit = False )
213
+ return
214
+ elif isinstance (self ._upgrade , machine_upgrade .Upgrade ): # Machines only
215
+ if not self ._upgrade .is_compatible :
216
+ self .set_status (event = event )
217
+ return
218
+ if self ._upgrade .unit_state == "outdated" :
219
+ if self ._upgrade .authorized :
220
+ self ._upgrade .upgrade_unit (
221
+ workload_ = workload_ , tls = self ._tls_certificate_saved
222
+ )
223
+ else :
224
+ self .set_status (event = event )
225
+ logger .debug ("Waiting to upgrade" )
226
+ return
162
227
logger .debug (
163
228
"State of reconcile "
164
229
f"{ self ._unit_lifecycle .authorized_leader = } , "
165
230
f"{ isinstance (workload_ , workload .AuthenticatedWorkload )= } , "
166
231
f"{ workload_ .container_ready = } , "
167
- f"{ self ._database_requires .is_relation_breaking (event )= } "
232
+ f"{ self ._database_requires .is_relation_breaking (event )= } , "
233
+ f"{ self ._upgrade .in_progress = } "
168
234
)
169
235
if self ._unit_lifecycle .authorized_leader :
170
236
if self ._database_requires .is_relation_breaking (event ):
237
+ if self ._upgrade .in_progress :
238
+ logger .warning (
239
+ "Modifying relations during an upgrade is not supported. The charm may be in a broken, unrecoverable state. Re-deploy the charm"
240
+ )
171
241
self ._database_provides .delete_all_databags ()
172
242
elif (
173
- isinstance (workload_ , workload .AuthenticatedWorkload ) and workload_ .container_ready
243
+ not self ._upgrade .in_progress
244
+ and isinstance (workload_ , workload .AuthenticatedWorkload )
245
+ and workload_ .container_ready
174
246
):
175
247
self ._database_provides .reconcile_users (
176
248
event = event ,
@@ -182,4 +254,25 @@ def reconcile_database_relations(self, event=None) -> None:
182
254
workload_ .enable (tls = self ._tls_certificate_saved , unit_name = self .unit .name )
183
255
elif workload_ .container_ready :
184
256
workload_ .disable ()
257
+ # Empty waiting status means we're waiting for database requires relation before starting
258
+ # workload
259
+ if not workload_ .status or workload_ .status == ops .WaitingStatus ():
260
+ self ._upgrade .unit_state = "healthy"
261
+ if self ._unit_lifecycle .authorized_leader :
262
+ self ._upgrade .reconcile_partition ()
263
+ if not self ._upgrade .in_progress :
264
+ self ._upgrade .set_versions_in_app_databag ()
185
265
self .set_status (event = event )
266
+
267
+ def _on_resume_upgrade_action (self , event : ops .ActionEvent ) -> None :
268
+ if not self ._unit_lifecycle .authorized_leader :
269
+ message = f"Must run action on leader unit. (e.g. `juju run { self .app .name } /leader { upgrade .RESUME_ACTION_NAME } `)"
270
+ logger .debug (f"Resume upgrade event failed: { message } " )
271
+ event .fail (message )
272
+ return
273
+ if not self ._upgrade or not self ._upgrade .in_progress :
274
+ message = "No upgrade in progress"
275
+ logger .debug (f"Resume upgrade event failed: { message } " )
276
+ event .fail (message )
277
+ return
278
+ self ._upgrade .reconcile_partition (action_event = event )
0 commit comments