Skip to content

Commit deadfa4

Browse files
committed
ref(release): cleanup old RCs and related kubernetes resources after deploy
Iterates through all available RCs beside the *latest*, scales them to 0 and deletes. Takes care of the release specific secret as well. The secret has its own iteration as well in case anything happened on prior runs which made it into an orphan (no RC) The biggest change here is that the code is not attempting to delete the secret during individual RC deploy but rather as part of the cleanup after the deploy. Fixes #540
1 parent 6aab1d1 commit deadfa4

3 files changed

Lines changed: 82 additions & 24 deletions

File tree

rootfs/api/models/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ def deploy(self, release):
384384
log_event(self, err, logging.ERROR)
385385
raise
386386

387+
# cleanup old releases from kubernetes
388+
release.cleanup_old()
389+
387390
def _default_structure(self, release):
388391
"""Scale to default structure based on release type"""
389392
# if there is no SHA, assume a docker image is being promoted

rootfs/api/models/release.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,7 @@ def rollback(self, user, version):
151151
def delete(self, *args, **kwargs):
152152
"""Delete release DB record and any RCs from the affect release"""
153153
try:
154-
labels = {
155-
'app': self.app.id,
156-
'version': 'v{}'.format(self.version)
157-
}
158-
controllers = self._scheduler._get_rcs(self.app.id, labels=labels)
159-
for controller in controllers.json()['items']:
160-
self._scheduler._delete_rc(self.app.id, controller['metadata']['name'])
154+
self._delete_release_in_scheduler(self.app.id, self.version)
161155
except KubeHTTPException as e:
162156
# 404 means they were already cleaned up
163157
if e.status_code is not 404:
@@ -168,6 +162,68 @@ def delete(self, *args, **kwargs):
168162
finally:
169163
super(Release, self).delete(*args, **kwargs)
170164

165+
def cleanup_old(self):
166+
"""Cleanup all but the latest release from Kubernetes"""
167+
latest_version = 'v{}'.format(self.version)
168+
log_event(self.app, 'Cleaning up RCS for releases older than {} (latest)'.format(latest_version)) # noqa
169+
170+
# Cleanup controllers
171+
controller_removal = []
172+
controllers = self._scheduler._get_rcs(self.app.id).json()
173+
for controller in controllers['items']:
174+
current_version = controller['metadata']['labels']['version']
175+
# skip the latest release
176+
if current_version == latest_version:
177+
continue
178+
179+
# aggregate versions together to removal all at once
180+
if current_version not in controller_removal:
181+
controller_removal.append(current_version)
182+
183+
if controller_removal:
184+
log_event(self.app, 'Found the following versions to cleanup: {}'.format(', '.join(controller_removal))) # noqa
185+
186+
for version in controller_removal:
187+
self._delete_release_in_scheduler(self.app.id, version)
188+
189+
# find stray env secrets to remove that may have been missed
190+
log_event(self.app, 'Cleaning up orphaned environment var secrets')
191+
labels = {
192+
'app': self.app.id,
193+
'type': 'env'
194+
}
195+
secrets = self._scheduler._get_secrets(self.app.id, labels=labels).json()
196+
for secret in secrets['items']:
197+
current_version = secret['metadata']['labels']['version']
198+
# skip the latest release
199+
if current_version == latest_version:
200+
continue
201+
202+
self._scheduler._delete_secret(self.app.id, secret['metadata']['name'])
203+
204+
def _delete_release_in_scheduler(self, namespace, version):
205+
"""
206+
Deletes a specific release in k8s
207+
208+
Scale RCs to 0 then delete RCs and the version specific
209+
secret that container the env var
210+
"""
211+
labels = {
212+
'app': namespace,
213+
'version': 'v{}'.format(version)
214+
}
215+
controllers = self._scheduler._get_rcs(namespace, labels=labels)
216+
for controller in controllers.json()['items']:
217+
self._scheduler._scale_rc(namespace, controller['metadata']['name'], 0)
218+
self._scheduler._delete_rc(namespace, controller['metadata']['name'])
219+
220+
# remove secret that contains env vars for the release
221+
try:
222+
secret_name = "{}-{}-env".format(namespace, version)
223+
self._scheduler._delete_secret(namespace, secret_name)
224+
except KubeHTTPException:
225+
pass
226+
171227
def save(self, *args, **kwargs): # noqa
172228
if not self.summary:
173229
self.summary = ''

rootfs/scheduler/__init__.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,10 @@
275275
"apiVersion": "$version",
276276
"metadata": {
277277
"name": "$name",
278-
"namespace": "$id"
278+
"namespace": "$id",
279+
"labels": {
280+
"app": "$id"
281+
}
279282
},
280283
"type": "Opaque",
281284
"data": {}
@@ -386,31 +389,20 @@ def deploy(self, namespace, name, image, command, **kwargs): # noqa
386389
new_rc["metadata"]["name"], desired)
387390
)
388391

389-
# Remove new release
392+
# Remove new release of the RC
390393
self._scale_rc(namespace, new_rc["metadata"]["name"], 0)
391394
self._delete_rc(namespace, new_rc["metadata"]["name"])
392-
# remove secret that contains env vars for the release
393-
try:
394-
secret_name = "{}-{}-env".format(namespace, kwargs.get('version'))
395-
self._delete_secret(namespace, secret_name)
396-
except KubeHTTPException:
397-
pass
398395

399-
# Bring back old release if available
396+
# Bring back old release if available of the RC
400397
if old_rc:
401398
self._scale_rc(namespace, old_rc["metadata"]["name"], desired)
402399

403400
raise KubeException('{} (scheduler::deploy): {}'.format(name, e))
404401

405402
# New release is live and kicking. Clean up old release
406403
if old_rc:
404+
self._scale_rc(namespace, old_rc["metadata"]["name"], 0)
407405
self._delete_rc(namespace, old_rc["metadata"]["name"])
408-
# remove secret that contains env vars for the release
409-
secret_name = "{}-{}-env".format(namespace, old_rc['metadata']['labels']['version'])
410-
try:
411-
self._delete_secret(namespace, secret_name)
412-
except KubeHTTPException:
413-
pass
414406

415407
# Make sure the application is routable and uses the correct port
416408
# Done after the fact to let initial deploy settle before routing
@@ -601,7 +593,11 @@ def _set_environment(self, data, namespace, **kwargs):
601593
secret_name = "{}-{}-env".format(namespace, kwargs.get('version'))
602594
self._get_secret(namespace, secret_name)
603595
except KubeHTTPException:
604-
self._create_secret(namespace, secret_name, secrets_env)
596+
labels = {
597+
'version': kwargs.get('version'),
598+
'type': 'env'
599+
}
600+
self._create_secret(namespace, secret_name, secrets_env, labels)
605601
else:
606602
self._update_secret(namespace, secret_name, secrets_env)
607603

@@ -1075,13 +1071,16 @@ def _get_secrets(self, namespace, **kwargs):
10751071

10761072
return response
10771073

1078-
def _create_secret(self, namespace, name, data):
1074+
def _create_secret(self, namespace, name, data, labels={}):
10791075
template = json.loads(string.Template(SECRET_TEMPLATE).substitute({
10801076
"version": self.apiversion,
10811077
"id": namespace,
10821078
"name": name
10831079
}))
10841080

1081+
# add in any additional label info
1082+
template['metadata']['labels'].update(labels)
1083+
10851084
for key, value in data.items():
10861085
value = value if isinstance(value, bytes) else bytes(value, 'UTF-8')
10871086
item = base64.b64encode(value).decode(encoding='UTF-8')

0 commit comments

Comments
 (0)