Skip to content

Commit 645c2fe

Browse files
committed
fix(serializers): validate tags properly as k8s labels
1 parent 67e536f commit 645c2fe

2 files changed

Lines changed: 47 additions & 7 deletions

File tree

rootfs/api/serializers.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
PROCTYPE_MATCH = re.compile(r'^(?P<type>[a-z]+)')
1515
MEMLIMIT_MATCH = re.compile(r'^(?P<mem>[0-9]+(MB|KB|GB|[BKMG]))$', re.IGNORECASE)
1616
CPUSHARE_MATCH = re.compile(r'^(?P<cpu>[0-9.]+[m]{0,1})$')
17-
TAGKEY_MATCH = re.compile(r'^[a-z]+$')
18-
TAGVAL_MATCH = re.compile(r'^\w+$')
17+
TAGVAL_MATCH = re.compile(r'^(?:[a-zA-Z\d][-\.\w]{0,61})?[a-zA-Z\d]$')
1918
CONFIGKEY_MATCH = re.compile(r'^[a-z_]+[a-z0-9_]*$', re.IGNORECASE)
2019

2120

@@ -181,11 +180,31 @@ def validate_tags(self, data):
181180
if value is None: # use NoneType to unset an item
182181
continue
183182

184-
if not re.match(TAGKEY_MATCH, key):
185-
raise serializers.ValidationError("Tag keys can only contain [a-z]")
183+
# split key into a prefix and name
184+
if '/' in key:
185+
prefix, name = key.split('/')
186+
else:
187+
prefix, name = None, key
188+
189+
# validate optional prefix
190+
if prefix:
191+
if len(prefix) > 253:
192+
raise serializers.ValidationError(
193+
"Tag key prefixes must 253 characters or less.")
194+
for part in prefix.split('/'):
195+
if not re.match(TAGVAL_MATCH, part):
196+
raise serializers.ValidationError(
197+
"Tag key prefixes must be DNS subdomains.")
198+
199+
# validate required name
200+
if not re.match(TAGVAL_MATCH, name):
201+
raise serializers.ValidationError(
202+
"Tag keys must be alphanumeric or \"-_.\", and 1-63 characters.")
186203

187-
if not re.match(TAGVAL_MATCH, str(value)):
188-
raise serializers.ValidationError("Invalid tag data")
204+
# validate value if it isn't empty
205+
if value and not re.match(TAGVAL_MATCH, str(value)):
206+
raise serializers.ValidationError(
207+
"Tag values must be alphanumeric or \"-_.\", and 1-63 characters.")
189208

190209
return data
191210

rootfs/api/tests/test_config.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,12 +509,33 @@ def test_tags(self):
509509
tags4 = response.data
510510
self.assertNotEqual(tags3['uuid'], tags4['uuid'])
511511
self.assertNotIn('rack', json.dumps(response.data['tags']))
512+
# set valid values
513+
body = {'tags': json.dumps({'kubernetes.io/hostname': 'valid'})}
514+
response = self.client.post(url, json.dumps(body), content_type='application/json',
515+
HTTP_AUTHORIZATION='token {}'.format(self.token))
516+
self.assertEqual(response.status_code, 201)
517+
body = {'tags': json.dumps({'is.valid': 'is-also_valid'})}
518+
response = self.client.post(url, json.dumps(body), content_type='application/json',
519+
HTTP_AUTHORIZATION='token {}'.format(self.token))
520+
self.assertEqual(response.status_code, 201)
521+
body = {'tags': json.dumps({'host.the-name.com/is.valid': 'valid'})}
522+
response = self.client.post(url, json.dumps(body), content_type='application/json',
523+
HTTP_AUTHORIZATION='token {}'.format(self.token))
524+
self.assertEqual(response.status_code, 201)
512525
# set invalid values
513526
body = {'tags': json.dumps({'valid': 'in\nvalid'})}
514527
response = self.client.post(url, json.dumps(body), content_type='application/json',
515528
HTTP_AUTHORIZATION='token {}'.format(self.token))
516529
self.assertEqual(response.status_code, 400)
517-
body = {'tags': json.dumps({'in.valid': 'valid'})}
530+
body = {'tags': json.dumps({'host.name.com/notvalid-': 'valid'})}
531+
response = self.client.post(url, json.dumps(body), content_type='application/json',
532+
HTTP_AUTHORIZATION='token {}'.format(self.token))
533+
self.assertEqual(response.status_code, 400)
534+
body = {'tags': json.dumps({'valid': 'invalid.'})}
535+
response = self.client.post(url, json.dumps(body), content_type='application/json',
536+
HTTP_AUTHORIZATION='token {}'.format(self.token))
537+
self.assertEqual(response.status_code, 400)
538+
body = {'tags': json.dumps({'host.name.com/,not.valid': 'valid'})}
518539
response = self.client.post(url, json.dumps(body), content_type='application/json',
519540
HTTP_AUTHORIZATION='token {}'.format(self.token))
520541
self.assertEqual(response.status_code, 400)

0 commit comments

Comments
 (0)