Skip to content

Commit 3c6ea6c

Browse files
committed
Merge pull request #702 from helgi/apps_coverage
ref(models): use DRF APIException as a base for all exceptions
2 parents 36fa29b + 4ee8057 commit 3c6ea6c

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)