|
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