Skip to content

Commit a1c4619

Browse files
committed
feat(config): validate PORT and HEALTHCHECK_* values for config:set operations
Fixes #774 Fixes #777 Fixes #781
1 parent b001515 commit a1c4619

2 files changed

Lines changed: 109 additions & 12 deletions

File tree

rootfs/api/serializers.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
import re
7+
from urllib.parse import urlparse
78

89
from django.contrib.auth.models import User
910
from django.utils import timezone
@@ -139,6 +140,39 @@ def validate_values(self, data):
139140
"Config keys must start with a letter or underscore and "
140141
"only contain [A-z0-9_]")
141142

143+
# Validate PORT
144+
if key == 'PORT':
145+
if not str(value).isnumeric():
146+
raise serializers.ValidationError('PORT can only be a numeric value')
147+
elif int(value) not in range(1, 65536):
148+
# check if hte port is between 1 and 65535. One extra added for range()
149+
# http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_serviceport
150+
raise serializers.ValidationError('PORT needs to be between 1 and 65535')
151+
152+
# Validate HEALTHCHECK_*
153+
if key == 'HEALTHCHECK_URL':
154+
# Only Path information is supported, not query / anchor or anything else
155+
# Path is the only thing Kubernetes supports right now
156+
# See https://github.com/deis/controller/issues/774
157+
uri = urlparse(value)
158+
159+
if not uri.path:
160+
raise serializers.ValidationError(
161+
'{} is missing a URI path (such as /healthz). '
162+
'Without it no health check can be done'.format(key)
163+
)
164+
165+
# Disallow everything but path
166+
# https://docs.python.org/3/library/urllib.parse.html
167+
if uri.query or uri.fragment or uri.netloc:
168+
raise serializers.ValidationError(
169+
'{} can only be a URI path (such as /healthz) that does not contain '
170+
'other things such as query params'.format(key)
171+
)
172+
elif key.startswith('HEALTHCHECK_') and not str(value).isnumeric():
173+
# all other healthchecks are integers
174+
raise serializers.ValidationError('{} can only be a numeric value'.format(key))
175+
142176
return data
143177

144178
def validate_memory(self, data):

rootfs/api/tests/test_config.py

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,25 @@ def test_invalid_config_keys(self, mock_requests):
276276
resp = self.client.post(url, body)
277277
self.assertEqual(resp.status_code, 400)
278278

279+
def test_invalid_config_values(self, mock_requests):
280+
"""
281+
Test that invalid config values are rejected.
282+
Right now only PORT is checked
283+
"""
284+
data = [
285+
{'field': 'PORT', 'value': 'dog'},
286+
{'field': 'PORT', 'value': 99999}
287+
]
288+
url = '/v2/apps'
289+
response = self.client.post(url)
290+
self.assertEqual(response.status_code, 201, response.data)
291+
app_id = response.data['id']
292+
url = '/v2/apps/{app_id}/config'.format(**locals())
293+
for row in data:
294+
body = {'values': json.dumps({row['field']: row['value']})}
295+
resp = self.client.post(url, body)
296+
self.assertEqual(resp.status_code, 400, response.data)
297+
279298
def test_admin_can_create_config_on_other_apps(self, mock_requests):
280299
"""If a non-admin creates an app, an administrator should be able to set config
281300
values for that app.
@@ -727,33 +746,77 @@ def test_healthchecks(self, mock_requests):
727746
"""
728747
Test that healthchecks can be applied
729748
"""
730-
url = '/v2/apps'
731-
response = self.client.post(url)
749+
response = self.client.post('/v2/apps')
732750
self.assertEqual(response.status_code, 201, response.data)
733751
app_id = response.data['id']
734752

735-
# Set healthcheck URL to get defaults set
736-
body = {'values': json.dumps({'HEALTHCHECK_INITIAL_DELAY': '25'})}
753+
# Set a healthcheck option before URL is around (URL is required for full setting)
737754
resp = self.client.post(
738755
'/v2/apps/{app_id}/config'.format(**locals()),
739-
body
756+
{'values': json.dumps({'HEALTHCHECK_INITIAL_DELAY': '25'})}
740757
)
741-
self.assertEqual(resp.status_code, 201)
758+
self.assertEqual(resp.status_code, 201, response.data)
742759
self.assertIn('HEALTHCHECK_INITIAL_DELAY', resp.data['values'])
743760
self.assertEqual(resp.data['values']['HEALTHCHECK_INITIAL_DELAY'], '25')
744761

745762
# Set healthcheck URL to get defaults set
746-
body = {'values': json.dumps({'HEALTHCHECK_URL': '/health'})}
747763
resp = self.client.post(
748764
'/v2/apps/{app_id}/config'.format(**locals()),
749-
body
765+
{'values': json.dumps({'HEALTHCHECK_URL': '/health'})}
750766
)
751-
self.assertEqual(resp.status_code, 201)
767+
self.assertEqual(resp.status_code, 201, response.data)
752768
self.assertIn('HEALTHCHECK_URL', resp.data['values'])
753769
self.assertEqual(resp.data['values']['HEALTHCHECK_URL'], '/health')
754770

755771
# post a new build
756-
url = "/v2/apps/{app_id}/builds".format(**locals())
757-
body = {'image': 'quay.io/autotest/example'}
758-
response = self.client.post(url, body)
772+
response = self.client.post(
773+
"/v2/apps/{app_id}/builds".format(**locals()),
774+
{'image': 'quay.io/autotest/example'}
775+
)
776+
self.assertEqual(response.status_code, 201, response.data)
777+
778+
def test_healthchecks_validations(self, mock_requests):
779+
"""
780+
Test that healthchecks validations work
781+
"""
782+
response = self.client.post('/v2/apps')
759783
self.assertEqual(response.status_code, 201, response.data)
784+
app_id = response.data['id']
785+
786+
# Set one of the values that require a numeric value to a string
787+
resp = self.client.post(
788+
'/v2/apps/{app_id}/config'.format(**locals()),
789+
{'values': json.dumps({'HEALTHCHECK_INITIAL_DELAY': 'horse'})}
790+
)
791+
self.assertEqual(resp.status_code, 400, response.data)
792+
793+
# test URL - Path is the only allowed thing
794+
# Try setting various things such as query param
795+
796+
# query param
797+
resp = self.client.post(
798+
'/v2/apps/{app_id}/config'.format(**locals()),
799+
{'values': json.dumps({'HEALTHCHECK_URL': '/health?testing=0'})}
800+
)
801+
self.assertEqual(resp.status_code, 400, response.data)
802+
803+
# fragment
804+
resp = self.client.post(
805+
'/v2/apps/{app_id}/config'.format(**locals()),
806+
{'values': json.dumps({'HEALTHCHECK_URL': '/health#db'})}
807+
)
808+
self.assertEqual(resp.status_code, 400, response.data)
809+
810+
# netloc
811+
resp = self.client.post(
812+
'/v2/apps/{app_id}/config'.format(**locals()),
813+
{'values': json.dumps({'HEALTHCHECK_URL': 'http://someurl.com/health/'})}
814+
)
815+
self.assertEqual(resp.status_code, 400, response.data)
816+
817+
# no path
818+
resp = self.client.post(
819+
'/v2/apps/{app_id}/config'.format(**locals()),
820+
{'values': json.dumps({'HEALTHCHECK_URL': 'http://someurl.com'})}
821+
)
822+
self.assertEqual(resp.status_code, 400, response.data)

0 commit comments

Comments
 (0)