Skip to content

Commit 5ca0046

Browse files
committed
feat(network): add network policy features
1 parent cb20016 commit 5ca0046

11 files changed

Lines changed: 192 additions & 14 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"spec": {
3+
"podSelector": {},
4+
"policyTypes": [
5+
"Ingress"
6+
],
7+
"ingress": [
8+
{
9+
"from": [
10+
{
11+
"namespaceSelector": {
12+
"matchLabels": {
13+
"drycc.cc/workspace_id": "{{workspace_id}}"
14+
}
15+
}
16+
}
17+
]
18+
},
19+
{
20+
"from": [
21+
{
22+
"ipBlock": {
23+
"cidr": "0.0.0.0/0",
24+
"except": [
25+
"10.0.0.0/8",
26+
"172.16.0.0/12",
27+
"192.168.0.0/16"
28+
]
29+
}
30+
}
31+
]
32+
},
33+
{
34+
"from": [
35+
{
36+
"ipBlock": {
37+
"cidr": "::/0",
38+
"except": [
39+
"fc00::/7",
40+
"fe80::/10"
41+
]
42+
}
43+
}
44+
]
45+
}
46+
]
47+
}
48+
}

charts/controller/templates/controller-configmap.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ data:
4141
{{- else}}
4242
{{- .Files.Get "files/volume-claim-template.json" | nindent 4 }}
4343
{{- end }}
44+
network-policy-template.json: |
45+
{{- if .Values.config.networkPolicyTemplate }}
46+
{{- (tpl .Values.config.networkPolicyTemplate $) | nindent 4 }}
47+
{{- else}}
48+
{{- .Files.Get "files/network-policy-template.json" | nindent 4 }}
49+
{{- end }}
4450
volume-usage-template.promql: |
4551
{{- if .Values.config.volumeUsageTemplate }}
4652
{{- (tpl .Values.config.volumeUsageTemplate $) | nindent 4 }}

charts/controller/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ config:
7777
volumeClaimTemplate: ""
7878
volumeUsageTemplate: ""
7979
networkUsageTemplate: ""
80+
networkPolicyTemplate: ""
8081

8182
# Service
8283
service:

rootfs/api/models/app.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from docker import auth as docker_auth
1717
from django.conf import settings
1818
from django.db import models
19+
from django.template import Template, Context
1920
from django.db.models import F, Func, Value, JSONField
2021
from django.contrib.auth import get_user_model
2122
from rest_framework.exceptions import ValidationError
@@ -181,6 +182,7 @@ def create(self, *args, **kwargs): # noqa
181182
self.scheduler.ns.create(namespace)
182183
except KubeException as e:
183184
raise ServiceUnavailable('Could not create the Namespace in Kubernetes') from e
185+
self._create_network_policy()
184186
try:
185187
self.appsettings_set.latest()
186188
except AppSettings.DoesNotExist:
@@ -945,6 +947,27 @@ def _create_default_ingress(self, target_port):
945947
raise DryccException(msg)
946948
route.save()
947949

950+
def _create_network_policy(self):
951+
if not settings.DRYCC_NETWORK_POLICY_TEMPLATE:
952+
return
953+
name = namespace = self.id
954+
json_str = Template(settings.DRYCC_NETWORK_POLICY_TEMPLATE).render(
955+
Context({"workspace_id": str(self.workspace_id)})).strip()
956+
kwargs = json.loads(json_str) if json_str else {}
957+
try:
958+
self.scheduler.networkpolicy.get(namespace, name)
959+
self.scheduler.networkpolicy.patch(namespace, name, **kwargs)
960+
except KubeHTTPException as e:
961+
if e.response.status_code == 404:
962+
try:
963+
self.scheduler.networkpolicy.create(namespace, name, **kwargs)
964+
except KubeException as ex:
965+
raise ServiceUnavailable(
966+
'Could not create the NetworkPolicy in Kubernetes') from ex
967+
else:
968+
raise ServiceUnavailable(
969+
'Could not get the NetworkPolicy in Kubernetes') from e
970+
948971
def _verify_http_health(self, service, **kwargs):
949972
"""
950973
Verify an application is healthy via the svc.
@@ -1083,6 +1106,7 @@ def _build_env_vars(self, release, ptype):
10831106
# mix in default environment information drycc may require
10841107
default_env = {
10851108
'DRYCC_APP': self.id,
1109+
'DRYCC_WORKSPACE': self.workspace_id,
10861110
'WORKFLOW_RELEASE': release.version_name,
10871111
'WORKFLOW_RELEASE_SUMMARY': release.summary,
10881112
'WORKFLOW_RELEASE_CREATED_AT': str(release.created.strftime(

rootfs/api/models/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ def app_log(self, app_id, msg):
6060

6161
@property
6262
def scheduler(self):
63-
annotations = {}
63+
labels = annotations = {}
6464
if hasattr(self, 'app'):
65-
annotations["drycc.cc/app_id"] = str(self.app.id)
66-
annotations["drycc.cc/workspace_id"] = str(self.app.workspace_id)
67-
return get_scheduler(metadata={"annotations": annotations})
65+
labels["drycc.cc/app_id"] = str(self.app.id)
66+
labels["drycc.cc/workspace_id"] = str(self.app.workspace_id)
67+
return get_scheduler(metadata={"labels": labels, "annotations": annotations})
6868

6969

7070
class UuidAuditedModel(AuditedModel):

rootfs/api/settings/production.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,14 @@ def randstr(k):
308308
with open(DRYCC_VOLUME_CLAIM_TEMPLATE_PATH) as fd:
309309
DRYCC_VOLUME_CLAIM_TEMPLATE = fd.read()
310310

311+
# drycc network policy template
312+
DRYCC_NETWORK_POLICY_TEMPLATE = {}
313+
DRYCC_NETWORK_POLICY_TEMPLATE_PATH = os.environ.get(
314+
'DRYCC_NETWORK_POLICY_TEMPLATE_PATH', '/etc/controller/network-policy-template.json')
315+
if os.path.exists(DRYCC_NETWORK_POLICY_TEMPLATE_PATH):
316+
with open(DRYCC_NETWORK_POLICY_TEMPLATE_PATH) as fd:
317+
DRYCC_NETWORK_POLICY_TEMPLATE = fd.read()
318+
311319
# drycc volume usage template
312320
DRYCC_VOLUME_USAGE_TEMPLATE = ""
313321
DRYCC_VOLUME_USAGE_TEMPLATE_PATH = os.environ.get(

rootfs/api/tests/test_app.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ def tearDown(self):
6262
# make sure every test has a clean slate for k8s mocking
6363
cache.clear()
6464

65+
def test_app_network_policy(self, mock_requests):
66+
"""
67+
Test that a network policy is created when an app is created.
68+
"""
69+
with self.settings(DRYCC_NETWORK_POLICY_TEMPLATE='{"spec": {"podSelector": {}}}'):
70+
app_id = self.create_app()
71+
app = App.objects.get(id=app_id)
72+
73+
try:
74+
response = app.scheduler.networkpolicy.get(app.id, app.id).json()
75+
self.assertEqual(response['metadata']['name'], app.id)
76+
except Exception as e:
77+
self.fail(f"Network policy not created: {e}")
78+
6579
def test_app(self, mock_requests):
6680
"""
6781
Test that a user can create, read, update and delete an application
@@ -674,6 +688,7 @@ def test_build_env_vars(self, mock_requests):
674688
app._build_env_vars(app.release_set.latest(), PTYPE_WEB),
675689
{
676690
'DRYCC_APP': app.id,
691+
'DRYCC_WORKSPACE': app.workspace_id,
677692
'WORKFLOW_RELEASE': 'v2',
678693
'PORT': 5000,
679694
'SOURCE_VERSION': '',
@@ -689,6 +704,7 @@ def test_build_env_vars(self, mock_requests):
689704
app._build_env_vars(app.release_set.latest(), PTYPE_WEB),
690705
{
691706
'DRYCC_APP': app.id,
707+
'DRYCC_WORKSPACE': app.workspace_id,
692708
'WORKFLOW_RELEASE': 'v3',
693709
'PORT': 5000,
694710
'SOURCE_VERSION': 'abc1234',

rootfs/scheduler/mock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _acquire(self):
8383
'namespaces', 'nodes', 'pods', 'replicationcontrollers',
8484
'secrets', 'services', 'events', 'deployments', 'replicasets',
8585
'horizontalpodautoscalers', 'scale', 'resourcequotas', 'ingresses',
86-
'persistentvolumeclaims', 'serviceinstances', 'servicebindings',
86+
'persistentvolumeclaims', 'serviceinstances', 'servicebindings', 'networkpolicies',
8787
'limitranges', 'gateways', 'httproutes', 'tcproutes', 'udproutes',
8888
'issuers', 'certificates', 'certificaterequests', 'jobs', 'clusterserviceclasses',
8989
'statefulsets', 'daemonsets',

rootfs/scheduler/resources/deployment.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import time
3+
import copy
34
import logging
45
from datetime import datetime, timedelta, timezone
56
from scheduler.resources import Resource
@@ -32,16 +33,14 @@ def get(self, namespace, name=None, ignore_exception=False, **kwargs):
3233
return response
3334

3435
def manifest(self, namespace, name, image, command, args, spec_annotations, **kwargs):
36+
app_type = kwargs.get('app_type')
3537
replicas = kwargs.get('replicas', 0)
3638
batches = kwargs.get('deploy_batches', None)
3739
tags = kwargs.get('tags', {})
3840
annotations = kwargs.get('annotations', {})
3941

40-
labels = {
41-
'app': namespace,
42-
'type': kwargs.get('app_type'),
43-
'heritage': 'drycc',
44-
}
42+
labels = copy.deepcopy(kwargs.get('labels', {}))
43+
labels.update({'app': namespace, 'type': app_type, 'heritage': 'drycc'})
4544

4645
manifest = {
4746
'kind': 'Deployment',
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from api import utils
2+
from scheduler.resources import Resource
3+
from scheduler.exceptions import KubeHTTPException
4+
5+
6+
class NetworkPolicy(Resource):
7+
api_prefix = 'apis'
8+
api_version = 'networking.k8s.io/v1'
9+
10+
def manifest(self, namespace, name, **kwargs):
11+
data = {
12+
"apiVersion": self.api_version,
13+
"kind": "NetworkPolicy",
14+
"metadata": {
15+
"name": name,
16+
"namespace": namespace,
17+
"labels": {
18+
"heritage": "drycc"
19+
}
20+
}
21+
}
22+
data = utils.dict_merge(data, kwargs)
23+
if "version" in kwargs:
24+
data["metadata"]["resourceVersion"] = kwargs.get("version")
25+
return data
26+
27+
def get(self, namespace, name=None, ignore_exception=False, **kwargs):
28+
"""
29+
Fetch a single NetworkPolicy or a list
30+
"""
31+
if name is not None:
32+
url = self.api("/namespaces/{}/networkpolicies/{}", namespace, name)
33+
message = 'get NetworkPolicy "{}" in Namespace "{}"'.format(name, namespace)
34+
else:
35+
url = self.api("/namespaces/{}/networkpolicies", namespace)
36+
message = 'get NetworkPolicies in Namespace "{}"'.format(namespace)
37+
38+
response = self.http_get(url, params=self.query_params(**kwargs))
39+
if not ignore_exception and self.unhealthy(response.status_code):
40+
raise KubeHTTPException(response, message)
41+
42+
return response
43+
44+
def create(self, namespace, name, ignore_exception=False, **kwargs):
45+
url = self.api("/namespaces/{}/networkpolicies", namespace)
46+
data = self.manifest(namespace, name, **kwargs)
47+
response = self.http_post(url, json=data)
48+
49+
if not ignore_exception and self.unhealthy(response.status_code):
50+
raise KubeHTTPException(
51+
response, 'create NetworkPolicy "{}" in Namespace "{}"', name, namespace)
52+
53+
return response
54+
55+
def patch(self, namespace, name, ignore_exception=False, **kwargs):
56+
url = self.api("/namespaces/{}/networkpolicies/{}", namespace, name)
57+
data = self.manifest(namespace, name, **kwargs)
58+
response = self.http_patch(
59+
url,
60+
json=data,
61+
headers={"Content-Type": "application/merge-patch+json"}
62+
)
63+
if not ignore_exception and self.unhealthy(response.status_code):
64+
raise KubeHTTPException(
65+
response, 'patch NetworkPolicy "{}" in Namespace "{}"', name, namespace)
66+
return response
67+
68+
def delete(self, namespace, name, ignore_exception=False):
69+
url = self.api("/namespaces/{}/networkpolicies/{}", namespace, name)
70+
response = self.http_delete(url)
71+
if not ignore_exception and self.unhealthy(response.status_code):
72+
raise KubeHTTPException(
73+
response, 'delete NetworkPolicy "{}" in Namespace "{}"', name, namespace)
74+
return response

0 commit comments

Comments
 (0)