Skip to content

Commit 406b415

Browse files
committed
feat(tags): implement tags as k8s node selectors
Implement deis tags as k8s nodeSelector. Uses the /nodes API endpoint to verify if a label already exists or not. Multiple labels compound. This is the first implementation of labelSelector and fieldSelector on a GET call. It will be ported to most of the GET functions eventually
1 parent 0beddda commit 406b415

4 files changed

Lines changed: 71 additions & 20 deletions

File tree

rootfs/api/models.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ class Meta:
121121
"""Mark :class:`AuditedModel` as abstract."""
122122
abstract = True
123123

124+
@property
125+
def _scheduler(self):
126+
mod = importlib.import_module(settings.SCHEDULER_MODULE)
127+
return mod.SchedulerClient(settings.SCHEDULER_URL,
128+
settings.SCHEDULER_AUTH,
129+
settings.SCHEDULER_OPTIONS)
130+
124131

125132
class UuidAuditedModel(AuditedModel):
126133
"""Add a UUID primary key to an :class:`AuditedModel`."""
@@ -160,13 +167,6 @@ def select_app_name(self):
160167

161168
return name
162169

163-
@property
164-
def _scheduler(self):
165-
mod = importlib.import_module(settings.SCHEDULER_MODULE)
166-
return mod.SchedulerClient(settings.SCHEDULER_URL,
167-
settings.SCHEDULER_AUTH,
168-
settings.SCHEDULER_OPTIONS)
169-
170170
def save(self, **kwargs):
171171
if not self.id:
172172
self.id = generate_app_name()
@@ -528,10 +528,6 @@ class Container(UuidAuditedModel):
528528
type = models.CharField(max_length=128, blank=False)
529529
num = models.PositiveIntegerField()
530530

531-
@property
532-
def _scheduler(self):
533-
return self.app._scheduler
534-
535531
@property
536532
def state(self):
537533
return self._scheduler.state(self.job_id).name
@@ -778,6 +774,18 @@ def save(self, **kwargs):
778774
except Config.DoesNotExist:
779775
pass
780776

777+
# verify the tags exist on any nodes as labels
778+
if self.tags:
779+
# Get all nodes with label selectors
780+
nodes = self._scheduler._get_nodes(labels=self.tags).json()
781+
if not nodes['items']:
782+
labels = ['{}={}'.format(key, value) for key, value in self.tags.items()]
783+
raise EnvironmentError(
784+
'These tags do not match labels on kubernetes nodes: {}'.format(
785+
', '.join(labels)
786+
)
787+
)
788+
781789
return super(Config, self).save(**kwargs)
782790

783791

@@ -952,13 +960,6 @@ class Domain(AuditedModel):
952960
app = models.ForeignKey('App')
953961
domain = models.TextField(blank=False, null=False, unique=True)
954962

955-
@property
956-
def _scheduler(self):
957-
mod = importlib.import_module(settings.SCHEDULER_MODULE)
958-
return mod.SchedulerClient(settings.SCHEDULER_URL,
959-
settings.SCHEDULER_AUTH,
960-
settings.SCHEDULER_OPTIONS)
961-
962963
def _fetch_service_config(self, app):
963964
# Get the service from k8s to attach the domain correctly
964965
svc = self._scheduler._get_service(app, app).json()

rootfs/api/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,12 @@ class ConfigViewSet(ReleasableViewSet):
274274
model = models.Config
275275
serializer_class = serializers.ConfigSerializer
276276

277+
def create(self, request, **kwargs):
278+
try:
279+
return super(ConfigViewSet, self).create(request, **kwargs)
280+
except EnvironmentError as e:
281+
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
282+
277283
def post_save(self, config):
278284
release = config.app.release_set.latest()
279285
self.release = release.new(self.request.user, config=config, build=release.build)

rootfs/scheduler/__init__.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@
8080
}
8181
]
8282
}
83-
]
83+
],
84+
"nodeSelector": {}
8485
}
8586
}
8687
}
@@ -156,6 +157,7 @@
156157
]
157158
}
158159
],
160+
"nodeSelector": {},
159161
"volumes":[
160162
{
161163
"name":"minio-user",
@@ -480,8 +482,8 @@ def _create_namespace(self, app_name):
480482
"apiVersion": self.apiversion,
481483
"metadata": {
482484
"name": app_name
483-
}
484485
}
486+
}
485487
resp = self.session.post(url, json=data)
486488
if not resp.status_code == 201:
487489
error(resp, "create Namespace {}".format(app_name))
@@ -622,16 +624,24 @@ def _create_rc(self, name, image, command, **kwargs): # noqa
622624
}
623625
template = string.Template(TEMPLATE).substitute(l)
624626
js_template = json.loads(template)
627+
628+
# apply tags as needed
629+
tags = kwargs.get('tags', {})
630+
js_template["spec"]["template"]["spec"]["nodeSelector"] = tags
631+
632+
# Deal with container information
625633
containers = js_template["spec"]["template"]["spec"]["containers"]
626634
containers[0]['args'] = args
627635
loc = locals().copy()
628636
loc.update(re.match(MATCH, container_fullname).groupdict())
629637
mem = kwargs.get('memory', {}).get(app_type)
630638
cpu = kwargs.get('cpu', {}).get(app_type)
631639
env = kwargs.get('envs', {})
640+
632641
if env:
633642
for k, v in env.items():
634643
containers[0]["env"].append({"name": k, "value": v})
644+
635645
if mem or cpu:
636646
containers[0]["resources"] = {"limits": {}}
637647

@@ -653,11 +663,13 @@ def _create_rc(self, name, image, command, **kwargs): # noqa
653663
if unhealthy(resp.status_code):
654664
error(resp, 'create ReplicationController "{}" in Namespace "{}"',
655665
name, app_name)
666+
656667
create = False
657668
for _ in range(30):
658669
if not create and self._get_rc_status(name, app_name) == 404:
659670
time.sleep(1)
660671
continue
672+
661673
create = True
662674
rc = self._get_rc(name, app_name)
663675
if (
@@ -835,4 +847,30 @@ def _pod_log(self, name, namespace):
835847

836848
return resp.status_code, resp.text, resp.reason
837849

850+
# NODES #
851+
852+
def _get_nodes(self, **kwargs):
853+
path = '/nodes'
854+
query = {}
855+
856+
# labels and fields are encoded slightly differently than python-requests can do
857+
labels = kwargs.get('labels', {})
858+
if labels:
859+
# http://kubernetes.io/v1.1/docs/user-guide/labels.html#list-and-watch-filtering
860+
labels = ['{}={}'.format(key, value) for key, value in labels.items()]
861+
query['labelSelector'] = ','.join(labels)
862+
863+
fields = kwargs.get('fields', {})
864+
if fields:
865+
fields = ['{}={}'.format(key, value) for key, value in fields.items()]
866+
query['fieldSelector'] = ','.join(fields)
867+
868+
url = self._api(path)
869+
response = self.session.get(url, params=query)
870+
if unhealthy(response.status_code):
871+
error(response, 'get Nodes')
872+
873+
return response
874+
875+
838876
SchedulerClient = KubeHTTPClient

rootfs/scheduler/mock.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,11 @@ def _get_service(self, namespace, name):
6464
def _update_service(self, namespace, name, data):
6565
pass
6666

67+
def _get_nodes(self, **kwargs):
68+
resp = requests.Response()
69+
resp.status_code = 200
70+
resp._content = b'{"items": [{"metadata": {"labels": {"env": "prod"}}}]}'
71+
return resp
72+
6773

6874
SchedulerClient = MockSchedulerClient

0 commit comments

Comments
 (0)