Skip to content

Commit 3bd5fce

Browse files
committed
feat(certificate): add cert-manager certificate api
1 parent a610c92 commit 3bd5fce

10 files changed

Lines changed: 196 additions & 245 deletions

File tree

charts/controller/templates/controller-clusterrole.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ rules:
5555
verbs: ["get", "list", "watch", "create", "update", "delete"]
5656
- apiGroups: ["certmanager.k8s.io"]
5757
resources: ["certificates", "issuers"]
58-
verbs: ["get", "list", "watch", "create", "delete", "deletecollection", "patch", "update"]
58+
verbs: ["get", "list", "watch", "create", "update", "delete"]
5959
{{- end -}}
6060
{{- end -}}

rootfs/api/models/app.py

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,68 @@ def _get_entrypoint(self, container_type):
158158
entrypoint = ['/bin/bash', '-c']
159159

160160
return entrypoint
161+
162+
def _refresh_tls(self, certs_auto_enabled, hosts):
163+
namespace = name = self.id
164+
try:
165+
data = self._scheduler.certificate.get(namespace, name).json()
166+
except KubeException:
167+
self.log("certificate {} does not exist".format(namespace), level=logging.INFO)
168+
data = None
169+
170+
if certs_auto_enabled:
171+
if data:
172+
version = data["metadata"]["resourceVersion"]
173+
self._scheduler.certificate.put(
174+
namespace, name, settings.INGRESS_CLASS, hosts, version)
175+
else:
176+
self._scheduler.certificate.create(
177+
namespace, name, settings.INGRESS_CLASS, hosts)
178+
elif data:
179+
self._scheduler.certificate.delete(namespace, name)
180+
181+
def _refresh_ingress(self, hosts, tls_map, ssl_redirect):
182+
ingress = namespace = self.id
183+
# Put Ingress
184+
kwargs = {
185+
"hosts": hosts,
186+
"tls": [{"secretName": k, "hosts": v} for k, v in tls_map.items()],
187+
"ssl_redirect": ssl_redirect
188+
}
189+
whitelist = self.appsettings_set.latest().whitelist
190+
if whitelist: kwargs.update({"whitelist": whitelist})
191+
data = self._scheduler.ingress.get(namespace, ingress).json()
192+
version = data["metadata"]["resourceVersion"]
193+
self._scheduler.ingress.put(
194+
ingress, settings.INGRESS_CLASS, namespace, version, **kwargs)
195+
196+
def _refresh_ingress_and_tls(self):
197+
ingress = self.id
198+
hosts, tls_map = [], {}
199+
200+
tls = self.tls_set.latest()
201+
ssl_redirect = "true" if bool(tls.https_enforced) else "false"
202+
certs_auto_enabled = bool(tls.certs_auto_enabled)
203+
204+
for domain in Domain.objects.filter(app=self):
205+
if str(domain.domain) == self.id:
206+
host = "%s.%s" % (ingress, settings.PLATFORM_DOMAIN)
207+
else:
208+
host = str(domain.domain)
209+
hosts.append(host)
210+
if certs_auto_enabled or domain.certificate:
211+
if certs_auto_enabled:
212+
secret_name = '%s-auto-tls' % self.id
213+
elif domain.certificate:
214+
secret_name = '%s-cert' % domain.certificate.name
215+
if secret_name not in tls_map:
216+
tls_map[secret_name] = []
217+
tls_map[secret_name].append(host)
218+
self._refresh_ingress(hosts, tls_map, ssl_redirect)
219+
self._refresh_tls(certs_auto_enabled, hosts)
220+
221+
def refresh(self):
222+
self._refresh_ingress_and_tls()
161223

162224
def log(self, message, level=logging.INFO):
163225
"""Logs a message in the context of this application.
@@ -190,9 +252,7 @@ def create(self, *args, **kwargs): # noqa
190252
)
191253

192254
# create required minimum resources in k8s for the application
193-
namespace = self.id
194-
ingress = self.id
195-
service = self.id
255+
namespace = ingress = service = self.id
196256
quota_name = '{}-quota'.format(self.id)
197257
try:
198258
self.log('creating Namespace {} and services'.format(namespace), level=logging.DEBUG)
@@ -234,13 +294,13 @@ def create(self, *args, **kwargs): # noqa
234294
if ingress == "":
235295
raise ServiceUnavailable('Empty hostname')
236296
try:
237-
self._scheduler.ingress.get(ingress)
297+
self._scheduler.ingress.get(namespace, ingress)
238298
except KubeException:
239299
self.log("creating Ingress {}".format(namespace), level=logging.INFO)
240300
host = "%s.%s" % (ingress, settings.PLATFORM_DOMAIN)
241301
self._scheduler.ingress.create(
242302
ingress, settings.INGRESS_CLASS, namespace,
243-
hosts=[host, ], tls=[])
303+
hosts=[host, ])
244304
except KubeException as e:
245305
raise ServiceUnavailable('Could not create Ingress in Kubernetes') from e
246306
try:
@@ -572,13 +632,8 @@ def deploy(self, release, force_deploy=False, rollback_on_failure=True): # noqa
572632
# let initial deploy settle before routing traffic to the application
573633
if deploys and app_type:
574634
app_settings = self.appsettings_set.latest()
575-
if app_settings.whitelist:
576-
addresses = ",".join(address for address in app_settings.whitelist)
577-
else:
578-
addresses = None
579635
service_annotations = {
580636
'maintenance': app_settings.maintenance,
581-
'whitelist': addresses
582637
}
583638

584639
routable = deploys[app_type].get('routable')
@@ -950,24 +1005,6 @@ def _update_application_service(self, namespace, app_type, port, routable=False,
9501005
self._scheduler.svc.update(namespace, namespace, data=old_service)
9511006
raise ServiceUnavailable(str(e)) from e
9521007

953-
def whitelist(self, whitelist):
954-
"""
955-
Add/ Delete addresses to application whitelist
956-
"""
957-
service = self._fetch_service_config(self.id)
958-
959-
try:
960-
if whitelist:
961-
addresses = ",".join(address for address in whitelist)
962-
service['metadata']['annotations']['router.drycc.cc/whitelist'] = addresses
963-
elif 'router.drycc.cc/whitelist' in service['metadata']['annotations']:
964-
service['metadata']['annotations'].pop('router.drycc.cc/whitelist', None)
965-
else:
966-
return
967-
self._scheduler.svc.update(self.id, self.id, data=service)
968-
except KubeException as e:
969-
raise ServiceUnavailable(str(e)) from e
970-
9711008
def autoscale(self, proc_type, autoscale):
9721009
"""
9731010
Set autoscale rules for the application

rootfs/api/models/certificate.py

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,10 @@ def attach(self, *args, **kwargs):
172172
domain = get_object_or_404(Domain, domain=kwargs['domain'])
173173
if domain.certificate is not None:
174174
raise AlreadyExists("Domain already has a certificate attached to it")
175-
176-
domain.certificate = self
177-
domain.save()
178-
179175
# create in kubernetes
180176
self.attach_in_kubernetes(domain)
177+
domain.certificate = self
178+
domain.save()
181179

182180
def attach_in_kubernetes(self, domain):
183181
"""Creates the certificate as a kubernetes secret"""
@@ -203,22 +201,6 @@ def attach_in_kubernetes(self, domain):
203201
'{} for {}'.format(name, namespace)
204202
raise ServiceUnavailable(msg) from e
205203

206-
# get config for the service
207-
config = self._load_service_config(namespace, 'router')
208-
209-
# See if certificates are available
210-
if 'certificates' not in config:
211-
config['certificates'] = ''
212-
213-
# convert from string to list to work with and filter out empty strings
214-
cert = '{}:{}'.format(domain.domain, self.name)
215-
certificates = [_f for _f in config['certificates'].split(',') if _f]
216-
if cert not in certificates:
217-
certificates.append(cert)
218-
config['certificates'] = ','.join(certificates)
219-
220-
self._save_service_config(namespace, 'router', config)
221-
222204
def detach(self, *args, **kwargs):
223205
# remove the certificate from the domain
224206
domain = get_object_or_404(Domain, domain=kwargs['domain'])
@@ -235,20 +217,4 @@ def detach(self, *args, **kwargs):
235217
self._scheduler.secret.get(namespace, name)
236218
self._scheduler.secret.delete(namespace, name)
237219
except KubeException as e:
238-
raise ServiceUnavailable("Could not delete certificate secret {} for application {}".format(name, namespace)) from e # noqa
239-
240-
# get config for the service
241-
config = self._load_service_config(namespace, 'router')
242-
243-
# See if certificates are available
244-
if 'certificates' not in config:
245-
config['certificates'] = ''
246-
247-
# convert from string to list to work with and filter out empty strings
248-
cert = '{}:{}'.format(domain.domain, self.name)
249-
certificates = [_f for _f in config['certificates'].split(',') if _f]
250-
if cert in certificates:
251-
certificates.remove(cert)
252-
config['certificates'] = ','.join(certificates)
253-
254-
self._save_service_config(namespace, 'router', config)
220+
raise ServiceUnavailable("Could not delete certificate secret {} for application {}".format(name, namespace)) from e # noqa

rootfs/api/models/domain.py

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.db import models
22
from django.conf import settings
3+
from django.db import transaction
34

45
from api.models import AuditedModel
56

@@ -23,53 +24,24 @@ class Domain(AuditedModel):
2324
class Meta:
2425
ordering = ['domain', 'certificate']
2526

27+
@transaction.atomic
2628
def save(self, *args, **kwargs):
27-
app = str(self.app)
28-
domain = str(self.domain)
29-
30-
# get config for the service
31-
config = self._load_service_config(app, 'router')
32-
33-
# See if domains are available
34-
if 'domains' not in config:
35-
config['domains'] = ''
36-
37-
# convert from string to list to work with and filter out empty strings
38-
domains = [_f for _f in config['domains'].split(',') if _f]
39-
if domain not in domains:
40-
domains.append(domain)
41-
config['domains'] = ','.join(domains)
42-
43-
self._save_service_config(app, 'router', config)
44-
45-
# Save to DB
46-
return super(Domain, self).save(*args, **kwargs)
29+
try:
30+
# Save to DB
31+
return super(Domain, self).save(*args, **kwargs)
32+
finally:
33+
self.app.refresh()
4734

35+
@transaction.atomic
4836
def delete(self, *args, **kwargs):
49-
app = str(self.app)
50-
domain = str(self.domain)
51-
5237
# Deatch cert, updates k8s
5338
if self.certificate:
54-
self.certificate.detach(domain=domain)
55-
56-
# get config for the service
57-
config = self._load_service_config(app, 'router')
58-
59-
# See if domains are available
60-
if 'domains' not in config:
61-
config['domains'] = ''
62-
63-
# convert from string to list to work with and filter out empty strings
64-
domains = [_f for _f in config['domains'].split(',') if _f]
65-
if domain in domains:
66-
domains.remove(domain)
67-
config['domains'] = ','.join(domains)
68-
69-
self._save_service_config(app, 'router', config)
70-
71-
# Delete from DB
72-
return super(Domain, self).delete(*args, **kwargs)
39+
self.certificate.detach(domain=str(self.domain))
40+
try:
41+
# Delete from DB
42+
return super(Domain, self).delete(*args, **kwargs)
43+
finally:
44+
self.app.refresh()
7345

7446
def __str__(self):
7547
return self.domain

rootfs/api/models/tls.py

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class TLS(UuidAuditedModel):
99
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
1010
app = models.ForeignKey('App', on_delete=models.CASCADE)
1111
https_enforced = models.NullBooleanField(default=None)
12+
certs_auto_enabled = models.NullBooleanField(default=None)
1213

1314
class Meta:
1415
get_latest_by = 'created'
@@ -18,57 +19,38 @@ class Meta:
1819
def __str__(self):
1920
return "{}-{}".format(self.app.id, str(self.uuid)[:7])
2021

21-
def _load_service_config(self, app, component):
22-
config = super()._load_service_config(app, component)
23-
24-
# See if the ssl.enforce annotation is available
25-
if 'ssl' not in config:
26-
config['ssl'] = {}
27-
if 'enforce' not in config['ssl']:
28-
config['ssl']['enforce'] = 'false'
29-
30-
return config
31-
3222
def _check_previous_tls_settings(self):
23+
"""
24+
Only one value can be set at a time
25+
If the other value is None, using the previous setting.
26+
"""
3327
try:
3428
previous_tls_settings = self.app.tls_set.latest()
35-
36-
if (
37-
previous_tls_settings.https_enforced is not None and
38-
self.https_enforced == previous_tls_settings.https_enforced
39-
):
40-
self.delete()
41-
raise AlreadyExists("{} changed nothing".format(self.owner))
29+
if self.https_enforced is not None:
30+
if previous_tls_settings.https_enforced == self.https_enforced:
31+
raise AlreadyExists(
32+
"{} changed nothing".format(self.owner))
33+
self.certs_auto_enabled = \
34+
previous_tls_settings.certs_auto_enabled
35+
elif self.certs_auto_enabled is not None:
36+
if previous_tls_settings.certs_auto_enabled == \
37+
self.certs_auto_enabled:
38+
raise AlreadyExists(
39+
"{} changed nothing".format(self.owner))
40+
self.https_enforced = previous_tls_settings.https_enforced
41+
previous_tls_settings.delete()
4242
except TLS.DoesNotExist:
4343
pass
4444

4545
def save(self, *args, **kwargs):
4646
self._check_previous_tls_settings()
47+
try:
48+
# Save to DB
49+
return super(TLS, self).save(*args, **kwargs)
50+
finally:
51+
self.app.refresh()
4752

48-
app = str(self.app)
49-
https_enforced = bool(self.https_enforced)
50-
51-
# get config for the service
52-
config = self._load_service_config(app, 'router')
53-
54-
# convert from bool to string
55-
config['ssl']['enforce'] = str(https_enforced)
56-
57-
self._save_service_config(app, 'router', config)
58-
59-
# Save to DB
60-
return super(TLS, self).save(*args, **kwargs)
6153

6254
def sync(self):
63-
try:
64-
app = str(self.app)
55+
self.app.refresh()
6556

66-
config = self._load_service_config(app, 'router')
67-
if (
68-
config['ssl']['enforce'] != str(self.https_enforced) and
69-
self.https_enforced is not None
70-
):
71-
config['ssl']['enforce'] = str(self.https_enforced)
72-
self._save_service_config(app, 'router', config)
73-
except TLS.DoesNotExist:
74-
pass

rootfs/scheduler/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,23 @@ def http_put(self, path, data=None, **kwargs):
212212

213213
return response
214214

215+
def http_patch(self, path, data=None, **kwargs):
216+
"""
217+
Make a PATCH request to the k8s server.
218+
"""
219+
try:
220+
url = urljoin(self.url, path)
221+
response = self.session.patch(url, data=data, **kwargs)
222+
except requests.exceptions.ConnectionError as err:
223+
# reraise as KubeException, but log stacktrace.
224+
message = "There was a problem patching data to " \
225+
"the Kubernetes API server. URL: {}, " \
226+
"data: {}".format(url, data)
227+
logger.error(message)
228+
raise KubeException(message) from err
229+
230+
return response
231+
215232
def http_delete(self, path, **kwargs):
216233
"""
217234
Make a DELETE request to the k8s server.

0 commit comments

Comments
 (0)