Skip to content

Commit 6b592cb

Browse files
committed
feat(pipeline): add dryccfile support
1 parent 090a993 commit 6b592cb

33 files changed

Lines changed: 788 additions & 358 deletions

charts/controller/templates/controller-celery-deloyment.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ spec:
3939
- $(DRYCC_REDIS_ADDRS),$(DRYCC_CONTROLLER_API_SERVICE_HOST):$(DRYCC_CONTROLLER_API_SERVICE_PORT)
4040
{{- include "controller.envs" . | indent 8 }}
4141
containers:
42-
{{- range $key := (list "low" "middle" "high") }}
43-
- name: drycc-controller-celery-{{$key}}
42+
- name: drycc-controller-celery
4443
image: {{$.Values.imageRegistry}}/{{$.Values.imageOrg}}/controller:{{$.Values.imageTag}}
4544
imagePullPolicy: {{$.Values.imagePullPolicy}}
4645
{{- if $.Values.diagnosticMode.enabled }}
@@ -50,8 +49,7 @@ spec:
5049
args:
5150
- /bin/bash
5251
- -c
53-
- celery -A api worker -Q {{$key}} --autoscale=32,1 --loglevel=WARNING
52+
- celery --app api worker --queues controller.low,controller.middle,controller.high --autoscale=32,1 --loglevel=WARNING
5453
{{- end }}
5554
{{- include "controller.limits" $ | indent 8 }}
5655
{{- include "controller.envs" $ | indent 8 }}
57-
{{- end }}

rootfs/api/exceptions.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def custom_exception_handler(exc, context):
3939
# Call REST framework's default exception handler after specific 404 handling,
4040
# to get the standard error response.
4141
response = exception_handler(exc, context)
42-
4342
# No response means DRF couldn't handle it
4443
# Output a generic 500 in a JSON format
4544
if response is None:

rootfs/api/management/commands/cluster_lock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def lock(self):
2424
print("lock completed!")
2525

2626
def unlock(self):
27-
cache.set(lock_key, settings.VERSION)
27+
cache.set(lock_key, settings.VERSION, timeout=None)
2828
print("unlock completed!")
2929

3030
def waitting(self):
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 4.2.10 on 2024-04-03 02:03
2+
3+
import api.models.service
4+
from django.db import migrations, models
5+
import functools
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('api', '0003_limitspec_remove_app_procfile_structure_and_more'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='build',
17+
name='dryccfile',
18+
field=models.JSONField(blank=True, default=dict),
19+
),
20+
migrations.AddField(
21+
model_name='release',
22+
name='state',
23+
field=models.TextField(choices=[('created', 'Release created but not deployed'), ('crashed', 'Release pipeline runtime crashed'), ('succeed', 'Release pipeline runtime succeed')], default='created'),
24+
),
25+
migrations.AlterField(
26+
model_name='service',
27+
name='ports',
28+
field=models.JSONField(default=list, validators=[functools.partial(api.models.service.validate_json, *(), **{'schema': {'$schema': 'http://json-schema.org/schema#', 'items': {'properties': {'name': {'type': 'string'}, 'port': {'type': 'integer'}, 'protocol': {'type': 'string'}, 'targetPort': {'type': 'integer'}}, 'required': ['name', 'port', 'protocol', 'targetPort'], 'type': 'object'}, 'minItems': 1, 'type': 'array'}})]),
29+
),
30+
]

rootfs/api/models/app.py

Lines changed: 144 additions & 155 deletions
Large diffs are not rendered by default.

rootfs/api/models/build.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.db import models
44
from django.contrib.auth import get_user_model
55
from api.exceptions import DryccException, Conflict
6+
from api.tasks import run_pipeline
67
from .base import UuidAuditedModel
78

89
User = get_user_model()
@@ -22,6 +23,7 @@ class Build(UuidAuditedModel):
2223
# optional fields populated by builder
2324
sha = models.CharField(max_length=40, blank=True)
2425
procfile = models.JSONField(default=dict, blank=True)
26+
dryccfile = models.JSONField(default=dict, blank=True)
2527
dockerfile = models.TextField(blank=True)
2628

2729
class Meta:
@@ -40,6 +42,12 @@ def type(self):
4042
# container image (or any sort of image) used via drycc pull
4143
return 'image'
4244

45+
@property
46+
def procfile_types(self):
47+
if self.dryccfile:
48+
return list(self.dryccfile['deploy'].keys())
49+
return list(self.procfile.keys())
50+
4351
@property
4452
def source_based(self):
4553
"""
@@ -53,7 +61,16 @@ def source_based(self):
5361
def version(self):
5462
return 'git-{}'.format(self.sha) if self.source_based else 'latest'
5563

56-
def create(self, user, *args, **kwargs):
64+
def get_image(self, procfile_type, default_image=None):
65+
docker = self.dryccfile.get('build', {}).get('docker', {})
66+
if procfile_type in docker:
67+
if procfile_type == 'web':
68+
return self.image
69+
else:
70+
return f'{self.image}-{procfile_type}'
71+
return default_image if default_image else self.image
72+
73+
def create_release(self, user, *args, **kwargs):
5774
app_settings = self.app.appsettings_set.latest()
5875
latest_release = self.app.release_set.filter(failed=False).latest()
5976
latest_version = self.app.release_set.latest().version
@@ -64,22 +81,21 @@ def create(self, user, *args, **kwargs):
6481
config=latest_release.config,
6582
canary=len(app_settings.canaries) > 0,
6683
)
67-
self.app.deploy(new_release)
84+
run_pipeline.delay(new_release)
6885
return new_release
6986
except Exception as e:
7087
# check if the exception is during create or publish
7188
if ('new_release' not in locals() and
7289
self.app.release_set.latest().version == latest_version+1):
7390
new_release = self.app.release_set.latest()
74-
if 'new_release' in locals():
91+
new_release.state = "crashed"
7592
new_release.failed = True
7693
new_release.summary = "{} deployed {} which failed".format(self.owner, str(self.uuid)[:7]) # noqa
7794
# Get the exception that has occured
7895
new_release.exception = "error: {}".format(str(e))
7996
new_release.save()
80-
else:
97+
if 'new_release' not in locals():
8198
self.delete()
82-
8399
raise DryccException(str(e)) from e
84100

85101
def save(self, **kwargs):
@@ -90,14 +106,14 @@ def save(self, **kwargs):
90106
# previous release had a Procfile and the current one does not
91107
(
92108
previous_release.build is not None and
93-
len(previous_release.build.procfile) > 0 and
94-
len(self.procfile) == 0
109+
len(previous_release.procfile_types) > 0 and
110+
len(self.procfile_types) == 0
95111
)
96112
):
97113
# Reject deployment
98114
raise Conflict(
99-
'Last deployment had a Procfile but is missing in this deploy. '
100-
'For a successful deployment provide a Procfile.'
115+
'Last deployment had process types but is missing in this deploy. '
116+
'For a successful deployment provide process types.'
101117
)
102118

103119
# See if processes are permitted to be removed
@@ -107,16 +123,16 @@ def save(self, **kwargs):
107123
# previous release had a Procfile and the current one does as well
108124
(
109125
previous_release.build is not None and
110-
len(previous_release.build.procfile) > 0 and
111-
len(self.procfile) > 0
126+
len(previous_release.procfile_types) > 0 and
127+
len(self.procfile_types) > 0
112128
)
113129
)
114130

115131
# spin down any proc type removed between the last procfile and the newest one
116132
if remove_procs and previous_release.build is not None:
117133
removed = {}
118-
for proc in previous_release.build.procfile:
119-
if proc not in self.procfile and self.app.structure.get(proc, 0) > 0:
134+
for proc in previous_release.procfile_types:
135+
if proc not in self.procfile_types and self.app.structure.get(proc, 0) > 0:
120136
# Scale proc type down to 0
121137
removed[proc] = 0
122138

@@ -127,10 +143,11 @@ def save(self, **kwargs):
127143
if (
128144
settings.DRYCC_DEPLOY_PROCFILE_MISSING_REMOVE is False and
129145
previous_release.build is not None and
130-
len(previous_release.build.procfile) > 0 and
131-
len(self.procfile) == 0
146+
len(previous_release.procfile_types) > 0 and
147+
len(self.procfile_types) == 0
132148
):
133149
self.procfile = previous_release.build.procfile
150+
self.dryccfile = previous_release.build.dryccfile
134151

135152
return super(Build, self).save(**kwargs)
136153

rootfs/api/models/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def _set_limits(self, previous_config):
140140
if key not in data:
141141
raise UnprocessableEntity(
142142
'{} does not exist under {}'.format(key, 'limits'))
143-
if key in self.app.types:
143+
if key in self.app.procfile_types:
144144
raise UnprocessableEntity(
145145
"the %s has already been used and cannot be deleted" % key)
146146
else:

rootfs/api/models/release.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
from django.db import models
55
from django.contrib.auth import get_user_model
66
from api.utils import dict_diff
7+
from api.tasks import run_pipeline
78
from api.exceptions import DryccException, AlreadyExists
89
from scheduler import KubeHTTPException
910
from scheduler.resources.pod import DEFAULT_CONTAINER_PORT
1011
from .base import UuidAuditedModel
1112

13+
1214
User = get_user_model()
1315
logger = logging.getLogger(__name__)
1416

@@ -19,9 +21,14 @@ class Release(UuidAuditedModel):
1921
2022
Releases contain a :class:`Build` and a :class:`Config`.
2123
"""
22-
24+
STATE_CHOICES = (
25+
("created", "Release created but not deployed"),
26+
("crashed", "Release pipeline runtime crashed"),
27+
("succeed", "Release pipeline runtime succeed"),
28+
)
2329
owner = models.ForeignKey(User, on_delete=models.PROTECT)
2430
app = models.ForeignKey('App', on_delete=models.CASCADE)
31+
state = models.TextField(choices=STATE_CHOICES, default=STATE_CHOICES[0][0])
2532
version = models.PositiveIntegerField()
2633
summary = models.TextField(blank=True, null=True)
2734
failed = models.BooleanField(default=False)
@@ -40,8 +47,67 @@ def __str__(self):
4047
return "{0}-v{1}".format(self.app.id, self.version)
4148

4249
@property
43-
def image(self):
44-
return self.build.image
50+
def procfile_types(self):
51+
if self.build is not None:
52+
return self.build.procfile_types
53+
return []
54+
55+
def get_run_image(self):
56+
"""
57+
In the run phase of dryccfile
58+
Return the kubernetes "container image" to be sent off to the scheduler.
59+
"""
60+
image = self.build.dryccfile.get('run', {}).get(
61+
'image', self.build.get_image('run'))
62+
return self.build.get_image(image, default_image=image)
63+
64+
def get_run_args(self):
65+
"""
66+
In the run phase of dryccfile
67+
Return the kubernetes "container arguments" to be sent off to the scheduler.
68+
"""
69+
return self.build.dryccfile.get('run', {}).get('args', [])
70+
71+
def get_run_command(self):
72+
"""
73+
In the run phase of dryccfile
74+
Return the kubernetes "container command" to be sent off to the scheduler.
75+
"""
76+
return self.build.dryccfile.get('run', {}).get('command', [])
77+
78+
def get_deploy_image(self, container_type):
79+
"""
80+
In the deploy phase of dryccfile
81+
Return the kubernetes "container image" to be sent off to the scheduler.
82+
"""
83+
image = self.build.dryccfile.get('deploy', {}).get(container_type, {}).get(
84+
'image', self.build.get_image(container_type))
85+
return self.build.get_image(image, default_image=image)
86+
87+
def get_deploy_args(self, container_type):
88+
"""
89+
In the deploy phase of dryccfile
90+
Return the kubernetes "container arguments" to be sent off to the scheduler.
91+
"""
92+
if self.build is not None:
93+
if self.build.dryccfile:
94+
return self.build.dryccfile['deploy'].get(container_type, {}).get('args', [])
95+
else:
96+
# dockerfile or container image
97+
if self.build.dockerfile or not self.build.sha:
98+
# has profile
99+
if self.build.procfile and container_type in self.build.procfile:
100+
args = self.build.procfile[container_type]
101+
return args.split()
102+
return []
103+
104+
def get_deploy_command(self, container_type):
105+
"""
106+
In the deploy phase of dryccfile
107+
Return the kubernetes "container command" to be sent off to the scheduler.
108+
"""
109+
return self.build.dryccfile.get(
110+
'deploy', {}).get(container_type, {}).get('command', [])
45111

46112
def log(self, message, level=logging.INFO):
47113
"""Logs a message in the context of this application.
@@ -159,14 +225,14 @@ def rollback(self, user, version=None):
159225
)
160226

161227
if self.build is not None:
162-
self.app.deploy(new_release, force_deploy=True)
228+
run_pipeline.delay(new_release, force_deploy=True)
163229
return new_release
164230
except Exception as e:
165231
# check if the exception is during create or publish
166232
if ('new_release' not in locals() and 'latest_version' in locals() and
167233
self.app.release_set.latest().version == latest_version+1):
168234
new_release = self.app.release_set.latest()
169-
if 'new_release' in locals():
235+
new_release.state = "crashed"
170236
new_release.failed = True
171237
new_release.summary = "{} performed roll back to a release that failed".format(self.owner) # noqa
172238
# Get the exception that has occured

rootfs/api/models/service.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,21 @@ def port_name(self, port, protocol):
7272
return "-".join([self.app.id, self.procfile_type, protocol, str(port)]).lower()
7373

7474
def get_port(self, port, protocol):
75-
for port in self.ports:
76-
if port["port"] == port and port["protocol"] == protocol:
77-
return port
75+
for item in self.ports:
76+
if item["port"] == port and item["protocol"] == protocol:
77+
return item
7878
return None
7979

8080
def add_port(self, port, protocol, target_port):
81-
self.ports.append({
82-
"name": self.port_name(port, protocol),
83-
"port": port,
84-
"protocol": protocol,
85-
"targetPort": target_port,
86-
})
81+
if self.get_port(port, protocol) is None:
82+
self.ports.append({
83+
"name": self.port_name(port, protocol),
84+
"port": port,
85+
"protocol": protocol,
86+
"targetPort": target_port,
87+
})
88+
return True
89+
return False
8790

8891
def update_port(self, port, protocol, target_port):
8992
item = self.get_port(port, protocol)
@@ -167,10 +170,4 @@ def _refresh_k8s_svc(self, svc_name):
167170

168171
def _delete_k8s_svc(self, svc_name):
169172
self.log('deleting Service: {}'.format(svc_name), level=logging.DEBUG)
170-
try:
171-
self.scheduler().svc.delete(self._namespace(), svc_name)
172-
except KubeException:
173-
# swallow exception
174-
# raise ServiceUnavailable('Kubernetes service could not be deleted') from e
175-
self.log('Kubernetes service cannot be deleted: {}'.format(svc_name),
176-
level=logging.ERROR)
173+
self.scheduler().svc.delete(self._namespace(), svc_name, ignore_exception=True)

rootfs/api/monitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def query_loadbalancer(namespaces: Iterator[str],
116116
response = requests.get(
117117
urljoin(settings.DRYCC_PROMETHEUS_URL, "/api/v1/query"), params=params)
118118
if response.status_code != 200:
119-
return StopIteration
119+
return
120120
yield from (metric["metric"] for metric in response.json()["data"]["result"])
121121

122122

0 commit comments

Comments
 (0)