diff --git a/requirements.txt b/requirements.txt index 313b8b2..d988e02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ PyHamcrest requests_mock dataclasses-json python-dotenv +pytest git+https://github.com/ucsd-ets/awsed_python_client.git@2024.1.2-RC1 diff --git a/tests/app/test_gpu_validator.py b/tests/app/test_gpu_validator.py index 0e357dd..2aec2ac 100644 --- a/tests/app/test_gpu_validator.py +++ b/tests/app/test_gpu_validator.py @@ -1,246 +1,71 @@ import inspect from operator import contains +from dsmlp.app.types import ValidationFailure from dsmlp.app.validator import Validator from dsmlp.plugin.awsed import ListTeamsResponse, TeamJson, UserResponse from dsmlp.plugin.kube import Namespace from hamcrest import assert_that, contains_inanyorder, equal_to, has_item from tests.fakes import FakeAwsedClient, FakeLogger, FakeKubeClient from dsmlp.ext.kube import DefaultKubeClient +from dsmlp.app.gpu_validator import GPUValidator +from tests.app.utils import gen_request, try_val_with_component -class TestValidator: + +class TestGPUValidator: def setup_method(self) -> None: self.logger = FakeLogger() self.awsed_client = FakeAwsedClient() self.kube_client = FakeKubeClient() - self.awsed_client.add_user('user10', UserResponse(uid=10, enrollments=[])) + self.awsed_client.add_user( + 'user10', UserResponse(uid=10, enrollments=[])) self.awsed_client.add_teams('user10', ListTeamsResponse( teams=[TeamJson(gid=1000)] )) - - self.kube_client.add_namespace('user10', Namespace(name='user10', labels={'k8s-sync': 'true'}, gpu_quota=10)) + + self.kube_client.add_namespace('user10', Namespace( + name='user10', labels={'k8s-sync': 'true'}, gpu_quota=10)) self.kube_client.set_existing_gpus('user10', 0) def test_no_gpus_requested(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "metadata": { - "labels": {} - }, - "kind": "Pod", - "spec": { - "containers": [{}] - } - } - }} + self.try_validate( + gen_request(), expected=True, message="Allowed" ) - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": True, "status": { - "message": "Allowed" - }}})) - def test_quota_not_reached(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "metadata": { - "labels": {} - }, - "kind": "Pod", - "spec": { - "containers": [{ - "resources": { - "requests": { - "nvidia.com/gpu": 10 - } - } - }] - } - } - }} - ) - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": True, "status": { - "message": "Allowed" - }}})) + self.try_validate( + gen_request(gpu_req=10), expected=True, message="Allowed" + ) def test_quota_exceeded(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "metadata": { - "labels": {} - }, - "kind": "Pod", - "spec": { - "containers": [{ - "resources": { - "requests": { - "nvidia.com/gpu": 11 - } - } - }] - } - } - }} + + self.try_validate( + gen_request(gpu_req=11), expected=False, message="GPU quota exceeded. Wanted 11 but with 0 already in use, the quota of 10 would be exceeded." ) - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, "status": { - "message": "GPU quota exceeded. Wanted 11 but with 0 already in use, the quota of 10 would be exceeded." - }}})) - def test_sum_exceeded(self): self.kube_client.set_existing_gpus('user10', 5) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "metadata": { - "labels": {} - }, - "kind": "Pod", - "spec": { - "containers": [{ - "resources": { - "requests": { - "nvidia.com/gpu": 6 - } - } - }] - } - } - }} + + self.try_validate( + gen_request(gpu_req=6), expected=False, message="GPU quota exceeded. Wanted 6 but with 5 already in use, the quota of 10 would be exceeded." ) - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, "status": { - "message": "GPU quota exceeded. Wanted 6 but with 5 already in use, the quota of 10 would be exceeded." - }}})) - def test_low_priority(self): self.kube_client.set_existing_gpus('user10', 5) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "metadata": { - "labels": {} - }, - "kind": "Pod", - "spec": { - "containers": [{ - "resources": { - "requests": { - "nvidia.com/gpu": 6 - } - } - }], - "priorityClassName": "low" - } - } - }} + + self.try_validate( + gen_request(gpu_req=6, low_priority=True), expected=True ) - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": True, "status": { - "message": "Allowed" - }}})) - # Should respond to limit as well as request def test_limit_exceeded(self): self.kube_client.set_existing_gpus('user10', 5) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "metadata": { - "labels": {} - }, - "kind": "Pod", - "spec": { - "containers": [{ - "resources": { - "limits": { - "nvidia.com/gpu": 6 - } - } - }] - } - } - }} - ) - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, "status": { - "message": "GPU quota exceeded. Wanted 6 but with 5 already in use, the quota of 10 would be exceeded." - }}})) - - def when_validate(self, json): - validator = Validator(self.awsed_client, self.kube_client, self.logger) - response = validator.validate_request(json) + self.try_validate( + gen_request(gpu_lim=6), expected=False, message="GPU quota exceeded. Wanted 6 but with 5 already in use, the quota of 10 would be exceeded." + ) - return response + def try_validate(self, json, expected: bool, message: str = None): + try_val_with_component(GPUValidator( + self.kube_client, self.logger), json, expected, message) diff --git a/tests/app/test_id_validator.py b/tests/app/test_id_validator.py index 6d7404e..de9e384 100644 --- a/tests/app/test_id_validator.py +++ b/tests/app/test_id_validator.py @@ -1,165 +1,56 @@ import inspect from operator import contains +from dsmlp.app.types import * +from dsmlp.app.id_validator import IDValidator from dsmlp.app.validator import Validator from dsmlp.plugin.awsed import ListTeamsResponse, TeamJson, UserResponse from dsmlp.plugin.kube import Namespace from hamcrest import assert_that, contains_inanyorder, equal_to, has_item +from tests.app.utils import gen_request, try_val_with_component from tests.fakes import FakeAwsedClient, FakeLogger, FakeKubeClient -class TestValidator: +class TestIDValidator: def setup_method(self) -> None: self.logger = FakeLogger() self.awsed_client = FakeAwsedClient() self.kube_client = FakeKubeClient() - self.awsed_client.add_user('user10', UserResponse(uid=10, enrollments=[])) + self.awsed_client.add_user( + 'user10', UserResponse(uid=10, enrollments=[])) self.awsed_client.add_teams('user10', ListTeamsResponse( teams=[TeamJson(gid=1000)] )) - self.kube_client.add_namespace('user10', Namespace(name='user10', labels={'k8s-sync': 'true'}, gpu_quota=10)) - - def test_log_request_details(self): - self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "namespace": "user10", - "userInfo": { - "username": "system:kube-system" - }, - "object": { - "metadata": { - "labels": {} - }, - "spec": { - "containers": [{}] - }, - } - } - } - ) - - assert_that(self.logger.messages, has_item( - "INFO Allowed request username=system:kube-system namespace=user10 uid=705ab4f5-6393-11e8-b7cc-42010a800002")) + self.kube_client.add_namespace('user10', Namespace( + name='user10', labels={'k8s-sync': 'true'}, gpu_quota=10)) def test_course_enrollment(self): - self.awsed_client.add_user('user1', UserResponse(uid=1, enrollments=["course1"])) - self.kube_client.add_namespace('user1', Namespace(name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user1" - }, - "namespace": "user1", - "object": { - "metadata": { - "labels": { - "dsmlp/course": "course1" - } - }, - "spec": { - "securityContext": { - "runAsUser": 1 - }, - "containers": [] - }, - } - } - } - ) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": True, - "status": { - "message": "Allowed" - }}})) + self.awsed_client.add_user( + 'user1', UserResponse(uid=1, enrollments=["course1"])) + self.kube_client.add_namespace('user1', Namespace( + name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) + + self.try_validate(gen_request( + course="course1", username="user1", run_as_user=1, has_container=False), True, "Allowed") def test_pod_security_context(self): - self.awsed_client.add_user('user1', UserResponse(uid=1, enrollments=[])) - self.kube_client.add_namespace('user1', Namespace(name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user1" - }, - "namespace": "user1", - "object": { - "metadata": { - "labels": {} - }, - "spec": { - "securityContext": { - "runAsUser": 1 - }, - "containers": [] - }, - } - } - } - ) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": True, - "status": { - "message": "Allowed" - }}})) + self.awsed_client.add_user( + 'user1', UserResponse(uid=1, enrollments=[])) + self.kube_client.add_namespace('user1', Namespace( + name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) + + self.try_validate(gen_request( + username="user1", run_as_user=1, has_container=False), True, "Allowed") def test_security_context(self): - self.awsed_client.add_user('user1', UserResponse(uid=1, enrollments=[])) - self.kube_client.add_namespace('user1', Namespace(name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user1" - }, - "namespace": "user1", - "object": { - "metadata": { - "labels": {} - }, - "spec": { - "securityContext": { - "runAsUser": 1 - }, - "containers": [ - { - "securityContext": { - "runAsUser": 1 - } - } - ] - } - } - } - } - ) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": True, - "status": {"message": "Allowed"}}})) + self.awsed_client.add_user( + 'user1', UserResponse(uid=1, enrollments=[])) + self.kube_client.add_namespace('user1', Namespace( + name='user1', labels={'k8s-sync': 'true'}, gpu_quota=10)) + + self.try_validate(gen_request( + username="user1", run_as_user=1, has_container=True), True, "Allowed") def test_deny_security_context(self): """ @@ -167,86 +58,16 @@ def test_deny_security_context(self): but the PodSecurityContext.runAsUser doesn't belong to them. Deny the request. """ - self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user2" - }, - "namespace": "user2", - "object": { - "metadata": { - "labels": {} - }, - "spec": { - "securityContext": {"runAsUser": 3}, - "containers": [] - } - } - }}) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, - "status": { - "message": "spec.securityContext: uid must be in range [2]" - }}})) - - def test_failures_are_logged(self): - self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user2" - }, - "namespace": "user2", - "object": { - "metadata": { - "labels": {} - }, - "spec": { - "containers": [], - "securityContext": {"runAsUser": 3}}, - }}}) - - assert_that(self.logger.messages, has_item( - f"INFO Denied request username=user2 namespace=user2 reason={response['response']['status']['message']} uid=705ab4f5-6393-11e8-b7cc-42010a800002")) + self.awsed_client.add_user( + 'user2', UserResponse(uid=2, enrollments=[])) + + self.try_validate(gen_request( + username="user2", run_as_user=3, has_container=False), False, "spec.securityContext: uid must be in range [2]") def test_deny_unknown_user(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user2" - }, - "namespace": "user2", - "object": { - "metadata": { - "labels": {} - }, - "spec": { - "containers": [], - "securityContext": {"runAsUser": 2}}, - }}}) - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, - "status": { - "message": "namespace: no AWSEd user found with username user2" - }}})) + + self.try_validate(gen_request( + username="user2", run_as_user=2, has_container=False), False, "namespace: no AWSEd user found with username user2") def test_deny_course_enrollment(self): """ @@ -254,75 +75,18 @@ def test_deny_course_enrollment(self): but they are not enrolled in the course in the label "dsmlp/course". Deny the request. """ - self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user2" - }, - "namespace": "user2", - "object": { - "metadata": { - "labels": { - "dsmlp/course": "course1" - } - }, - "spec": { - "securityContext": {"runAsUser": 2}, - "containers": [] - } - } - }}) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, - "status": { - "message": "metadata.labels: dsmlp/course must be in range []" - }}})) + self.awsed_client.add_user( + 'user2', UserResponse(uid=2, enrollments=[])) + + self.try_validate(gen_request( + course="course1", username="user2", run_as_user=2, has_container=False), False, "metadata.labels: dsmlp/course must be in range []") def test_deny_pod_security_context(self): - self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user2" - }, - "namespace": "user2", - "object": { - "kind": "Pod", - "metadata": { - "labels": {} - }, - "spec": { - "securityContext": {"runAsUser": 2}, - "containers": [ - {}, - { - "securityContext": {"runAsUser": 3} - } - ] - } - } - }}) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, "status": { - "message": "spec.containers[1].securityContext: uid must be in range [2]" - }}})) + self.awsed_client.add_user( + 'user2', UserResponse(uid=2, enrollments=[])) + + self.try_validate(gen_request( + username="user2", run_as_user=2, container_override=[Container(), Container(securityContext=SecurityContext(runAsUser=3))]), False, "spec.containers[1].securityContext: uid must be in range [2]") def test_deny_init_container(self): """ @@ -330,42 +94,11 @@ def test_deny_init_container(self): but the uid doesn't belong to them. Deny the request. """ - self.awsed_client.add_user('user2', UserResponse(uid=2, enrollments=[])) - - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user2" - }, - "namespace": "user2", - "object": { - "kind": "Pod", - "metadata": { - "labels": {} - }, - "spec": { - "containers": [{}], - "initContainers": [ - {}, - { - "securityContext": {"runAsUser": 99} - } - ] - } - } - }}) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, - "status": { - "message": "spec.initContainers[1].securityContext: uid must be in range [2]" - }}})) + self.awsed_client.add_user( + 'user2', UserResponse(uid=2, enrollments=[])) + + self.try_validate(gen_request( + username="user2", run_as_user=2, container_override=[Container()], init_containers=[Container(), Container(securityContext=SecurityContext(runAsUser=99))]), False, "spec.initContainers[1].securityContext: uid must be in range [2]") def test_deny_pod_security_context2(self): """ @@ -373,227 +106,38 @@ def test_deny_pod_security_context2(self): It should be launched. """ - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "kind": "Pod", - "metadata": { - "labels": {} - }, - "spec": { - "containers": [{}] - } - } - }}) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": True, "status": { - "message": "Allowed" - }}})) + self.try_validate(gen_request( + username="user10", container_override=[Container()]), True, "Allowed") # check podSecurityContext.runAsGroup def test_deny_team_gid(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "kind": "Pod", - "metadata": { - "labels": {} - }, - "spec": { - "securityContext": {"runAsGroup": 2}, - "containers": [{}] - } - } - }} - ) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, "status": { - "message": "spec.securityContext: gid must be in range [1000, 0, 100]" - }}})) + + self.try_validate(gen_request( + username="user10", run_as_group=2, container_override=[Container()]), False, "spec.securityContext: gid must be in range [1000, 0, 100]") # check podSecurityContext.fsGroup def test_deny_pod_fsGroup(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "kind": "Pod", - "metadata": { - "labels": {} - }, - "spec": { - "securityContext": {"fsGroup": 2}, - "containers": [{}] - } - } - }} - ) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, "status": { - "message": "spec.securityContext: gid must be in range [1000, 0, 100]" - }}})) + + self.try_validate(gen_request( + username="user10", fs_group=2, container_override=[Container()]), False, "spec.securityContext: gid must be in range [1000, 0, 100]") # check podSecurityContext.supplementalGroups def test_deny_pod_supplemental_groups(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "kind": "Pod", - "metadata": { - "labels": {} - }, - "spec": { - "securityContext": {"supplementalGroups": [2]}, - "containers": [{}] - } - } - }} - ) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, "status": { - "message": "spec.securityContext: gid must be in range [1000, 0, 100]" - }}})) + + self.try_validate(gen_request( + username="user10", supplemental_groups=[2], container_override=[Container()]), False, "spec.securityContext: gid must be in range [1000, 0, 100]") # check container.securityContext.runAsGroup def test_deny_container_run_as_group(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "kind": "Pod", - "metadata": { - "labels": {} - }, - "spec": { - "containers": [ - { - "securityContext": {"runAsGroup": 2} - } - ] - } - } - }} - ) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": False, "status": { - "message": "spec.containers[0].securityContext: gid must be in range [1000, 0, 100]" - }}})) + + self.try_validate(gen_request( + username="user10", container_override=[Container(securityContext=SecurityContext(runAsGroup=2))]), False, "spec.containers[0].securityContext: gid must be in range [1000, 0, 100]") def test_allow_gid_0_and_100a(self): - response = self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "kind": "Pod", - "metadata": { - "labels": {} - }, - "spec": { - "securityContext": {"runAsGroup": 0}, - "containers": [ - { - "securityContext": {"runAsGroup": 100} - } - ] - } - } - } - } - ) - - assert_that(response, equal_to({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "allowed": True, "status": { - "message": "Allowed" - }}})) - - def test_log_allowed_requests(self): - self.when_validate( - { - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "userInfo": { - "username": "user10" - }, - "namespace": "user10", - "object": { - "metadata": { - "labels": {} - }, - "spec": { - "containers": [{}] - } - } - } - } - ) - - assert_that(self.logger.messages, has_item( - "INFO Allowed request username=user10 namespace=user10 uid=705ab4f5-6393-11e8-b7cc-42010a800002")) - - def when_validate(self, json): - validator = Validator(self.awsed_client, self.kube_client, self.logger) - response = validator.validate_request(json) - - return response \ No newline at end of file + + self.try_validate(gen_request( + username="user10", run_as_group=0, container_override=[Container(securityContext=SecurityContext(runAsGroup=100))]), True, "Allowed") + + def try_validate(self, json, expected: bool, message: str = None): + try_val_with_component(IDValidator( + self.awsed_client, self.logger), json, expected, message) diff --git a/tests/app/test_logs.py b/tests/app/test_logs.py new file mode 100644 index 0000000..76c0ac7 --- /dev/null +++ b/tests/app/test_logs.py @@ -0,0 +1,101 @@ +import inspect +from operator import contains +from dsmlp.app.validator import Validator +from dsmlp.plugin.awsed import ListTeamsResponse, TeamJson, UserResponse +from dsmlp.plugin.kube import Namespace +from hamcrest import assert_that, contains_inanyorder, equal_to, has_item +from tests.fakes import FakeAwsedClient, FakeLogger, FakeKubeClient + + +class TestLogs: + def setup_method(self) -> None: + self.logger = FakeLogger() + self.awsed_client = FakeAwsedClient() + self.kube_client = FakeKubeClient() + + self.awsed_client.add_user( + 'user10', UserResponse(uid=10, enrollments=[])) + self.awsed_client.add_teams('user10', ListTeamsResponse( + teams=[TeamJson(gid=1000)] + )) + + self.kube_client.add_namespace('user10', Namespace( + name='user10', labels={'k8s-sync': 'true'}, gpu_quota=10)) + + def test_log_request_details(self): + self.when_validate( + { + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "namespace": "user10", + "userInfo": { + "username": "system:kube-system" + }, + "object": { + "metadata": { + "labels": {} + }, + "spec": { + "containers": [{}] + }, + } + } + } + ) + + assert_that(self.logger.messages, has_item( + "INFO Allowed request username=system:kube-system namespace=user10 uid=705ab4f5-6393-11e8-b7cc-42010a800002")) + + def test_failures_are_logged(self): + self.awsed_client.add_user( + 'user2', UserResponse(uid=2, enrollments=[])) + + response = self.when_validate( + { + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "userInfo": { + "username": "user2" + }, + "namespace": "user2", + "object": { + "metadata": { + "labels": {} + }, + "spec": { + "containers": [], + "securityContext": {"runAsUser": 3}}, + }}}) + + assert_that(self.logger.messages, has_item( + f"INFO Denied request username=user2 namespace=user2 reason={response['response']['status']['message']} uid=705ab4f5-6393-11e8-b7cc-42010a800002")) + + def test_log_allowed_requests(self): + self.when_validate( + { + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "userInfo": { + "username": "user10" + }, + "namespace": "user10", + "object": { + "metadata": { + "labels": {} + }, + "spec": { + "containers": [{}] + } + } + } + } + ) + + assert_that(self.logger.messages, has_item( + "INFO Allowed request username=user10 namespace=user10 uid=705ab4f5-6393-11e8-b7cc-42010a800002")) + + def when_validate(self, json): + validator = Validator(self.awsed_client, self.kube_client, self.logger) + response = validator.validate_request(json) + + return response diff --git a/tests/app/utils.py b/tests/app/utils.py new file mode 100644 index 0000000..47a6a0f --- /dev/null +++ b/tests/app/utils.py @@ -0,0 +1,77 @@ +from hamcrest import assert_that, equal_to +from dsmlp.app.config import GPU_LABEL +from dsmlp.app.validator import Validator +from src.dsmlp.app.types import * +from typing import List + + +def gen_request(gpu_req: int = 0, gpu_lim: int = 0, low_priority: bool = False, uid: str = "705ab4f5-6393-11e8-b7cc-42010a800002", course: str = None, + run_as_user: int = None, run_as_group: int = None, fs_group: int = None, supplemental_groups: List[int] = None, username: str = "user10", has_container: bool = True, + container_override: List[Container] = None, init_containers: List[Container] = None) -> Request: + + res_req = None + if gpu_req > 0: + if res_req is None: + res_req = ResourceRequirements() + + res_req.requests = {GPU_LABEL: gpu_req} + + if gpu_lim > 0: + if res_req is None: + res_req = ResourceRequirements() + + res_req.limits = {GPU_LABEL: gpu_lim} + + p_class = None + if low_priority: + p_class = "low" + + labels = {} + if course is not None: + labels["dsmlp/course"] = course + + sec_context = None + if run_as_user is not None or run_as_group is not None or fs_group is not None or supplemental_groups is not None: + sec_context = PodSecurityContext( + runAsUser=run_as_user, runAsGroup=run_as_group, fsGroup=fs_group, supplementalGroups=supplemental_groups) + + containers = [] + if has_container: + c = Container(resources=res_req) + + if run_as_user is not None or run_as_group is not None: + c.securityContext = SecurityContext(runAsUser=run_as_user, + runAsGroup=run_as_group) + + containers.append(c) + + if container_override is not None: + containers = container_override + + request = Request( + uid=uid, + namespace=username, + object=Object( + metadata=ObjectMeta(labels=labels), + spec=PodSpec( + containers=containers, + priorityClassName=p_class, + securityContext=sec_context, + initContainers=init_containers + ) + ), + userInfo=UserInfo(username=username) + ) + + return request + + +def try_val_with_component(validator: Validator, json, expected: bool, message: str = None): + try: + response = validator.validate_pod(json) + if not expected: + raise AssertionError(f"Expected exception but got {response}") + except Exception as e: + if expected: + raise AssertionError(f"Expected no exception but got {e}") + assert_that(e.message, equal_to(message)) diff --git a/tests/test_admission_controller.py b/tests/test_admission_controller.py deleted file mode 100644 index 86ce985..0000000 --- a/tests/test_admission_controller.py +++ /dev/null @@ -1,107 +0,0 @@ -import pytest -import inspect -import os -import shutil -import tempfile -import dsmlp -from dsmlp.app import factory -# from dsmlp.plugin.awsed import CourseJson -from tests.fakes import FakeAwsedClient - - -@pytest.mark.integration -class TestDirCreateMain: - def setup_method(self) -> None: - self.awsed_client = FakeAwsedClient() - factory.awsed_client = self.awsed_client - - # teams1 = {"teams": [ - # { - # "gid": 1000, - # "members": [ - # { - # "firstName": "string", - # "lastName": "string", - # "role": "string", - # "uid": 0, - # "username": "user1" - # } - # ], - # "teamName": "string" - # } - # ]} - - # self.awsed_client.add_teams_for_course_from_dict('course1', teams1) - - # teams2 = {"teams": [ - # { - # "gid": 2000, - # "members": [ - # { - # "firstName": "string", - # "lastName": "string", - # "role": "string", - # "uid": 0, - # "username": "user2" - # } - # ], - # "teamName": "string" - # } - # ]} - - # self.awsed_client.add_teams_for_course_from_dict('course2', teams2) - - # teams3 = {"teams": [ - # { - # "gid": 3000, - # "members": [ - # { - # "firstName": "string", - # "lastName": "string", - # "role": "string", - # "uid": 0, - # "username": "user1" - # } - # ], - # "teamName": "string" - # }, - # { - # "gid": 4000, - # "members": [ - # { - # "firstName": "string", - # "lastName": "string", - # "role": "string", - # "uid": 0, - # "username": "user2" - # } - # ], - # "teamName": "string" - # } - # ]} - # self.awsed_client.add_teams_for_course_from_dict('course3', teams3) - - def test_something(self, capsys): - pass - # self.awsed_client.add_course(CourseJson(courseId='course1', tags=['teams-enabled'])) - - # os.environ["COURSE_IDS"] = "course1" - # os.environ["TEAM_ROOT"] = tempfile.gettempdir() - # dsmlp.app.factory.course_provider = EnvVarConfigProvider('COURSE_IDS') - # self.clean_dir(tempfile.gettempdir() + "/course1") - # cdir = tempfile.gettempdir() + "/course1/string" - # main() - # captured = capsys.readouterr() - # assert captured.out == "Reading course1...\n" + f"{cdir}, uid=0, gid=1000\n" - # assert captured.out == inspect.cleandoc( - # f""" - # Reading course1... - # {cdir}, uid=0, gid=1000 - # """) + "\n" - - # noinspection PyMethodMayBeStatic - # def clean_dir(self, course_): - # try: - # shutil.rmtree(course_) - # except FileNotFoundError: - # pass