Skip to content

Commit 4ee8057

Browse files
committed
ref(models): use DRF APIException as a base for all exceptions
Move to using DeisException that extends APIException (with 400 as a status code) instead of EnvironmentError, have AlreadyExists extend APIException with 409 and KubeException and Logger related exceptions get raised up to the view as ServiceUnavailable Additionally using "from e" when re-raising exception to keep contextual information if that is ever required (https://www.python.org/dev/peps/pep-3134/) https://github.com/tomchristie/django-rest-framework/blob/master/docs/api-guide/exceptions.md has further info on DRF exceptions ref #612
1 parent 36fa29b commit 4ee8057

9 files changed

Lines changed: 131 additions & 168 deletions

File tree

rootfs/api/models/__init__.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,27 @@
1313
from django.db import models
1414
from django.db.models.signals import post_delete, post_save
1515
from django.dispatch import receiver
16-
from django.core.exceptions import ValidationError
16+
17+
from rest_framework.exceptions import ValidationError, APIException
1718
from rest_framework.authtoken.models import Token
1819

1920
from api.utils import dict_merge
21+
from scheduler import KubeException
2022

2123
logger = logging.getLogger(__name__)
2224

2325

24-
class AlreadyExists(EnvironmentError):
25-
pass
26+
class DeisException(APIException):
27+
status_code = 400
28+
29+
30+
class AlreadyExists(APIException):
31+
status_code = 409
32+
33+
34+
class ServiceUnavailable(APIException):
35+
status_code = 503
36+
default_detail = 'Service temporarily unavailable, try again later.'
2637

2738

2839
def log_event(app, msg, level=logging.INFO):
@@ -57,8 +68,12 @@ def _scheduler(self):
5768
return mod.SchedulerClient()
5869

5970
def _fetch_service_config(self, app):
60-
# Get the service from k8s to attach the domain correctly
61-
svc = self._scheduler._get_service(app, app).json()
71+
try:
72+
# Get the service from k8s to attach the domain correctly
73+
svc = self._scheduler._get_service(app, app).json()
74+
except KubeException as e:
75+
raise ServiceUnavailable(str(e)) from e
76+
6277
# Get minimum structure going if it is missing on the service
6378
if 'metadata' not in svc or 'annotations' not in svc['metadata']:
6479
default = {'metadata': {'annotations': {}}}
@@ -90,8 +105,11 @@ def _save_service_config(self, app, component, data):
90105
data = {"%s%s" % (component, key): value for key, value in list(data.items())}
91106
svc['metadata']['annotations'].update(morph.flatten(data))
92107

93-
# Update the k8s service for the application with new domain information
94-
self._scheduler._update_service(app, app, svc)
108+
# Update the k8s service for the application with new service information
109+
try:
110+
self._scheduler._update_service(app, app, svc)
111+
except KubeException as e:
112+
raise ServiceUnavailable(str(e)) from e
95113

96114

97115
class UuidAuditedModel(AuditedModel):

rootfs/api/models/app.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111

1212
from django.conf import settings
1313
from django.db import models
14-
from django.core.exceptions import ValidationError
14+
from rest_framework.exceptions import ValidationError, NotFound
1515
from jsonfield import JSONField
1616

1717
from deis import __version__ as deis_version
18-
from api.models import UuidAuditedModel, log_event, AlreadyExists
18+
from api.models import UuidAuditedModel, log_event, AlreadyExists, \
19+
DeisException, ServiceUnavailable
1920

2021
from api.utils import generate_app_name, app_build_type
2122
from api.models.release import Release
@@ -43,7 +44,7 @@ def validate_app_structure(value):
4344
if any(int(v) < 0 for v in value.values()):
4445
raise ValueError("Must be greater than or equal to zero")
4546
except ValueError as err:
46-
raise ValidationError(err)
47+
raise ValidationError(str(err))
4748

4849

4950
def validate_reserved_names(value):
@@ -152,7 +153,10 @@ def create(self, *args, **kwargs):
152153
)
153154

154155
# create required minimum resources in k8s for the application
155-
self._scheduler.create(self.id)
156+
try:
157+
self._scheduler.create(self.id)
158+
except KubeException as e:
159+
raise ServiceUnavailable(str(e)) from e
156160

157161
# Attach the platform specific application sub domain to the k8s service
158162
# Only attach it on first release in case a customer has remove the app domain
@@ -164,8 +168,8 @@ def delete(self, *args, **kwargs):
164168
try:
165169
# attempt to remove application from kubernetes
166170
self._scheduler.destroy(self.id)
167-
except KubeException:
168-
pass
171+
except KubeException as e:
172+
raise ServiceUnavailable(str(e)) from e
169173

170174
self._clean_app_logs()
171175
return super(App, self).delete(*args, **kwargs)
@@ -213,7 +217,7 @@ def restart(self, **kwargs): # noqa
213217
while True:
214218
# timed out
215219
if elapsed >= timeout:
216-
raise KubeException('timeout - 5 minutes have passed and pods are not up')
220+
raise DeisException('timeout - 5 minutes have passed and pods are not up')
217221

218222
# restarting a single pod behaves differently, fetch the *newest* pod
219223
# and hope it is the right one. Comes back sorted
@@ -234,7 +238,6 @@ def restart(self, **kwargs): # noqa
234238

235239
elapsed += 5
236240
time.sleep(5)
237-
238241
except Exception as e:
239242
err = "warning, some pods failed to start:\n{}".format(str(e))
240243
log_event(self, err, logging.WARNING)
@@ -261,18 +264,26 @@ def scale(self, user, structure): # noqa
261264
self.create()
262265

263266
if self.release_set.latest().build is None:
264-
raise EnvironmentError('No build associated with this release')
267+
raise DeisException('No build associated with this release')
265268

266269
release = self.release_set.latest()
267270

271+
# Validate structure
272+
try:
273+
for target, count in structure.copy().items():
274+
structure[target] = int(count)
275+
validate_app_structure(structure)
276+
except (TypeError, ValueError) as e:
277+
raise DeisException('Invalid scaling format: {}'.format(e))
278+
268279
# test for available process types
269280
available_process_types = release.build.procfile or {}
270281
for container_type in structure:
271282
if container_type == 'cmd':
272283
continue # allow docker cmd types in case we don't have the image source
273284

274285
if container_type not in available_process_types:
275-
raise EnvironmentError(
286+
raise DeisException(
276287
'Container type {} does not exist in application'.format(container_type))
277288

278289
# merge current structure and the new items together
@@ -323,16 +334,15 @@ def _scale_pods(self, scale_types):
323334
command=command,
324335
**kwargs
325336
)
326-
327337
except Exception as e:
328338
err = '{} (scale): {}'.format(self._get_job_id(scale_type), e)
329339
log_event(self, err, logging.ERROR)
330-
raise
340+
raise ServiceUnavailable(e) from e
331341

332342
def deploy(self, release):
333343
"""Deploy a new release to this application"""
334344
if release.build is None:
335-
raise EnvironmentError('No build associated with this release')
345+
raise DeisException('No build associated with this release')
336346

337347
# use create to make sure minimum resources are created
338348
self.create()
@@ -386,7 +396,7 @@ def deploy(self, release):
386396
except Exception as e:
387397
err = '{} (app::deploy): {}'.format(self._get_job_id(scale_type), e)
388398
log_event(self, err, logging.ERROR)
389-
raise
399+
raise ServiceUnavailable(err) from e
390400

391401
# cleanup old releases from kubernetes
392402
release.cleanup_old()
@@ -508,19 +518,20 @@ def logs(self, log_lines=str(settings.LOG_LINES)):
508518
r = requests.get(url)
509519
# Handle HTTP request errors
510520
except requests.exceptions.RequestException as e:
511-
logger.error("Error accessing deis-logger using url '{}': {}".format(url, e))
512-
raise e
521+
msg = "Error accessing deis-logger using url '{}': {}".format(url, e)
522+
logger.error(msg)
523+
raise ServiceUnavailable(msg) from e
513524

514525
# Handle logs empty or not found
515526
if r.status_code == 204 or r.status_code == 404:
516527
logger.info("GET {} returned a {} status code".format(url, r.status_code))
517-
raise EnvironmentError('Could not locate logs')
528+
raise NotFound('Could not locate logs')
518529

519530
# Handle unanticipated status codes
520531
if r.status_code != 200:
521532
logger.error("Error accessing deis-logger: GET {} returned a {} status code"
522533
.format(url, r.status_code))
523-
raise EnvironmentError('Error accessing deis-logger')
534+
raise ServiceUnavailable('Error accessing deis-logger')
524535

525536
# cast content to string since it comes as bytes via the requests object
526537
return str(r.content)
@@ -532,7 +543,7 @@ def pod_name(size=5, chars=string.ascii_lowercase + string.digits):
532543
"""Run a one-off command in an ephemeral app container."""
533544
release = self.release_set.latest()
534545
if release.build is None:
535-
raise EnvironmentError('No build associated with this release to run this command')
546+
raise DeisException('No build associated with this release to run this command')
536547

537548
# TODO: add support for interactive shell
538549
# SECURITY: shell-escape user input
@@ -576,7 +587,7 @@ def pod_name(size=5, chars=string.ascii_lowercase + string.digits):
576587
except Exception as e:
577588
err = '{} (run): {}'.format(name, e)
578589
log_event(self, err, logging.ERROR)
579-
raise
590+
raise ServiceUnavailable(str(e)) from e
580591

581592
def list_pods(self, *args, **kwargs):
582593
"""Used to list basic information about pods running for a given application"""
@@ -627,7 +638,7 @@ def list_pods(self, *args, **kwargs):
627638
except Exception as e:
628639
err = '(list pods): {}'.format(e)
629640
log_event(self, err, logging.ERROR)
630-
raise
641+
raise ServiceUnavailable(err) from e
631642

632643
def _scheduler_filter(self, **kwargs):
633644
labels = {'app': self.id}

rootfs/api/models/build.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.db import models
33
from jsonfield import JSONField
44

5-
from api.models import UuidAuditedModel
5+
from api.models import UuidAuditedModel, DeisException
66

77
import logging
88
logger = logging.getLogger(__name__)
@@ -43,10 +43,11 @@ def create(self, user, *args, **kwargs):
4343
try:
4444
self.app.deploy(new_release)
4545
return new_release
46-
except Exception:
46+
except Exception as e:
4747
if 'new_release' in locals():
4848
new_release.delete()
49-
raise
49+
50+
raise DeisException(str(e)) from e
5051

5152
def save(self, **kwargs):
5253
try:

rootfs/api/models/certificate.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
from django.shortcuts import get_object_or_404
99
from django.db import models
1010
from django.conf import settings
11-
from django.core.exceptions import ValidationError, SuspiciousOperation
11+
from django.core.exceptions import SuspiciousOperation
1212
from django.contrib.postgres.fields import ArrayField
13+
from rest_framework.exceptions import ValidationError
1314

14-
from api.models import AuditedModel, validate_label, AlreadyExists
15+
from api.models import AuditedModel, validate_label, AlreadyExists, ServiceUnavailable
1516
from api.models.domain import Domain
1617

17-
from scheduler import KubeHTTPException
18+
from scheduler import KubeException
1819

1920
import logging
2021
logger = logging.getLogger(__name__)
@@ -179,15 +180,15 @@ def attach_in_kubernetes(self, domain):
179180
}
180181

181182
secret = self._scheduler._get_secret(app, name).json()['data']
182-
except KubeHTTPException:
183+
except KubeException:
183184
self._scheduler._create_secret(app, name, data)
184185
else:
185186
# update cert secret to the TLS Ingress format if required
186187
if secret != data:
187188
try:
188189
self._scheduler._update_secret(app, name, data)
189-
except KubeHTTPException:
190-
raise
190+
except KubeException as e:
191+
raise ServiceUnavailable(str(e)) from e
191192

192193
# get config for the service
193194
config = self._load_service_config(app, 'router')
@@ -220,9 +221,8 @@ def detach(self, *args, **kwargs):
220221
# We raise an exception when a secret doesn't exist
221222
self._scheduler._get_secret(app, name)
222223
self._scheduler._delete_secret(app, name)
223-
except KubeHTTPException as e:
224-
logger.critical(e)
225-
raise EnvironmentError("Could not delete certificate secret {} for application {}".format(name, app)) # noqa
224+
except KubeException as e:
225+
raise ServiceUnavailable("Could not delete certificate secret {} for application {}".format(name, app)) from e # noqa
226226

227227
# get config for the service
228228
config = self._load_service_config(app, 'router')

rootfs/api/models/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.db import models
33
from jsonfield import JSONField
44

5-
from api.models import UuidAuditedModel
5+
from api.models import UuidAuditedModel, DeisException
66

77

88
class Config(UuidAuditedModel):
@@ -107,7 +107,7 @@ def set_tags(self, previous_config):
107107
new = set(labels) - set(old)
108108
message += ' - Addition of {} is the cause'.format(', '.join(new))
109109

110-
raise EnvironmentError(message)
110+
raise DeisException(message)
111111

112112
def save(self, **kwargs):
113113
"""merge the old config with the new"""

rootfs/api/models/key.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from django.conf import settings
44
from django.db import models
5-
from django.core.exceptions import ValidationError
5+
from rest_framework.exceptions import ValidationError
66

77
from api.models import UuidAuditedModel
88
from api.utils import fingerprint
@@ -13,7 +13,7 @@ def validate_base64(value):
1313
try:
1414
base64.b64decode(value.split()[1])
1515
except Exception as e:
16-
raise ValidationError(e)
16+
raise ValidationError(str(e))
1717

1818

1919
class Key(UuidAuditedModel):

0 commit comments

Comments
 (0)