Skip to content

Commit 3d21898

Browse files
committed
feat(tags): add ptype tags
1 parent 45cb63c commit 3d21898

14 files changed

Lines changed: 92 additions & 158 deletions

File tree

rootfs/api/models/app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ def _deploy(self, deploys, procfile_types, prev_release,
714714
# Sort deploys so routable comes first
715715
deploys = OrderedDict(sorted(deploys.items(), key=lambda d: d[1].get('routable')))
716716
# Check if any proc type has a Deployment in progress
717-
self._check_deployment_in_progress(deploys, release, force_deploy)
717+
self._check_deployment_in_progress(deploys, force_deploy)
718718

719719
try:
720720
tasks = []
@@ -969,7 +969,7 @@ def _verify_tcp_health(self, service, **kwargs):
969969
else:
970970
time.sleep(3)
971971

972-
def _check_deployment_in_progress(self, deploys, release, force_deploy=False):
972+
def _check_deployment_in_progress(self, deploys, force_deploy=False):
973973
if force_deploy:
974974
return
975975
for scale_type, kwargs in deploys.items():
@@ -1127,7 +1127,7 @@ def _gather_app_settings(self, release, app_settings, procfile_type, replicas, v
11271127
healthcheck = config.healthcheck.get(procfile_type, {})
11281128
volumes, volume_mounts = self._get_volumes_and_mounts(procfile_type, volumes)
11291129
return {
1130-
'tags': config.tags,
1130+
'tags': config.tags.get(procfile_type, {}),
11311131
'envs': envs,
11321132
'registry': config.registry,
11331133
'replicas': replicas,

rootfs/api/models/config.py

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import json
21
import logging
3-
from django.conf import settings
42
from django.db import models
53
from django.contrib.auth import get_user_model
64
from api.utils import dict_diff
@@ -43,20 +41,20 @@ def __str__(self):
4341

4442
def previous(self):
4543
"""
46-
Return the previous Release to this one.
44+
Return the previous Config to this one.
4745
48-
:return: the previous :class:`Release`, or None
46+
:return: the previous :class:`Config`, or None
4947
"""
5048
configs = self.app.config_set
5149
if self.pk:
5250
configs = configs.exclude(pk=self.pk)
5351

5452
try:
55-
# Get the Release previous to this one
56-
prev_release = configs.latest()
57-
except Release.DoesNotExist:
58-
prev_release = None
59-
return prev_release
53+
# Get the Config previous to this one
54+
prev_config = configs.latest()
55+
except Config.DoesNotExist:
56+
prev_config = None
57+
return prev_config
6058

6159
def diff(self, config=None):
6260
old_config = config if config else self.previous()
@@ -76,7 +74,7 @@ def save(self, **kwargs):
7674
# usually means a totally new app
7775
previous_config = self.app.config_set.latest()
7876

79-
for attr in ['tags', 'registry', 'values', 'lifecycle_post_start',
77+
for attr in ['registry', 'values', 'lifecycle_post_start',
8078
'lifecycle_pre_stop', 'termination_grace_period']:
8179
data = getattr(previous_config, attr, {}).copy()
8280
new_data = getattr(self, attr, {}).copy()
@@ -88,7 +86,7 @@ def save(self, **kwargs):
8886
self._set_registry()
8987
self._set_tags(previous_config)
9088
except Config.DoesNotExist:
91-
self._set_tags({'tags': {}})
89+
self._set_tags(previous_config={'tags': {}})
9290

9391
return super(Config, self).save(**kwargs)
9492

@@ -117,34 +115,31 @@ def _set_registry(self):
117115

118116
def _set_tags(self, previous_config):
119117
"""verify the tags exist on any nodes as labels"""
120-
if not self.tags:
121-
if settings.DRYCC_DEFAULT_CONFIG_TAGS:
122-
try:
123-
tags = json.loads(settings.DRYCC_DEFAULT_CONFIG_TAGS)
124-
self.tags = tags
125-
except json.JSONDecodeError as e:
126-
logger.exception(e)
127-
return
118+
data = getattr(previous_config, 'tags', {}).copy()
119+
new_data = getattr(self, 'tags', {}).copy()
120+
# remove config keys if a null value is provided
121+
for procfile_type, values in new_data.items():
122+
if not values:
123+
# error if unsetting non-existing key
124+
if procfile_type not in data:
125+
raise UnprocessableEntity(
126+
'{} does not exist under {}'.format(procfile_type, 'tags'))
127+
data.pop(procfile_type)
128128
else:
129-
return
130-
131-
# Get all nodes with label selectors
132-
nodes = self.scheduler().node.get(labels=self.tags).json()
133-
if nodes['items']:
134-
return
135-
136-
labels = ['{}={}'.format(key, value) for key, value in self.tags.items()]
137-
message = 'No nodes matched the provided labels: {}'.format(', '.join(labels))
138-
139-
# Find out if there are any other tags around
140-
old_tags = getattr(previous_config, 'tags')
141-
if old_tags:
142-
old = ['{}={}'.format(key, value) for key, value in old_tags.items()]
143-
new = set(labels) - set(old)
144-
if new:
145-
message += ' - Addition of {} is the cause'.format(', '.join(new))
146-
147-
raise DryccException(message)
129+
if not self.scheduler().node.get(labels=values).json()['items']:
130+
labels = ['{}={}'.format(key, value) for key, value in values.items()]
131+
message = 'No nodes matched the provided labels: {}'.format(', '.join(labels))
132+
# Find out if there are any other tags around
133+
old_tags = previous_config.tags.get(procfile_type, {})
134+
if old_tags:
135+
old = ['{}={}'.format(key, value) for key, value in old_tags.items()]
136+
new = set(labels) - set(old)
137+
if new:
138+
message += ' - Addition of {} is the cause'.format(', '.join(new))
139+
raise DryccException(message)
140+
data[procfile_type] = self._merge_data(
141+
'tags', data.get(procfile_type, {}), values)
142+
setattr(self, 'tags', data)
148143

149144
def _set_limits(self, previous_config):
150145
data = getattr(previous_config, 'limits', {}).copy()

rootfs/api/serializers/__init__.py

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,7 @@ def validate_values(data):
286286
@classmethod
287287
def validate_typed_values(cls, data):
288288
for procfile_type, values in data.items():
289-
if not re.match(PROCTYPE_MATCH, procfile_type):
290-
raise serializers.ValidationError(PROCTYPE_MISMATCH_MSG)
289+
validate_procfile_type(procfile_type)
291290
if values is None: # use NoneType to unset an item
292291
continue
293292
cls.validate_values(values)
@@ -297,8 +296,7 @@ def validate_typed_values(cls, data):
297296
def validate_limits(data):
298297
req_plan_ids = []
299298
for procfile_type, plan_id in data.items():
300-
if not re.match(PROCTYPE_MATCH, procfile_type):
301-
raise serializers.ValidationError(PROCTYPE_MISMATCH_MSG)
299+
validate_procfile_type(procfile_type)
302300
if plan_id is not None:
303301
req_plan_ids.append(plan_id)
304302
plan_ids = [plan.id for plan in models.limit.LimitPlan.objects.filter(
@@ -311,8 +309,7 @@ def validate_limits(data):
311309
@staticmethod
312310
def validate_termination_grace_period(data):
313311
for procfile_type, value in data.items():
314-
if not re.match(PROCTYPE_MATCH, procfile_type):
315-
raise serializers.ValidationError(PROCTYPE_MISMATCH_MSG)
312+
validate_procfile_type(procfile_type)
316313
if value is None: # use NoneType to unset an item
317314
continue
318315
timeout = re.match(TERMINATION_GRACE_PERIOD_MATCH, str(value))
@@ -324,37 +321,38 @@ def validate_termination_grace_period(data):
324321

325322
@staticmethod
326323
def validate_tags(data):
327-
for key, value in data.items():
328-
if value is None: # use NoneType to unset an item
329-
continue
330-
331-
# split key into a prefix and name
332-
if '/' in key:
333-
prefix, name = key.split('/')
334-
else:
335-
prefix, name = None, key
324+
for procfile_type, values in data.items():
325+
validate_procfile_type(procfile_type)
326+
for key, value in values.items():
327+
if value is None: # use NoneType to unset an item
328+
continue
336329

337-
# validate optional prefix
338-
if prefix:
339-
if len(prefix) > 253:
340-
raise serializers.ValidationError(
341-
"Tag key prefixes must 253 characters or less.")
330+
# split key into a prefix and name
331+
if '/' in key:
332+
prefix, name = key.split('/')
333+
else:
334+
prefix, name = None, key
342335

343-
for part in prefix.split('/'):
344-
if not re.match(TAGVAL_MATCH, part):
336+
# validate optional prefix
337+
if prefix:
338+
if len(prefix) > 253:
345339
raise serializers.ValidationError(
346-
"Tag key prefixes must be DNS subdomains.")
340+
"Tag key prefixes must 253 characters or less.")
347341

348-
# validate required name
349-
if not re.match(TAGVAL_MATCH, name):
350-
raise serializers.ValidationError(
351-
"Tag keys must be alphanumeric or \"-_.\", and 1-63 characters.")
342+
for part in prefix.split('/'):
343+
if not re.match(TAGVAL_MATCH, part):
344+
raise serializers.ValidationError(
345+
"Tag key prefixes must be DNS subdomains.")
352346

353-
# validate value if it isn't empty
354-
if value and not re.match(TAGVAL_MATCH, str(value)):
355-
raise serializers.ValidationError(
356-
"Tag values must be alphanumeric or \"-_.\", and 1-63 characters.")
347+
# validate required name
348+
if not re.match(TAGVAL_MATCH, name):
349+
raise serializers.ValidationError(
350+
"Tag keys must be alphanumeric or \"-_.\", and 1-63 characters.")
357351

352+
# validate value if it isn't empty
353+
if value and not re.match(TAGVAL_MATCH, str(value)):
354+
raise serializers.ValidationError(
355+
"Tag values must be alphanumeric or \"-_.\", and 1-63 characters.")
358356
return data
359357

360358
@staticmethod
@@ -371,8 +369,7 @@ def validate_registry(data):
371369
@staticmethod
372370
def validate_healthcheck(data):
373371
for procfile_type, healthcheck in data.items():
374-
if not re.match(PROCTYPE_MATCH, procfile_type):
375-
raise serializers.ValidationError(PROCTYPE_MISMATCH_MSG)
372+
validate_procfile_type(procfile_type)
376373
if healthcheck is None:
377374
continue
378375
for key, value in healthcheck.items():
@@ -630,8 +627,7 @@ def validate_path(data):
630627
logger.debug(f"mount validate_path data: {data}")
631628
new_data = {}
632629
for key, value in data.items():
633-
if not re.match(PROCTYPE_MATCH, key):
634-
raise serializers.ValidationError(PROCTYPE_MISMATCH_MSG)
630+
validate_procfile_type(key)
635631
if value is None: # use NoneType to unset an item
636632
new_data[key] = value
637633
continue

rootfs/api/settings/production.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,6 @@
361361

362362
DRYCC_APP_POD_EXEC_TIMEOUT = int(os.environ.get('DRYCC_APP_POD_EXEC_TIMEOUT', "3600"))
363363

364-
DRYCC_DEFAULT_CONFIG_TAGS = os.environ.get('DRYCC_DEFAULT_CONFIG_TAGS', '')
365-
366364
KUBERNETES_DEPLOYMENTS_REVISION_HISTORY_LIMIT = os.environ.get(
367365
'KUBERNETES_DEPLOYMENTS_REVISION_HISTORY_LIMIT', None)
368366

rootfs/api/settings/testing.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@
4343
KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS = int(
4444
os.environ.get('KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS', 2))
4545

46-
DRYCC_DEFAULT_CONFIG_TAGS = os.environ.get('DRYCC_DEFAULT_CONFIG_TAGS', '')
47-
4846
DRYCC_APP_STORAGE_CLASS = os.environ.get('DRYCC_APP_STORAGE_CLASS', '')
4947

5048

rootfs/api/tests/test_config.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from io import StringIO
99
from django.contrib.auth import get_user_model
1010
from django.core.cache import cache
11-
from django.conf import settings
1211
from django.core.management import call_command
1312

1413
from unittest import mock
@@ -43,8 +42,6 @@ def setUp(self):
4342
self.app = App.objects.all()[0]
4443

4544
def tearDown(self):
46-
# Restore default tags to empty string
47-
settings.DRYCC_DEFAULT_CONFIG_TAGS = ''
4845
# make sure every test has a clean slate for k8s mocking
4946
cache.clear()
5047

@@ -129,40 +126,6 @@ def test_config(self, mock_requests):
129126
self.assertEqual(response.status_code, 405, response.data)
130127
return config5
131128

132-
def test_default_tags(self, mock_requests):
133-
settings.DRYCC_DEFAULT_CONFIG_TAGS = '{"ssd": "true"}'
134-
app_id = self.create_app()
135-
url = f"/v2/apps/{app_id}/config"
136-
response = self.client.get(url)
137-
expected = {
138-
'owner': self.user.username,
139-
'app': app_id,
140-
'values': {},
141-
'limits': {
142-
PROCFILE_TYPE_RUN: 'std1.large.c1m1',
143-
PROCFILE_TYPE_WEB: 'std1.large.c1m1'
144-
},
145-
'tags': {'ssd': 'true'},
146-
'registry': {}
147-
}
148-
self.assertEqual(response.data, expected | response.data)
149-
150-
# make sure changes not drop tags
151-
body = {'values': json.dumps({'PORT': '5001'})}
152-
response = self.client.post(url, body)
153-
expected = {
154-
'owner': self.user.username,
155-
'app': app_id,
156-
'values': {'PORT': '5001'},
157-
'limits': {
158-
PROCFILE_TYPE_RUN: 'std1.large.c1m1',
159-
PROCFILE_TYPE_WEB: 'std1.large.c1m1'
160-
},
161-
'tags': {'ssd': 'true'},
162-
'registry': {}
163-
}
164-
self.assertEqual(response.data, response.data | expected)
165-
166129
def test_response_data(self, mock_requests):
167130
"""Test that the serialized response contains only relevant data."""
168131
app_id = self.create_app()

rootfs/api/tests/test_event.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77
from django.contrib.auth import get_user_model
88
from django.core.cache import cache
9-
from django.conf import settings
109

1110
from api.models.app import App
1211

@@ -33,8 +32,6 @@ def setUp(self):
3332
self.app = App.objects.all()[0]
3433

3534
def tearDown(self):
36-
# Restore default tags to empty string
37-
settings.DRYCC_DEFAULT_CONFIG_TAGS = ''
3835
# make sure every test has a clean slate for k8s mocking
3936
cache.clear()
4037

rootfs/api/tests/test_ptypes.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77
from django.contrib.auth import get_user_model
88
from django.core.cache import cache
9-
from django.conf import settings
109

1110
from api.models.app import App
1211

@@ -33,8 +32,6 @@ def setUp(self):
3332
self.app = App.objects.all()[0]
3433

3534
def tearDown(self):
36-
# Restore default tags to empty string
37-
settings.DRYCC_DEFAULT_CONFIG_TAGS = ''
3835
# make sure every test has a clean slate for k8s mocking
3936
cache.clear()
4037

rootfs/api/tests/test_resource.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77
from django.contrib.auth import get_user_model
88
from django.core.cache import cache
9-
from django.conf import settings
109
from api.tests import adapter, DryccTransactionTestCase
1110
from api.exceptions import DryccException
1211
import requests_mock
@@ -27,8 +26,6 @@ def setUp(self):
2726
self.app_id = self.create_app()
2827

2928
def tearDown(self):
30-
# Restore default tags to empty string
31-
settings.DRYCC_DEFAULT_CONFIG_TAGS = ''
3229
# make sure every test has a clean slate for k8s mocking
3330
cache.clear()
3431

0 commit comments

Comments
 (0)