|
3 | 3 | """ |
4 | 4 | Data models for the Drycc API. |
5 | 5 | """ |
| 6 | +import os |
6 | 7 | import time |
7 | 8 | import hashlib |
8 | 9 | import hmac |
9 | | -import importlib |
10 | 10 | import logging |
11 | | -import morph |
12 | | -import re |
13 | 11 | import urllib.parse |
14 | | -import uuid |
| 12 | +import pkgutil |
| 13 | +import inspect |
15 | 14 | import requests |
16 | 15 | from datetime import timedelta |
17 | 16 | from django.conf import settings |
|
20 | 19 | from django.utils.timezone import now |
21 | 20 | from django.dispatch import receiver |
22 | 21 | from django.contrib.auth import get_user_model |
23 | | -from rest_framework.exceptions import ValidationError |
24 | 22 | from rest_framework.authtoken.models import Token |
25 | | -from requests_toolbelt import user_agent |
26 | | -from scheduler.exceptions import KubeException |
27 | | -from .. import __version__ as drycc_version |
28 | | -from ..exceptions import DryccException, AlreadyExists, ServiceUnavailable, UnprocessableEntity # noqa |
| 23 | +from api.utils import get_session |
| 24 | +from api.tasks import retrieve_resource, send_measurements |
| 25 | +from .app import App |
| 26 | +from .appsettings import AppSettings |
| 27 | +from .build import Build |
| 28 | +from .certificate import Certificate |
| 29 | +from .config import Config |
| 30 | +from .domain import Domain |
| 31 | +from .release import Release |
| 32 | +from .tls import TLS |
| 33 | +from .volume import Volume |
| 34 | +from .resource import Resource |
| 35 | + |
29 | 36 |
|
30 | 37 | User = get_user_model() |
31 | 38 | logger = logging.getLogger(__name__) |
32 | | -session = None |
33 | | - |
34 | | - |
35 | | -def get_session(): |
36 | | - global session |
37 | | - if session is None: |
38 | | - session = requests.Session() |
39 | | - session.headers = { |
40 | | - # https://toolbelt.readthedocs.org/en/latest/user-agent.html#user-agent-constructor |
41 | | - 'User-Agent': user_agent('Drycc Controller', drycc_version), |
42 | | - } |
43 | | - # `mount` a custom adapter that retries failed connections for HTTP and HTTPS requests. |
44 | | - # http://docs.python-requests.org/en/latest/api/#requests.adapters.HTTPAdapter |
45 | | - session.mount('http://', requests.adapters.HTTPAdapter(max_retries=10)) |
46 | | - session.mount('https://', requests.adapters.HTTPAdapter(max_retries=10)) |
47 | | - return session |
48 | | - |
49 | | - |
50 | | -def validate_label(value): |
51 | | - """ |
52 | | - Check that the value follows the kubernetes name constraints |
53 | | - http://kubernetes.io/v1.1/docs/design/identifiers.html |
54 | | - """ |
55 | | - match = re.match(r'^[a-z0-9-]+$', value) |
56 | | - if not match: |
57 | | - raise ValidationError("Can only contain a-z (lowercase), 0-9 and hyphens") |
58 | | - |
59 | | - |
60 | | -class AuditedModel(models.Model): |
61 | | - """Add created and updated fields to a model.""" |
62 | | - |
63 | | - created = models.DateTimeField(auto_now_add=True) |
64 | | - updated = models.DateTimeField(auto_now=True) |
65 | | - |
66 | | - class Meta: |
67 | | - """Mark :class:`AuditedModel` as abstract.""" |
68 | | - abstract = True |
69 | | - |
70 | | - @classmethod |
71 | | - @property |
72 | | - def _scheduler(cls): |
73 | | - mod = importlib.import_module(settings.SCHEDULER_MODULE) |
74 | | - return mod.SchedulerClient(settings.SCHEDULER_URL, settings.K8S_API_VERIFY_TLS) |
75 | | - |
76 | | - def _fetch_service_config(self, app, svc_name=None): |
77 | | - try: |
78 | | - # Get the service from k8s to attach the domain correctly |
79 | | - if svc_name is None: |
80 | | - svc_name = app |
81 | | - svc = self._scheduler.svc.get(app, svc_name).json() |
82 | | - except KubeException as e: |
83 | | - raise ServiceUnavailable('Could not fetch Kubernetes Service {}'.format(app)) from e |
84 | | - |
85 | | - # Get minimum structure going if it is missing on the service |
86 | | - if 'metadata' not in svc or 'annotations' not in svc['metadata']: |
87 | | - default = {'metadata': {'annotations': {}}} |
88 | | - svc = dict_merge(svc, default) |
89 | | - |
90 | | - if 'labels' not in svc['metadata']: |
91 | | - default = {'metadata': {'labels': {}}} |
92 | | - svc = dict_merge(svc, default) |
93 | | - |
94 | | - return svc |
95 | | - |
96 | | - def _load_service_config(self, app, component, svc_name=None): |
97 | | - # fetch setvice definition with minimum structure |
98 | | - svc = self._fetch_service_config(app, svc_name) |
99 | | - |
100 | | - # always assume a .drycc.cc/ ending |
101 | | - component = "%s.drycc.cc/" % component |
102 | | - |
103 | | - # Filter to only include values for the component and strip component out of it |
104 | | - # Processes dots into a nested structure |
105 | | - config = morph.unflatten(morph.pick(svc['metadata']['annotations'], prefix=component)) |
106 | | - |
107 | | - return config |
108 | | - |
109 | | - def _save_service_config(self, app, component, data, svc_name=None): |
110 | | - if svc_name is None: |
111 | | - svc_name = app |
112 | | - # fetch setvice definition with minimum structure |
113 | | - svc = self._fetch_service_config(app, svc_name) |
114 | | - |
115 | | - # always assume a .drycc.cc ending |
116 | | - component = "%s.drycc.cc/" % component |
117 | | - |
118 | | - # add component to data and flatten |
119 | | - data = {"%s%s" % (component, key): value for key, value in list(data.items()) if value} |
120 | | - svc['metadata']['annotations'].update(morph.flatten(data)) |
121 | | - |
122 | | - # Update the k8s service for the application with new service information |
123 | | - try: |
124 | | - self._scheduler.svc.update(app, svc_name, svc) |
125 | | - except KubeException as e: |
126 | | - raise ServiceUnavailable('Could not update Kubernetes Service {}'.format(app)) from e |
127 | | - |
128 | | - |
129 | | -class UuidAuditedModel(AuditedModel): |
130 | | - """Add a UUID primary key to an :class:`AuditedModel`.""" |
131 | | - |
132 | | - uuid = models.UUIDField('UUID', |
133 | | - default=uuid.uuid4, |
134 | | - primary_key=True, |
135 | | - editable=False, |
136 | | - auto_created=True, |
137 | | - unique=True) |
138 | | - |
139 | | - class Meta: |
140 | | - """Mark :class:`UuidAuditedModel` as abstract.""" |
141 | | - abstract = True |
142 | | - |
143 | | - |
144 | | -from .app import App, validate_app_id, validate_reserved_names, validate_app_structure # noqa |
145 | | -from .appsettings import AppSettings # noqa |
146 | | -from .blocklist import Blocklist # noqa |
147 | | -from .build import Build # noqa |
148 | | -from .certificate import Certificate, validate_certificate # noqa |
149 | | -from .config import Config # noqa |
150 | | -from .domain import Domain # noqa |
151 | | -from .service import Service # noqa |
152 | | -from .key import Key, validate_base64 # noqa |
153 | | -from .release import Release # noqa |
154 | | -from .tls import TLS # noqa |
155 | | -from .volume import Volume # noqa |
156 | | -from .resource import Resource # noqa |
157 | | -from ..tasks import retrieve_resource, send_measurements # noqa |
158 | | -from ..utils import dict_merge # noqa |
| 39 | + |
| 40 | + |
| 41 | +# In order to comply with the Django specification, all models need to be imported |
| 42 | +def import_all_models(): |
| 43 | + for _, modname, ispkg in pkgutil.iter_modules([os.path.dirname(__file__)]): |
| 44 | + if not ispkg: |
| 45 | + exec(f"from api.models.{modname} import *") |
| 46 | + for key, value in locals().items(): |
| 47 | + if inspect.isclass(value) and issubclass(value, models.Model): |
| 48 | + globals()[key] = value |
| 49 | + |
| 50 | + |
| 51 | +import_all_models() |
| 52 | + |
159 | 53 |
|
160 | 54 | # define update/delete callbacks for synchronizing |
161 | 55 | # models with the configuration management backend |
|
0 commit comments