def test_get_pod_manifest_tolerates_mixed_input(): """ Test that the get_pod_manifest function can handle a either a dictionary or an object both representing V1Container objects and that the function returns a V1Pod object containing V1Container objects. """ c = Config() dict_model = { 'name': 'mock_name_1', 'image': 'mock_image_1', 'command': ['mock_command_1'] } object_model = V1Container( name="mock_name_2", image="mock_image_2", command=['mock_command_2'], security_context=V1SecurityContext( privileged=True, run_as_user=0, capabilities=V1Capabilities(add=['NET_ADMIN']) ) ) c.KubeSpawner.init_containers = [dict_model, object_model] spawner = KubeSpawner(config=c, _mock=True) # this test ensures the following line doesn't raise an error manifest = sync_wait(spawner.get_pod_manifest()) # and tests the return value's types assert isinstance(manifest, V1Pod) assert isinstance(manifest.spec.init_containers[0], V1Container) assert isinstance(manifest.spec.init_containers[1], V1Container)
async def test_spawn_progress(kube_ns, kube_client, config, hub_pod, hub): spawner = KubeSpawner( hub=hub, user=MockUser(name="progress"), config=config, ) # empty spawner isn't running status = await spawner.poll() assert isinstance(status, int) # start the spawner start_future = spawner.start() # check progress events messages = [] async for progress in spawner.progress(): assert 'progress' in progress assert isinstance(progress['progress'], int) assert 'message' in progress assert isinstance(progress['message'], str) messages.append(progress['message']) # ensure we can serialize whatever we return with open(os.devnull, "w") as devnull: json.dump(progress, devnull) assert 'Started container' in '\n'.join(messages) await start_future # stop the pod await spawner.stop()
async def test_user_options_api_with_memory_cpu(): spawner = KubeSpawner(_mock=True) spawner.profile_list = _test_profiles cpu_limit = 1 mem_limit = 4 * 1024 * 1024 * 1024 # set user_options directly (e.g. via api) spawner.user_options = { 'profile': _test_profiles[1]['display_name'], 'cpu_limit': cpu_limit, 'mem_limit': mem_limit } # nothing should be loaded yet assert spawner.cpu_limit is None assert spawner.mem_limit is None await spawner.load_user_options() for key, value in _test_profiles[1]['kubespawner_override'].items(): if key == 'cpu_limit' or key == 'mem_limit': continue assert getattr(spawner, key) == value assert spawner.cpu_limit == float(cpu_limit) assert spawner.mem_limit == mem_limit
async def test_pod_connect_ip(kube_ns, kube_client, config, hub_pod, hub): config.KubeSpawner.pod_connect_ip = ( "jupyter-{username}--{servername}.foo.example.com") user = MockUser(name="connectip") # w/o servername spawner = KubeSpawner(hub=hub, user=user, config=config) # start the spawner res = await spawner.start() # verify the pod IP and port assert res == "http://jupyter-connectip.foo.example.com:8888" await spawner.stop() # w/ servername spawner = KubeSpawner( hub=hub, user=user, config=config, orm_spawner=MockOrmSpawner(), ) # start the spawner res = await spawner.start() # verify the pod IP and port assert res == "http://jupyter-connectip--server.foo.example.com:8888" await spawner.stop()
async def test_default_profile(): spawner = KubeSpawner(_mock=True) spawner.profile_list = _test_profiles spawner.user_options = {} # nothing should be loaded yet assert spawner.cpu_limit is None await spawner.load_user_options() for key, value in _test_profiles[0]['kubespawner_override'].items(): assert getattr(spawner, key) == value
def test_deprecated_runtime_access(): """Runtime access/modification of deprecated traits works""" spawner = KubeSpawner(_mock=True) spawner.singleuser_uid = 10 assert spawner.uid == 10 assert spawner.singleuser_uid == 10 spawner.uid = 20 assert spawner.uid == 20 assert spawner.singleuser_uid == 20
async def test_user_options_api(): spawner = KubeSpawner(_mock=True) spawner.profile_list = _test_profiles # set user_options directly (e.g. via api) spawner.user_options = {'profile': _test_profiles[1]['display_name']} # nothing should be loaded yet assert spawner.cpu_limit is None await spawner.load_user_options() for key, value in _test_profiles[1]['kubespawner_override'].items(): assert getattr(spawner, key) == value
async def test_user_options_set_from_form(): spawner = KubeSpawner(_mock=True) spawner.profile_list = _test_profiles # render the form await spawner.get_options_form() spawner.user_options = spawner.options_from_form({'profile': [1]}) assert spawner.user_options == { 'profile': _test_profiles[1]['display_name'], } # nothing should be loaded yet assert spawner.cpu_limit is None await spawner.load_user_options() for key, value in _test_profiles[1]['kubespawner_override'].items(): assert getattr(spawner, key) == value
async def test_delete_pvc(kube_ns, kube_client, hub, config): config.KubeSpawner.storage_pvc_ensure = True config.KubeSpawner.storage_capacity = '1M' spawner = KubeSpawner( hub=hub, user=MockUser(name="mockuser"), config=config, _mock=True, ) spawner.api = kube_client # start the spawner await spawner.start() # verify the pod exists pod_name = "jupyter-%s" % spawner.user.name pods = kube_client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert pod_name in pod_names # verify PVC is created pvc_name = spawner.pvc_name pvc_list = kube_client.list_namespaced_persistent_volume_claim( kube_ns).items pvc_names = [s.metadata.name for s in pvc_list] assert pvc_name in pvc_names # stop the pod await spawner.stop() # verify pod is gone pods = kube_client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert "jupyter-%s" % spawner.user.name not in pod_names # delete the PVC await spawner.delete_forever() # verify PVC is deleted, it may take a little while for i in range(5): pvc_list = kube_client.list_namespaced_persistent_volume_claim( kube_ns).items pvc_names = [s.metadata.name for s in pvc_list] if pvc_name in pvc_names: time.sleep(1) else: break assert pvc_name not in pvc_names
def test_spawner_can_use_list_of_image_pull_secrets(): secrets = ["ecr", "regcred", "artifactory"] c = Config() c.KubeSpawner.image_spec = "private.docker.registry/jupyter:1.2.3" c.KubeSpawner.image_pull_secrets = secrets spawner = KubeSpawner(hub=Hub(), config=c, _mock=True) assert spawner.image_pull_secrets == secrets secrets = [dict(name=secret) for secret in secrets] c = Config() c.KubeSpawner.image_spec = "private.docker.registry/jupyter:1.2.3" c.KubeSpawner.image_pull_secrets = secrets spawner = KubeSpawner(hub=Hub(), config=c, _mock=True) assert spawner.image_pull_secrets == secrets
async def test_spawn(kube_ns, kube_client, config): spawner = KubeSpawner(hub=Hub(), user=MockUser(), config=config) # empty spawner isn't running status = await spawner.poll() assert isinstance(status, int) # start the spawner await spawner.start() # verify the pod exists pods = kube_client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert "jupyter-%s" % spawner.user.name in pod_names # verify poll while running status = await spawner.poll() assert status is None # stop the pod await spawner.stop() # verify pod is gone pods = kube_client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert "jupyter-%s" % spawner.user.name not in pod_names # verify exit status status = await spawner.poll() assert isinstance(status, int)
def test_pod_name_custom_template(): user = MockUser() user.name = "some_user" pod_name_template = "prefix-{username}-suffix" spawner = KubeSpawner(user=user, pod_name_template=pod_name_template, _mock=True) assert spawner.pod_name == "prefix-some-5fuser-suffix"
def test_pod_name_collision(): user1 = MockUser() user1.name = "user-has-dash" orm_spawner1 = Spawner() orm_spawner1.name = "" user2 = MockUser() user2.name = "user-has" orm_spawner2 = Spawner() orm_spawner2.name = "2ddash" spawner = KubeSpawner(user=user1, orm_spawner=orm_spawner1, _mock=True) assert spawner.pod_name == "jupyter-user-2dhas-2ddash" assert spawner.pvc_name == "claim-user-2dhas-2ddash" named_spawner = KubeSpawner(user=user2, orm_spawner=orm_spawner2, _mock=True) assert named_spawner.pod_name == "jupyter-user-2dhas--2ddash" assert spawner.pod_name != named_spawner.pod_name assert named_spawner.pvc_name == "claim-user-2dhas--2ddash" assert spawner.pvc_name != named_spawner.pvc_name
def test_get_pvc_manifest(): c = Config() c.KubeSpawner.pvc_name_template = "user-{username}" c.KubeSpawner.storage_extra_labels = {"user": "******"} c.KubeSpawner.storage_selector = {"matchLabels": {"user": "******"}} spawner = KubeSpawner(config=c, _mock=True) manifest = spawner.get_pvc_manifest() assert isinstance(manifest, V1PersistentVolumeClaim) assert manifest.metadata.name == "user-mock-5fname" assert manifest.metadata.labels == { "user": "******", "app": "jupyterhub", "component": "singleuser-storage", "heritage": "jupyterhub", } assert manifest.spec.selector == {"matchLabels": {"user": "******"}}
async def test_spawn_progress(kube_ns, kube_client, config): spawner = KubeSpawner(hub=Hub(), user=MockUser(name="progress"), config=config) # empty spawner isn't running status = await spawner.poll() assert isinstance(status, int) # start the spawner start_future = spawner.start() # check progress events messages = [] async for event in spawner.progress(): assert 'progress' in event assert isinstance(event['progress'], int) assert 'message' in event assert isinstance(event['message'], str) messages.append(event['message']) assert 'Started container' in '\n'.join(messages) await start_future # stop the pod await spawner.stop()
def test_pod_name_custom_template(): c = Config() c.JupyterHub.allow_named_servers = False user = Config() user.name = "some_user" pod_name_template = "prefix-{username}-suffix" spawner = KubeSpawner(config=c, user=user, pod_name_template=pod_name_template, _mock=True) assert spawner.pod_name == "prefix-some-5fuser-suffix"
def test_pod_name_no_named_servers(): c = Config() c.JupyterHub.allow_named_servers = False user = Config() user.name = "user" orm_spawner = Spawner() spawner = KubeSpawner(config=c, user=user, orm_spawner=orm_spawner, _mock=True) assert spawner.pod_name == "jupyter-user"
def test_pod_name_escaping(): c = Config() c.JupyterHub.allow_named_servers = True user = Config() user.name = "some_user" orm_spawner = Spawner() orm_spawner.name = "test-server!" spawner = KubeSpawner(config=c, user=user, orm_spawner=orm_spawner, _mock=True) assert spawner.pod_name == "jupyter-some-5fuser-test-2dserver-21"
async def test_spawn_start( kube_ns, kube_client, config, hub, exec_python, ): spawner = KubeSpawner( hub=hub, user=MockUser(name="start"), config=config, api_token="abc123", oauth_client_id="unused", ) # empty spawner isn't running status = await spawner.poll() assert isinstance(status, int) pod_name = spawner.pod_name # start the spawner url = await spawner.start() # verify the pod exists pods = kube_client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert pod_name in pod_names # pod should be running when start returns pod = kube_client.read_namespaced_pod(namespace=kube_ns, name=pod_name) assert pod.status.phase == "Running" # verify poll while running status = await spawner.poll() assert status is None # make sure spawn url is correct r = exec_python(check_up, {"url": url}, _retries=3) assert r == "302" # stop the pod await spawner.stop() # verify pod is gone pods = kube_client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert pod_name not in pod_names # verify exit status status = await spawner.poll() assert isinstance(status, int)
async def test_multi_namespace_spawn(): # We cannot use the fixtures, because they assume the standard # namespace and client for that namespace. spawner = KubeSpawner( hub=Hub(), user=MockUser(), config=Config(), enable_user_namespaces=True, ) # empty spawner isn't running status = await spawner.poll() assert isinstance(status, int) # get a client kube_ns = spawner.namespace load_kube_config() client = shared_client('CoreV1Api') # the spawner will create the namespace on its own. # Wrap in a try block so we clean up the namespace. saved_exception = None try: # start the spawner await spawner.start() # verify the pod exists pods = client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert "jupyter-%s" % spawner.user.name in pod_names # verify poll while running status = await spawner.poll() assert status is None # stop the pod await spawner.stop() # verify pod is gone pods = client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert "jupyter-%s" % spawner.user.name not in pod_names # verify exit status status = await spawner.poll() assert isinstance(status, int) except Exception as saved_exception: pass # We will raise after namespace removal # remove namespace client.delete_namespace(kube_ns, body={}) if saved_exception is not None: raise saved_exception
async def test_pod_connect_ip(kube_ns, kube_client, config): config.KubeSpawner.pod_connect_ip = "jupyter-{username}--{servername}.foo.example.com" # w/o servername spawner = KubeSpawner(hub=Hub(), user=MockUser(), config=config) # start the spawner res = await spawner.start() # verify the pod IP and port assert res == ("jupyter-fake.foo.example.com", 8888) await spawner.stop() # w/ servername spawner = KubeSpawner(hub=Hub(), user=MockUser(), config=config, orm_spawner=MockOrmSpawner()) # start the spawner res = await spawner.start() # verify the pod IP and port assert res == ("jupyter-fake--server.foo.example.com", 8888)
async def test_url_changed(kube_ns, kube_client, config, hub_pod, hub): user = MockUser(name="url") config.KubeSpawner.pod_connect_ip = ( "jupyter-{username}--{servername}.foo.example.com") spawner = KubeSpawner(hub=hub, user=user, config=config) spawner.db = Mock() # start the spawner res = await spawner.start() pod_host = "http://jupyter-url.foo.example.com:8888" assert res == pod_host # Mock an incorrect value in the db # Can occur e.g. by interrupting a launch with a hub restart # or possibly weird network things in kubernetes spawner.server = Server.from_url(res + "/users/url/") spawner.server.ip = "1.2.3.4" spawner.server.port = 0 assert spawner.server.host == "http://1.2.3.4:0" assert spawner.server.base_url == "/users/url/" # poll checks the url, and should restore the correct value await spawner.poll() # verify change noticed and persisted to db assert spawner.server.host == pod_host assert spawner.db.commit.call_count == 1 # base_url should be left alone assert spawner.server.base_url == "/users/url/" previous_commit_count = spawner.db.commit.call_count # run it again, to make sure we aren't incorrectly detecting and committing # changes on every poll await spawner.poll() assert spawner.db.commit.call_count == previous_commit_count await spawner.stop()
def test_deprecated_config(): """Deprecated config is handled correctly""" cfg = Config() ks_cfg = cfg.KubeSpawner # both set, non-deprecated wins ks_cfg.singleuser_fs_gid = 5 ks_cfg.fs_gid = 10 # only deprecated set, should still work ks_cfg.singleuser_extra_pod_config = extra_pod_config = {"key": "value"} spawner = KubeSpawner(config=cfg, _mock=True) assert spawner.fs_gid == 10 assert spawner.extra_pod_config == extra_pod_config # deprecated access gets the right values, too assert spawner.singleuser_fs_gid == spawner.fs_gid assert spawner.singleuser_extra_pod_config == spawner.singleuser_extra_pod_config
def test_deprecated_config(): """Deprecated config is handled correctly""" c = Config() # both set, non-deprecated wins c.KubeSpawner.singleuser_fs_gid = 5 c.KubeSpawner.fs_gid = 10 # only deprecated set, should still work c.KubeSpawner.singleuser_extra_pod_config = extra_pod_config = {"key": "value"} c.KubeSpawner.image_spec = 'abc:123' spawner = KubeSpawner(config=c, _mock=True) assert spawner.fs_gid == 10 assert spawner.extra_pod_config == extra_pod_config # deprecated access gets the right values, too assert spawner.singleuser_fs_gid == spawner.fs_gid assert spawner.singleuser_extra_pod_config == spawner.extra_pod_config assert spawner.image == 'abc:123'
def test_deprecated_runtime_access(): """Runtime access/modification of deprecated traits works""" spawner = KubeSpawner(_mock=True) spawner.singleuser_uid = 10 assert spawner.uid == 10 assert spawner.singleuser_uid == 10 spawner.uid = 20 assert spawner.uid == 20 assert spawner.singleuser_uid == 20 spawner.image_spec = 'abc:latest' assert spawner.image_spec == 'abc:latest' assert spawner.image == 'abc:latest' spawner.image = 'abc:123' assert spawner.image_spec == 'abc:123' assert spawner.image == 'abc:123' spawner.image_pull_secrets = 'k8s-secret-a' assert spawner.image_pull_secrets[0]["name"] == 'k8s-secret-a'
def test_deprecated_config(): """Deprecated config is handled correctly""" with pytest.warns(DeprecationWarning): c = Config() # both set, non-deprecated wins c.KubeSpawner.singleuser_fs_gid = 5 c.KubeSpawner.fs_gid = 10 # only deprecated set, should still work c.KubeSpawner.hub_connect_ip = '10.0.1.1' c.KubeSpawner.singleuser_extra_pod_config = extra_pod_config = {"key": "value"} c.KubeSpawner.image_spec = 'abc:123' spawner = KubeSpawner(hub=Hub(), config=c, _mock=True) assert spawner.hub.connect_ip == '10.0.1.1' assert spawner.fs_gid == 10 assert spawner.extra_pod_config == extra_pod_config # deprecated access gets the right values, too assert spawner.singleuser_fs_gid == spawner.fs_gid assert spawner.singleuser_extra_pod_config == spawner.extra_pod_config assert spawner.image == 'abc:123'
async def test_spawn_internal_ssl( kube_ns, kube_client, ssl_app, hub_pod_ssl, hub_ssl, config, exec_python_pod, ): hub_pod_name = hub_pod_ssl.metadata.name spawner = KubeSpawner( config=config, hub=hub_ssl, user=MockUser(name="ssl"), api_token="abc123", oauth_client_id="unused", internal_ssl=True, internal_trust_bundles=ssl_app.internal_trust_bundles, internal_certs_location=ssl_app.internal_certs_location, ) # initialize ssl config hub_paths = await spawner.create_certs() spawner.cert_paths = await spawner.move_certs(hub_paths) # start the spawner url = await spawner.start() pod_name = "jupyter-%s" % spawner.user.name # verify the pod exists pods = kube_client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert pod_name in pod_names # verify poll while running status = await spawner.poll() assert status is None # verify service and secret exist secret_name = spawner.secret_name secrets = kube_client.list_namespaced_secret(kube_ns).items secret_names = [s.metadata.name for s in secrets] assert secret_name in secret_names service_name = pod_name services = kube_client.list_namespaced_service(kube_ns).items service_names = [s.metadata.name for s in services] assert service_name in service_names # resolve internal-ssl paths in hub-ssl pod # these are in /etc/jupyterhub/internal-ssl hub_ssl_dir = "/etc/jupyterhub" hub_ssl_ca = os.path.join(hub_ssl_dir, ssl_app.internal_trust_bundles["hub-ca"]) # use certipy to resolve these? hub_internal = os.path.join(hub_ssl_dir, "internal-ssl", "hub-internal") hub_internal_cert = os.path.join(hub_internal, "hub-internal.crt") hub_internal_key = os.path.join(hub_internal, "hub-internal.key") r = exec_python_pod( hub_pod_name, check_up, { "url": url, "ssl_ca": hub_ssl_ca, "ssl_client_cert": hub_internal_cert, "ssl_client_key": hub_internal_key, }, _retries=3, ) assert r == "302" # stop the pod await spawner.stop() # verify pod is gone pods = kube_client.list_namespaced_pod(kube_ns).items pod_names = [p.metadata.name for p in pods] assert "jupyter-%s" % spawner.user.name not in pod_names # verify service and secret are gone # it may take a little while for them to get cleaned up for i in range(5): secrets = kube_client.list_namespaced_secret(kube_ns).items secret_names = {s.metadata.name for s in secrets} services = kube_client.list_namespaced_service(kube_ns).items service_names = {s.metadata.name for s in services} if secret_name in secret_names or service_name in service_names: time.sleep(1) else: break assert secret_name not in secret_names assert service_name not in service_names
def test_spawner_values(): """Spawner values are set correctly""" spawner = KubeSpawner(_mock=True) def set_id(spawner): return 1 spawner.uid = 10 assert spawner.uid == 10 spawner.uid = set_id assert spawner.uid == set_id spawner.uid = None assert spawner.uid == None spawner.gid = 20 assert spawner.gid == 20 spawner.gid = set_id assert spawner.gid == set_id spawner.gid = None assert spawner.gid == None spawner.fs_gid = 30 assert spawner.fs_gid == 30 spawner.fs_gid = set_id assert spawner.fs_gid == set_id spawner.fs_gid = None assert spawner.fs_gid == None
async def test_variable_expansion(ssl_app): """ Variable expansion not tested here: - pod_connect_ip: is tested in test_url_changed - user_namespace_template: isn't tested because it isn't set as part of the Pod manifest but. """ config_to_test = { "pod_name_template": { "configured_value": "pod-name-template-{username}", "findable_in": ["pod"], }, "pvc_name_template": { "configured_value": "pvc-name-template-{username}", "findable_in": ["pvc"], }, "secret_name_template": { "configured_value": "secret-name-template-{username}", "findable_in": ["secret"], }, "storage_selector": { "configured_value": { "matchLabels": { "dummy": "storage-selector-{username}" } }, "findable_in": ["pvc"], }, "storage_extra_labels": { "configured_value": { "dummy": "storage-extra-labels-{username}" }, "findable_in": ["pvc"], }, "extra_labels": { "configured_value": { "dummy": "common-extra-labels-{username}" }, "findable_value": "common-extra-labels-user1", "findable_in": ["pod", "service", "secret"], }, "extra_annotations": { "configured_value": { "dummy": "common-extra-annotations-{username}" }, "findable_value": "common-extra-annotations-user1", "findable_in": ["pod", "service", "secret"], }, "working_dir": { "configured_value": "working-dir-{username}", "findable_in": ["pod"], }, "service_account": { "configured_value": "service-account-{username}", "findable_in": ["pod"], }, "volumes": { "configured_value": [{ 'name': 'volumes-{username}', 'secret': { 'secretName': 'dummy' }, }], "findable_in": ["pod"], }, "volume_mounts": { "configured_value": [{ 'name': 'volume-mounts-{username}', 'mountPath': '/tmp/', }], "findable_in": ["pod"], }, "init_containers": { "configured_value": [{ 'name': 'init-containers-{username}', 'image': 'busybox', }], "findable_in": ["pod"], }, "extra_containers": { "configured_value": [{ 'name': 'extra-containers-{username}', 'image': 'busybox', }], "findable_in": ["pod"], }, "extra_pod_config": { "configured_value": { "schedulerName": "extra-pod-config-{username}" }, "findable_in": ["pod"], }, } c = Config() for key, value in config_to_test.items(): c.KubeSpawner[key] = value["configured_value"] if "findable_value" not in value: value["findable_value"] = key.replace("_", "-") + "-user1" user = MockUser(name="user1") spawner = KubeSpawner( config=c, user=user, internal_trust_bundles=ssl_app.internal_trust_bundles, internal_certs_location=ssl_app.internal_certs_location, _mock=True, ) hub_paths = await spawner.create_certs() spawner.cert_paths = await spawner.move_certs(hub_paths) manifests = { "pod": await spawner.get_pod_manifest(), "pvc": spawner.get_pvc_manifest(), "secret": spawner.get_secret_manifest("dummy-owner-ref"), "service": spawner.get_service_manifest("dummy-owner-ref"), } for resource_kind, manifest in manifests.items(): manifest_string = str(manifest) for config in config_to_test.values(): if resource_kind in config["findable_in"]: assert config["findable_value"] in manifest_string, ( manifest_string + "\n\n" + "finable_value: " + config["findable_value"] + "\n" + "resource_kind: " + resource_kind)
def test_enable_user_namespaces(): user = MockUser() spawner = KubeSpawner(user=user, _mock=True, enable_user_namespaces=True) assert spawner.namespace.endswith("-{}".format(user.escaped_name))