Skip to content

Commit af5e4b8

Browse files
committed
feat(route): support multi backend
1 parent e25d7ce commit af5e4b8

6 files changed

Lines changed: 94 additions & 101 deletions

File tree

rootfs/api/models/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -938,8 +938,8 @@ def _create_default_ingress(self, target_port):
938938
route.save()
939939
except Route.DoesNotExist:
940940
route = Route(app=self, owner=self.owner, kind="HTTPRoute", name=self.id,
941-
port=DEFAULT_HTTP_PORT, ptype=service.ptype)
942-
route.rules = route.default_rules
941+
rules=[{"backendRefs": [{"kind": "Service", "name": service.name,
942+
"port": DEFAULT_HTTP_PORT, "weight": 100}]}])
943943
attached, msg = route.attach(gateway.name, DEFAULT_HTTP_PORT)
944944
if not attached:
945945
raise DryccException(msg)

rootfs/api/models/gateway.py

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import logging
2+
import threading
23
from django.db import models
34
from django.conf import settings
4-
from django.shortcuts import get_object_or_404
55
from django.contrib.auth import get_user_model
66
from django.db.models import Q
7+
from django.http import Http404
78

89
from api.exceptions import ServiceUnavailable
910
from scheduler import KubeException
1011

11-
from .base import AuditedModel, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, PTYPE_MAX_LENGTH
12+
from .base import AuditedModel, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT
1213

1314
User = get_user_model()
1415
logger = logging.getLogger(__name__)
@@ -167,6 +168,7 @@ class Meta:
167168

168169

169170
class Route(AuditedModel):
171+
CACHE = threading.local()
170172
PROTOCOLS_CHOICES = {
171173
"TLSRoute": ("TCP", ),
172174
"TCPRoute": ("TCP", ),
@@ -180,11 +182,21 @@ class Route(AuditedModel):
180182
kind = models.CharField(max_length=15, choices=[
181183
(key, '/'.join(value)) for key, value in PROTOCOLS_CHOICES.items()])
182184
name = models.CharField(max_length=63, db_index=True)
183-
port = models.PositiveIntegerField()
184185
rules = models.JSONField(default=list)
185186
routable = models.BooleanField(default=True)
186187
parent_refs = models.JSONField(default=list)
187-
ptype = models.CharField(max_length=PTYPE_MAX_LENGTH)
188+
189+
@property
190+
def services(self):
191+
key = f"{self.app.id}_{self.name}"
192+
if not hasattr(self.CACHE, key):
193+
service_names = set()
194+
for rule in self.rules:
195+
for backend in rule['backendRefs']:
196+
service_names.add(backend['name'])
197+
setattr(self.CACHE, key,
198+
[s for s in self.app.service_set.all() if s.name in service_names])
199+
return getattr(self.CACHE, key)
188200

189201
@property
190202
def protocols(self):
@@ -195,30 +207,47 @@ def protocols(self):
195207
@property
196208
def hostnames(self):
197209
return [domain.domain for domain in self.app.domain_set.filter(
198-
ptype=self.ptype)]
210+
ptype__in=[s.ptype for s in self.services])]
211+
212+
@property
213+
def cleaned_rules(self):
214+
services, rules = self.services, []
215+
for rule in self.rules:
216+
backend_refs = []
217+
for backend_ref in rule["backendRefs"]:
218+
for service in services:
219+
ports = [item["port"] for item in service.ports]
220+
if backend_ref["port"] in ports and backend_ref["name"] == service.name:
221+
backend_refs.append(backend_ref)
222+
if backend_refs:
223+
rule['backendRefs'] = backend_refs
224+
rules.append(rule)
225+
return rules
199226

200227
@property
201228
def tls_force_hostnames(self):
202229
tls = self.app.tls_set.latest()
203-
q = Q(ptype=self.ptype)
230+
q = Q(ptype__int=[s.ptype for s in self.services])
204231
if not tls.certs_auto_enabled:
205232
q &= Q(certificate__isnull=False)
206233
domains = self.app.domain_set.filter(q)
207234
return [domain.domain for domain in domains]
208235

209-
@property
210-
def default_rules(self):
211-
service = get_object_or_404(self.app.service_set, ptype=self.ptype)
212-
backend_refs = []
213-
for item in service.ports:
214-
if item["port"] == self.port:
215-
backend_refs.append({
216-
"kind": "Service",
217-
"name": str(service),
218-
"port": item["port"],
219-
"weight": 100,
220-
})
221-
return [{"backendRefs": backend_refs}]
236+
def check_rules(self, rules):
237+
for rule in rules:
238+
for backend_ref in rule["backendRefs"]:
239+
kind = backend_ref.get("kind", "Service")
240+
if kind != "Service":
241+
return False, {"detail": "BackendRef only supports service kind"}
242+
has_service = False
243+
for service in self.services:
244+
ports = [item["port"] for item in service.ports]
245+
if backend_ref["port"] in ports and backend_ref["name"] == service.name:
246+
has_service = True
247+
break
248+
if not has_service:
249+
return False, {"detail": f"service {backend_ref['name']} not exists"}
250+
return True, ""
222251

223252
def log(self, message, level=logging.INFO):
224253
"""Logs a message in the context of this service.
@@ -231,17 +260,6 @@ def log(self, message, level=logging.INFO):
231260
"""
232261
logger.log(level, "[{}]: {}".format(self.app.id, message))
233262

234-
def check_rules(self):
235-
service = self.app.service_set.filter(
236-
ptype=self.ptype).first()
237-
ports = [item["port"] for item in service.ports]
238-
for rule in self.rules:
239-
for backend_ref in rule["backendRefs"]:
240-
port = backend_ref["port"]
241-
if port not in ports or backend_ref["name"] != str(service):
242-
return False, {"detail": "backendRefs associated with incorrect service"}
243-
return True, ""
244-
245263
def refresh_to_k8s(self):
246264
if self.routable:
247265
parent_refs, http_parent_refs = self._get_all_parent_refs()
@@ -290,9 +308,12 @@ def detach(self, gateway_name, port):
290308

291309
def save(self, *args, **kwargs):
292310
self.change_default_tls()
293-
ok, msg = self.check_rules()
294-
if not ok:
295-
raise ValueError(msg)
311+
cleaned_rules = self.cleaned_rules
312+
if not cleaned_rules:
313+
msg = f"route {self.name} no available backend"
314+
self.log(msg, level=logging.ERROR)
315+
raise Http404(msg)
316+
self.rules = cleaned_rules
296317
super().save(*args, **kwargs)
297318
self.refresh_to_k8s()
298319

rootfs/api/serializers/__init__.py

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@
6161
HEALTHCHECK_MISMATCH_MSG = "Healthcheck pattern: %s" % HEALTHCHECK_MATCH.pattern
6262

6363

64+
def validate_port(value):
65+
if not str(value).isnumeric():
66+
raise serializers.ValidationError('port can only be a numeric value')
67+
elif int(value) not in range(1, 65536):
68+
raise serializers.ValidationError('port needs to be between 1 and 65535')
69+
return value
70+
71+
6472
def validate_ptype(value):
6573
if not re.match(PROCTYPE_MATCH, value):
6674
raise serializers.ValidationError(PROCTYPE_MISMATCH_MSG)
@@ -482,14 +490,6 @@ class Meta:
482490
fields = ['owner', 'created', 'updated', 'app', 'ptype']
483491
read_only_fields = ['uuid']
484492

485-
@staticmethod
486-
def validate_port(value):
487-
if not str(value).isnumeric():
488-
raise serializers.ValidationError('port can only be a numeric value')
489-
elif int(value) not in range(1, 65536):
490-
raise serializers.ValidationError('port needs to be between 1 and 65535')
491-
return value
492-
493493
@staticmethod
494494
def validate_protocol(value):
495495
if value is None or value == "":
@@ -502,6 +502,7 @@ def validate_protocol(value):
502502
def validate_target_port(cls, value):
503503
return cls.validate_port(value)
504504

505+
validate_port = staticmethod(validate_port)
505506
validate_ptype = staticmethod(validate_ptype)
506507

507508

@@ -722,51 +723,37 @@ def to_representation(self, instance):
722723
representation['addresses'] = instance.addresses
723724
return representation
724725

725-
@staticmethod
726-
def validate_port(value):
727-
if not str(value).isnumeric():
728-
raise serializers.ValidationError('port can only be a numeric value')
729-
elif int(value) not in range(1, 65536):
730-
raise serializers.ValidationError('port needs to be between 1 and 65535')
731-
return value
732-
733726
@staticmethod
734727
def validate_protocol(value):
735728
if not re.match(GATEWAY_PROTOCOL_MATCH, value):
736729
raise serializers.ValidationError(GATEWAY_PROTOCOL_MISMATCH_MSG)
737730
return value
738731

732+
validate_port = staticmethod(validate_port)
739733
validate_ptype = staticmethod(validate_ptype)
740734

741735

742-
class RouteSerializer(serializers.Serializer):
736+
class RouteSerializer(serializers.ModelSerializer):
743737
app = serializers.SlugRelatedField(slug_field='id', queryset=models.app.App.objects.all())
744738
owner = serializers.ReadOnlyField(source='owner.username')
745739
kind = serializers.CharField(max_length=15, required=False)
746740
name = serializers.CharField(max_length=63, required=True)
747-
port = serializers.IntegerField()
748-
ptype = serializers.CharField(max_length=63, required=True)
749741
rules = serializers.JSONField(required=False)
750742
parent_refs = serializers.JSONField(required=False)
751743

752-
@staticmethod
753-
def validate_port(value):
754-
if not str(value).isnumeric():
755-
raise serializers.ValidationError('port can only be a numeric value')
756-
elif int(value) not in range(1, 65536):
757-
raise serializers.ValidationError('port needs to be between 1 and 65535')
758-
return value
744+
class Meta:
745+
"""Metadata options for a :class:`RouteSerializer`."""
746+
model = models.gateway.Route
747+
fields = '__all__'
759748

760749
@staticmethod
761750
def validate_kind(value):
762751
if not re.match(ROUTE_PROTOCOL_MATCH, value):
763752
raise serializers.ValidationError(ROUTE_PROTOCOL_MISMATCH_MSG)
764753
return value
765754

766-
validate_ptype = staticmethod(validate_ptype)
767-
768755
def validate_rules(self, value):
769-
kind = getattr(self, "initial_data", getattr(self, "data", {})).get("kind", None)
756+
kind = getattr(self, "initial_data", {}).get("kind", None)
770757
if kind:
771758
schema = getattr(rules, f"{kind.replace("Route", "")}_RULES_SCHEMA", "SCHEMA")
772759
else:

rootfs/api/signals.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,11 @@ def gateway_changed_handle(
182182
def service_changed_handle(
183183
sender, instance: Service, created=False, update_fields=None, **kwargs):
184184
if kwargs['signal'] == post_delete:
185-
instance.app.route_set.filter(ptype=instance.ptype).delete()
185+
for route in instance.app.route_set.all():
186+
if route.cleaned_rules:
187+
route.save()
188+
else:
189+
route.delete()
186190

187191

188192
@receiver(signal=[post_save, post_delete], sender=Domain)

rootfs/api/tests/test_gateway.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,16 @@ def create_route(self, app_id, route_name="test-route",
410410
response = self.client.post(
411411
'/v2/apps/{}/routes/'.format(app_id),
412412
{
413-
"port": 5000,
414-
"ptype": ptype,
415413
"kind": kind,
416414
"name": route_name,
415+
"rules": [{
416+
"backendRefs": [{
417+
"kind": "Service",
418+
"name": f"{app_id}-{ptype}",
419+
"port": 5000,
420+
"weight": 100,
421+
}],
422+
}],
417423
}
418424
)
419425
self.assertEqual(response.status_code, 201, response.data)
@@ -433,6 +439,9 @@ def test_create_route(self):
433439
}
434440
)
435441
self.assertEqual(response.status_code, 404)
442+
response = self.client.get('/v2/apps/{}/routes/'.format(app_id))
443+
self.assertEqual(response.data["count"], 1)
444+
self.assertEqual(len(response.data["results"][0]["rules"]), 1)
436445

437446
def test_hostnames(self):
438447
app_id = self.create_app()
@@ -493,13 +502,14 @@ def test_route_check_parent_ok(self):
493502
_, port, route_name = self.create_route(app_id, "myroute1", "test")
494503
gateway_name_1 = 'bing-gateway-1'
495504
self.create_gateway(app_id, gateway_name_1, 5000, "HTTP")
496-
self.client.patch(
505+
response = self.client.patch(
497506
'/v2/apps/{}/routes/{}/attach/'.format(app_id, route_name),
498507
{
499508
"gateway": gateway_name_1,
500509
"port": port
501510
}
502511
)
512+
self.assertEqual(response.status_code, 204, response.data)
503513
response = self.client.get('/v2/apps/{}/routes/'.format(app_id))
504514
self.assertEqual(len(response.data["results"][0]["parent_refs"]), 1)
505515
# create other route

rootfs/api/views.py

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,45 +1086,16 @@ def set(self, request, *args, **kwargs):
10861086
if isinstance(rules, str):
10871087
rules = json.loads(rules)
10881088
rules = self.get_serializer(route, many=False).validate_rules(rules)
1089-
route.rules = rules
1090-
ok, msg = route.check_rules()
1089+
ok, msg = route.check_rules(rules)
10911090
if not ok:
10921091
return Response(status=status.HTTP_400_BAD_REQUEST, data=msg)
1092+
route.rules = rules
10931093
route.save()
10941094
return Response(status=status.HTTP_204_NO_CONTENT)
10951095

1096-
def create(self, request, *args, **kwargs):
1097-
app = self.get_app()
1098-
port = self.get_serializer().validate_port(request.data.get('port'))
1099-
app_settings = app.appsettings_set.latest()
1100-
ptype = self.get_serializer().validate_ptype(
1101-
request.data.get('ptype'))
1102-
kind = self.get_serializer().validate_kind(request.data.get('kind'))
1103-
name = request.data['name']
1104-
route = app.route_set.filter(name=name).first()
1105-
if route:
1106-
return Response(status=status.HTTP_400_BAD_REQUEST, data={
1107-
"detail": f"this route {name} already exists"})
1108-
route = self.model(
1109-
app=app,
1110-
owner=app.owner,
1111-
kind=kind,
1112-
name=name,
1113-
port=port,
1114-
routable=app_settings.routable,
1115-
ptype=ptype,
1116-
)
1117-
route.rules = route.default_rules
1118-
if route.rules and not route.rules[0]["backendRefs"]:
1119-
return Response(status=status.HTTP_400_BAD_REQUEST, data={
1120-
"detail": "this route does not match services. please add service first."
1121-
})
1122-
route.save()
1123-
return Response(status=status.HTTP_201_CREATED)
1124-
11251096
def attach(self, request, *args, **kwargs):
11261097
app = self.get_app()
1127-
port = self.get_serializer().validate_port(request.data.get('port'))
1098+
port = serializers.validate_port(request.data.get('port'))
11281099
gateway_name = request.data['gateway']
11291100
route = get_object_or_404(self.model, app=app, name=kwargs['name'])
11301101
attached, msg = route.attach(gateway_name, port)
@@ -1135,7 +1106,7 @@ def attach(self, request, *args, **kwargs):
11351106

11361107
def detach(self, request, *args, **kwargs):
11371108
app = self.get_app()
1138-
port = self.get_serializer().validate_port(request.data.get('port'))
1109+
port = serializers.validate_port(request.data.get('port'))
11391110
gateway_name = request.data['gateway']
11401111
route = get_object_or_404(self.model, app=app, name=kwargs['name'])
11411112
detached, msg = route.detach(gateway_name, port)

0 commit comments

Comments
 (0)