Skip to content

Commit 91a8ec8

Browse files
committed
feat(config): add typed_values
1 parent 777d46d commit 91a8ec8

20 files changed

Lines changed: 429 additions & 358 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.10 on 2024-04-26 04:55
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0004_build_dryccfile_release_state_alter_service_ports'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='config',
15+
name='typed_values',
16+
field=models.JSONField(blank=True, default=dict),
17+
),
18+
]

rootfs/api/models/app.py

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def pipeline(self, release, force_deploy=False, rollback_on_failure=True):
268268
release.save()
269269
self.log(f"{prefix} run completed...")
270270

271-
def deploy(self, release, structure=None, force_deploy=False, rollback_on_failure=True):
271+
def deploy(self, release, procfile_types=None, force_deploy=False, rollback_on_failure=True):
272272
"""
273273
Deploy a new release to this application
274274
@@ -296,16 +296,17 @@ def deploy(self, release, structure=None, force_deploy=False, rollback_on_failur
296296
volumes = self.volume_set.all()
297297
deploys = {}
298298
for scale_type, replicas in self.structure.items():
299-
if structure is not None and scale_type not in structure:
299+
if procfile_types is not None and scale_type not in procfile_types:
300300
continue
301301
scale_type_volumes = [_ for _ in volumes if scale_type in _.path.keys()]
302302
if not release.canary or scale_type in app_settings.canaries:
303303
deploys[scale_type] = self._gather_app_settings(
304304
release, app_settings, scale_type, replicas, volumes=scale_type_volumes)
305-
self._deploy(deploys, structure, prev_release, release, force_deploy, rollback_on_failure)
305+
self._deploy(
306+
deploys, procfile_types, prev_release, release, force_deploy, rollback_on_failure)
306307
# cleanup old release objects from kubernetes
307-
self.cleanup_old()
308-
release.cleanup_old()
308+
self.cleanup_old(procfile_types)
309+
release.cleanup_old(procfile_types)
309310

310311
def mount(self, user, volume, structure=None):
311312
if self.release_set.filter(failed=False).latest().build is None:
@@ -320,15 +321,17 @@ def mount(self, user, volume, structure=None):
320321
app_settings,
321322
structure=structure,
322323
)
323-
self._mount(user, volume, release, app_settings)
324+
self._mount(user, volume, release, app_settings, structure=structure)
324325

325-
def cleanup_old(self):
326+
def cleanup_old(self, procfile_types=None):
326327
names, app_settings = [], self.appsettings_set.latest()
327328
for scale_type in self.structure.keys():
328329
if scale_type in app_settings.canaries:
329330
names.append(self._get_job_id(scale_type, True))
330331
names.append(self._get_job_id(scale_type, False))
331332
labels = {'heritage': 'drycc'}
333+
if procfile_types:
334+
labels["type__in"] = procfile_types
332335
deployments = self.scheduler().deployments.get(self.id, labels=labels).json()["items"]
333336
if deployments is not None:
334337
for deployment in deployments:
@@ -390,12 +393,12 @@ def pod_name(size=5, chars=string.ascii_lowercase + string.digits):
390393
data['restart_policy'] = 'Never'
391394
data['active_deadline_seconds'] = timeout
392395
data['ttl_seconds_after_finished'] = expires
393-
# create application config and build the pod manifest
394-
self.set_application_config(release)
395396
name = self._get_job_id(PROCFILE_TYPE_RUN, release.canary) + '-' + pod_name()
396397
self.log("{} on {} runs '{}'".format(user.username, name, command))
397398
kwargs.update(data)
398399
try:
400+
# create application config and build the pod manifest
401+
self.set_application_config(release, PROCFILE_TYPE_RUN)
399402
self.scheduler().job.create(
400403
self.id,
401404
name,
@@ -506,7 +509,7 @@ def image_pull_secret(self, namespace, registry, image):
506509

507510
return name
508511

509-
def set_application_config(self, release):
512+
def set_application_config(self, release, procfile_type):
510513
"""
511514
Creates the application config as a secret in Kubernetes and
512515
updates it if it already exists
@@ -515,18 +518,19 @@ def set_application_config(self, release):
515518
version = 'v{}'.format(release.version)
516519
labels = {
517520
'version': version,
518-
'type': 'env'
521+
'type': procfile_type,
522+
'class': 'env'
519523
}
520524

521525
# secrets use dns labels for keys, map those properly here
522526
secrets_env = {}
523-
for key, value in self._build_env_vars(release).items():
527+
for key, value in self._build_env_vars(release, procfile_type).items():
524528
secrets_env[key.lower().replace('_', '-')] = str(value)
525529

526530
# dictionary sorted by key
527531
secrets_env = OrderedDict(sorted(secrets_env.items(), key=lambda t: t[0]))
528532

529-
secret_name = "{}-{}-env".format(self.id, version)
533+
secret_name = "{}-{}-{}-env".format(self.id, procfile_type, version)
530534
try:
531535
self.scheduler().secret.get(self.id, secret_name)
532536
except KubeHTTPException:
@@ -578,7 +582,8 @@ def _mount(self, user, volume, release, app_settings, structure=None):
578582
volumes = Volume.objects.filter(app=self)
579583
tasks = []
580584
for scale_type, replicas in structure.items() if structure else self.structure.items():
581-
if not release.canary or scale_type in app_settings.canaries:
585+
if scale_type != PROCFILE_TYPE_RUN and (
586+
not release.canary or scale_type in app_settings.canaries):
582587
replicas = self.structure.get(scale_type, 0)
583588
scale_type_volumes = [
584589
volume for volume in volumes if scale_type in volume.path.keys()]
@@ -588,53 +593,47 @@ def _mount(self, user, volume, release, app_settings, structure=None):
588593
self.id, self._get_job_id(scale_type, release.canary)).json()
589594
spec_annotations = deployment['spec']['template']['metadata'].get(
590595
'annotations', {})
596+
self.set_application_config(release, scale_type)
591597
# gather volume proc types to be deployed
592-
tasks.append(
593-
functools.partial(
594-
self.scheduler().deployment.patch,
595-
namespace=self.id,
596-
name=self._get_job_id(scale_type, release.canary),
597-
image=release.get_deploy_image(scale_type),
598-
command=release.get_deploy_command(scale_type),
599-
args=release.get_deploy_args(scale_type),
600-
spec_annotations=spec_annotations,
601-
resource_version=deployment["metadata"]["resourceVersion"],
602-
**data
603-
)
604-
)
598+
tasks.append(functools.partial(
599+
self.scheduler().deployment.patch,
600+
namespace=self.id,
601+
name=self._get_job_id(scale_type, release.canary),
602+
image=release.get_deploy_image(scale_type),
603+
command=release.get_deploy_command(scale_type),
604+
args=release.get_deploy_args(scale_type),
605+
spec_annotations=spec_annotations,
606+
resource_version=deployment["metadata"]["resourceVersion"],
607+
**data
608+
))
605609
try:
606-
# create the application config in k8s (secret in this case) for all deploy objects
607-
self.set_application_config(release)
608610
apply_tasks(tasks)
609611
except Exception as e:
610612
err = f'(changed volume mount for {volume}: {e}'
611613
self.log(err, logging.ERROR)
612614
raise ServiceUnavailable(err) from e
613615
self.log(f'{user.username} changed volume mount for {volume}')
614616

615-
def _deploy(self, deploys, structure, prev_release,
617+
def _deploy(self, deploys, procfile_types, prev_release,
616618
release, force_deploy, rollback_on_failure):
617619
# Sort deploys so routable comes first
618620
deploys = OrderedDict(sorted(deploys.items(), key=lambda d: d[1].get('routable')))
619621
# Check if any proc type has a Deployment in progress
620622
self._check_deployment_in_progress(deploys, release, force_deploy)
621623

622624
try:
623-
# create the application config in k8s (secret in this case) for all deploy objects
624-
self.set_application_config(release)
625-
626-
# gather all proc types to be deployed
627-
tasks = [
628-
functools.partial(
625+
tasks = []
626+
for scale_type, kwargs in deploys.items():
627+
self.set_application_config(release, scale_type)
628+
tasks.append(functools.partial(
629629
self.scheduler().deploy,
630630
namespace=self.id,
631631
name=self._get_job_id(scale_type, release.canary),
632632
image=release.get_deploy_image(scale_type),
633633
command=release.get_deploy_command(scale_type),
634634
args=release.get_deploy_args(scale_type),
635635
**kwargs
636-
) for scale_type, kwargs in deploys.items()
637-
]
636+
))
638637
try:
639638
apply_tasks(tasks)
640639
except KubeException as e:
@@ -647,7 +646,7 @@ def _deploy(self, deploys, structure, prev_release,
647646
# This goes in the log before the rollback starts
648647
self.log(err, logging.ERROR)
649648
# revert all process types to old release
650-
self.deploy(prev_release, structure,
649+
self.deploy(prev_release, procfile_types,
651650
force_deploy=True, rollback_on_failure=False)
652651
# let it bubble up
653652
raise DryccException('{}\n{}'.format(err, str(e))) from e
@@ -721,6 +720,8 @@ def _scale_pods(self, scale_types, release, app_settings):
721720
volume for volume in volumes if scale_type in volume.path.keys()]
722721
data = self._gather_app_settings(
723722
release, app_settings, scale_type, replicas, volumes=scale_type_volumes)
723+
# create the application config in k8s (secret in this case) for all deploy objects
724+
self.set_application_config(release, scale_type)
724725
# gather all proc types to be deployed
725726
tasks.append(
726727
functools.partial(
@@ -734,8 +735,6 @@ def _scale_pods(self, scale_types, release, app_settings):
734735
)
735736
)
736737
try:
737-
# create the application config in k8s (secret in this case) for all deploy objects
738-
self.set_application_config(release)
739738
apply_tasks(tasks)
740739
except Exception as e:
741740
err = '(scale): {}'.format(e)
@@ -912,7 +911,7 @@ def _scheduler_filter(self, **kwargs):
912911
labels.update({'version': version})
913912
return labels
914913

915-
def _build_env_vars(self, release):
914+
def _build_env_vars(self, release, procfile_type):
916915
"""
917916
Build a dict of env vars, setting default vars based on app type
918917
and then combining with the user set ones
@@ -935,9 +934,9 @@ def _build_env_vars(self, release):
935934
port = release.get_port()
936935
if port:
937936
default_env['PORT'] = port
938-
939937
# merge envs on top of default to make envs win
940938
default_env.update(release.config.values)
939+
default_env.update(release.config.typed_values.get(procfile_type, {}))
941940
return default_env
942941

943942
def _get_private_registry_config(self, image, registry=None):
@@ -999,7 +998,7 @@ def _gather_app_settings(self, release, app_settings, procfile_type, replicas, v
999998
Any global setting that can also be set per app goes here
1000999
"""
10011000

1002-
envs = self._build_env_vars(release)
1001+
envs = self._build_env_vars(release, procfile_type)
10031002
# Obtain a limit plan that must exist, if raise error here, it must be a bug
10041003
config = self._set_default_limit(release.config, procfile_type)
10051004
limit_plan = LimitPlan.objects.get(id=config.limits.get(procfile_type))

rootfs/api/models/config.py

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ class Config(UuidAuditedModel):
1818
during runtime execution of the Application.
1919
"""
2020
procfile_fields = ("lifecycle_post_start", "lifecycle_pre_stop", "tags", "limits",
21-
"healthcheck", "termination_grace_period")
21+
"typed_values", "healthcheck", "termination_grace_period")
2222
all_diff_fields = ("values", "registry") + procfile_fields
2323

2424
owner = models.ForeignKey(User, on_delete=models.PROTECT)
2525
app = models.ForeignKey('App', on_delete=models.CASCADE)
2626
values = models.JSONField(default=dict, blank=True)
27+
typed_values = models.JSONField(default=dict, blank=True)
2728
lifecycle_post_start = models.JSONField(default=dict, blank=True)
2829
lifecycle_pre_stop = models.JSONField(default=dict, blank=True)
2930
tags = models.JSONField(default=dict, blank=True)
@@ -79,17 +80,9 @@ def save(self, **kwargs):
7980
'lifecycle_pre_stop', 'termination_grace_period']:
8081
data = getattr(previous_config, attr, {}).copy()
8182
new_data = getattr(self, attr, {}).copy()
82-
83-
# remove config keys if a null value is provided
84-
for key, value in new_data.items():
85-
if value is None:
86-
# error if unsetting non-existing key
87-
if key not in data:
88-
raise UnprocessableEntity('{} does not exist under {}'.format(key, attr)) # noqa
89-
data.pop(key)
90-
else:
91-
data[key] = value
83+
self._merge_data(attr, data, new_data)
9284
setattr(self, attr, data)
85+
self._set_typed_values(previous_config)
9386
self._set_limits(previous_config)
9487
self._set_healthcheck(previous_config)
9588
self._set_registry()
@@ -99,6 +92,18 @@ def save(self, **kwargs):
9992

10093
return super(Config, self).save(**kwargs)
10194

95+
def _merge_data(self, field, data, new_data):
96+
# remove config keys if a null value is provided
97+
for key, value in new_data.items():
98+
if value is None:
99+
# error if unsetting non-existing key
100+
if key not in data:
101+
raise UnprocessableEntity('{} does not exist under {}'.format(key, field)) # noqa
102+
data.pop(key)
103+
else:
104+
data[key] = value
105+
return data
106+
102107
def _set_registry(self):
103108
# lower case all registry options for consistency
104109
self.registry = {key.lower(): value for key, value in self.registry.copy().items()}
@@ -144,20 +149,13 @@ def _set_tags(self, previous_config):
144149
def _set_limits(self, previous_config):
145150
data = getattr(previous_config, 'limits', {}).copy()
146151
new_data = getattr(self, 'limits', {}).copy()
147-
# remove config keys if a null value is provided
152+
# check procfile
148153
for key, value in new_data.items():
149154
if value is None:
150-
# error if unsetting non-existing key
151-
if key not in data:
152-
raise UnprocessableEntity(
153-
'{} does not exist under {}'.format(key, 'limits'))
154155
if key in self.app.procfile_types:
155156
raise UnprocessableEntity(
156157
"the %s has already been used and cannot be deleted" % key)
157-
else:
158-
data.pop(key)
159-
else:
160-
data[key] = value
158+
self._merge_data('limits', data, new_data)
161159
setattr(self, 'limits', data)
162160

163161
def _set_healthcheck(self, previous_config):
@@ -168,17 +166,25 @@ def _set_healthcheck(self, previous_config):
168166
if value is None:
169167
# error if unsetting non-existing key
170168
if key not in data:
171-
raise UnprocessableEntity('{} does not exist under {}'.format(key, 'healthcheck')) # noqa
169+
raise UnprocessableEntity(
170+
'{} does not exist under {}'.format(key, 'healthcheck'))
172171
data.pop(key)
173172
else:
174-
for probeType, probe in value.items():
175-
if probe is None:
176-
# error if unsetting non-existing key
177-
if key not in data or probeType not in data[key].keys():
178-
raise UnprocessableEntity('{} does not exist under {}'.format(key, 'healthcheck')) # noqa
179-
data[key].pop(probeType)
180-
else:
181-
if key not in data:
182-
data[key] = {}
183-
data[key][probeType] = probe
173+
data[key] = self._merge_data('healthcheck', data.get(key, {}), value)
184174
setattr(self, 'healthcheck', data)
175+
176+
def _set_typed_values(self, previous_config):
177+
data = getattr(previous_config, 'typed_values', {}).copy()
178+
new_data = getattr(self, 'typed_values', {}).copy()
179+
# remove config keys if a null value is provided
180+
for procfile_type, values in new_data.items():
181+
if not values:
182+
# error if unsetting non-existing key
183+
if procfile_type not in data:
184+
raise UnprocessableEntity(
185+
'{} does not exist under {}'.format(procfile_type, 'typed_values'))
186+
data.pop(procfile_type)
187+
else:
188+
data[procfile_type] = self._merge_data(
189+
'typed_values', data.get(procfile_type, {}), values)
190+
setattr(self, 'typed_values', data)

0 commit comments

Comments
 (0)