Exemple #1
0
    async def _wait_for_service_account_token(self, spawner: KubeSpawner,
                                              name: str,
                                              namespace: str) -> bool:
        """Waits for a service account to spawn an associated token.

        Returns
        -------
        done : `bool`
            `True` once the secret exists, `False` otherwise (so it can be
            called from ``exponential_backoff``)
        """
        api = shared_client("CoreV1Api")
        try:
            service_account = await asyncio.wait_for(
                api.read_namespaced_service_account(name, namespace),
                spawner.k8s_api_request_timeout,
            )
            if not service_account.secrets:
                return False
            secret_name = service_account.secrets[0].name
            secret = await asyncio.wait_for(
                api.read_namespaced_secret(secret_name, namespace),
                spawner.k8s_api_request_timeout,
            )
            return secret.metadata.name == secret_name
        except asyncio.TimeoutError:
            return False
        except ApiException as e:
            if e.status == 404:
                self.log.debug("Waiting for secret for service account {name}")
                return False
            raise
Exemple #2
0
def kube_client(request, kube_ns):
    """fixture for the Kubernetes client object.

    skips test that require kubernetes if kubernetes cannot be contacted
    """
    load_kube_config()
    client = shared_client("CoreV1Api")
    try:
        namespaces = client.list_namespace(_request_timeout=3)
    except Exception as e:
        pytest.skip("Kubernetes not found: %s" % e)

    if not any(ns.metadata.name == kube_ns for ns in namespaces.items):
        print("Creating namespace %s" % kube_ns)
        client.create_namespace(V1Namespace(metadata=dict(name=kube_ns)))
    else:
        print("Using existing namespace %s" % kube_ns)
    # delete the test namespace when we finish

    def cleanup_namespace():
        client.delete_namespace(kube_ns, body={}, grace_period_seconds=0)
        for i in range(3):
            try:
                ns = client.read_namespace(kube_ns)
            except ApiException as e:
                if e.status == 404:
                    return
                else:
                    raise
            else:
                print("waiting for %s to delete" % kube_ns)
                time.sleep(1)

    # request.addfinalizer(cleanup_namespace)
    return client
Exemple #3
0
    def get_custom_resources(self, namespace, plural):
        api_instance = shared_client('CustomObjectsApi')
        group = 'primehub.io'
        version = 'v1alpha1'

        api_response = api_instance.list_namespaced_custom_object(
            group, version, namespace, plural)
        return api_response['items']
Exemple #4
0
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
Exemple #5
0
    async def _wait_for_namespace_deletion(self, spawner: KubeSpawner) -> bool:
        """Waits for the user's namespace to be deleted.

        If the namespace exists but has not been marked for deletion, try to
        delete it.  If we're spawning a new lab while the namespace still
        exists, that means something has gone wrong with the user's lab and
        there's nothing salvagable.

        Returns
        -------
        done : `bool`
            `True` if the namespace has been deleted, `False` if it still
            exists
        """
        api = shared_client("CoreV1Api")
        try:
            ns_name = spawner.namespace
            # Note that this is not safe to run if you aren't using
            # user namespaces.  Check that:
            assert spawner.enable_user_namespaces
            # Let's do a further check for paranoia.  We will assume that
            # a user namespace will be of the form <hub-namespace>-<something>
            # This is true for Rubin user namespaces, but might not be
            # universally.
            #
            # If opening the ns_path file fails, it means we are not running
            # in a namespace with service accounts enabled, in which case
            # we definitely want to let the exception crash the process.
            ns_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
            with open(ns_path) as f:
                hub_ns = f.read().strip()
            assert ns_name.startswith(f"{hub_ns}-")

            namespace = await asyncio.wait_for(
                api.read_namespace(ns_name),
                spawner.k8s_api_request_timeout,
            )
            if namespace.status.phase != "Terminating":
                self.log.info(f"Deleting namespace {ns_name}")
                await asyncio.wait_for(
                    api.delete_namespace(ns_name),
                    spawner.k8s_api_request_timeout,
                )
            return False
        except asyncio.TimeoutError:
            return False
        except ApiException as e:
            if e.status == 404:
                return True
            raise
Exemple #6
0
 def _ensure_namespaced_service_account(self):
     """Create a service account with role and rolebinding to allow it
     to manipulate pods in the namespace."""
     namespace = self.get_user_namespace()
     account = self.service_account
     svcacct, role, rolebinding = self._create_namespaced_account_objects()
     if not svcacct:
         self.log.info("Service account not defined.")
         return
     try:
         self.api.create_namespaced_service_account(namespace=namespace,
                                                    body=svcacct)
     except ApiException as e:
         if e.status != 409:
             self.log.exception("Create service account '%s' " % account +
                                "in namespace '%s' " % namespace +
                                "failed: %s" % str(e))
             raise
         else:
             self.log.info("Service account '%s' " % account +
                           "in namespace '%s' already exists." % namespace)
     if not self.rbacapi:
         self.log.info("Creating RBAC API Client.")
         self.rbacapi = shared_client('RbacAuthorizationV1Api')
     try:
         self.rbacapi.create_namespaced_role(namespace, role)
     except ApiException as e:
         if e.status != 409:
             self.log.exception("Create role '%s' " % account +
                                "in namespace '%s' " % namespace +
                                "failed: %s" % str(e))
             raise
         else:
             self.log.info("Role '%s' " % account +
                           "already exists in namespace '%s'." % namespace)
     try:
         self.rbacapi.create_namespaced_role_binding(namespace, rolebinding)
     except ApiException as e:
         if e.status != 409:
             self.log.exception(
                 "Create rolebinding '%s'" % account +
                 "in namespace '%s' " % namespace + "failed: %s", str(e))
             raise
         else:
             self.log.info("Rolebinding '%s' " % account +
                           "already exists in '%s'." % namespace)
def kube_client(request, kube_ns):
    """fixture for the Kubernetes client object.

    skips test that require kubernetes if kubernetes cannot be contacted

    - Ensures kube_ns namespace exists
    - Hooks up kubernetes events and logs to pytest capture
    - Cleans up kubernetes namespace on exit
    """
    load_kube_config()
    client = shared_client("CoreV1Api")
    try:
        namespaces = client.list_namespace(_request_timeout=3)
    except Exception as e:
        pytest.skip("Kubernetes not found: %s" % e)

    if not any(ns.metadata.name == kube_ns for ns in namespaces.items):
        print("Creating namespace %s" % kube_ns)
        client.create_namespace(V1Namespace(metadata=dict(name=kube_ns)))
    else:
        print("Using existing namespace %s" % kube_ns)

    # begin streaming all logs and events in our test namespace
    t = Thread(target=watch_kubernetes, args=(client, kube_ns), daemon=True)
    t.start()

    # delete the test namespace when we finish
    def cleanup_namespace():
        client.delete_namespace(kube_ns, body={}, grace_period_seconds=0)
        for i in range(3):
            try:
                ns = client.read_namespace(kube_ns)
            except ApiException as e:
                if e.status == 404:
                    return
                else:
                    raise
            else:
                print("waiting for %s to delete" % kube_ns)
                time.sleep(1)

    # allow opting out of namespace cleanup, for post-mortem debugging
    if not os.environ.get("KUBESPAWNER_DEBUG_NAMESPACE"):
        request.addfinalizer(cleanup_namespace)
    return client
Exemple #8
0
def kube_client(request, kube_ns):
    """fixture for the Kubernetes client object.

    skips test that require kubernetes if kubernetes cannot be contacted
    """
    load_kube_config()
    client = shared_client('CoreV1Api')
    try:
        namespaces = client.list_namespace(_request_timeout=3)
    except Exception as e:
        pytest.skip("Kubernetes not found: %s" % e)
    if not any(ns.metadata.name == kube_ns for ns in namespaces.items):
        print("Creating namespace %s" % kube_ns)
        client.create_namespace(V1Namespace(metadata=dict(name=kube_ns)))
    else:
        print("Using existing namespace %s" % kube_ns)
    # delete the test namespace when we finish
    request.addfinalizer(lambda: client.delete_namespace(kube_ns, {}))
    return client
Exemple #9
0
async def test_multi_namespace_spawn():
    # We cannot use the fixtures, because they assume the standard
    #  namespace and client for that namespace.

    spawner = MultiNamespaceKubeSpawner(hub=Hub(),
                                        user=MockUser(),
                                        config=Config())

    # 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.

    # 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)

    # remove namespace
    client.delete_namespace(kube_ns, body={})
Exemple #10
0
    async def _create_kubernetes_resources(self, spawner: KubeSpawner,
                                           options: SelectedOptions) -> None:
        custom_api = shared_client("CustomObjectsApi")
        template_values = await self._build_template_values(spawner, options)

        # Generate the list of additional user resources from the template.
        t = Template(self.nublado_config.user_resources_template)
        templated_user_resources = t.render(template_values)
        self.log.debug("Generated user resources:")
        self.log.debug(templated_user_resources)
        resources = self.yaml.load(templated_user_resources)

        # Add in the standard labels and annotations common to every resource
        # and create the resources.
        service_account = None
        for resource in resources:
            if "metadata" not in resource:
                resource["metadata"] = {}
            resource["metadata"]["annotations"] = spawner.extra_annotations
            resource["metadata"]["labels"] = spawner.extra_labels

            # Custom resources cannot be created by create_from_dict:
            # https://github.com/kubernetes-client/python/issues/740
            #
            # Detect those from the apiVersion field and handle them
            # specially.
            api_version = resource["apiVersion"]
            if "." in api_version and ".k8s.io/" not in api_version:
                crd_parser = CRDParser.from_crd_body(resource)
                await asyncio.wait_for(
                    custom_api.create_namespaced_custom_object(
                        body=resource,
                        group=crd_parser.group,
                        version=crd_parser.version,
                        namespace=spawner.namespace,
                        plural=crd_parser.plural,
                    ),
                    spawner.k8s_api_request_timeout,
                )
            else:
                await asyncio.wait_for(
                    create_from_dict(spawner.api.api_client, resource),
                    spawner.k8s_api_request_timeout,
                )

            # If this was a service account, note its name.
            if resource["kind"] == "ServiceAccount":
                service_account = resource["metadata"]["name"]

        # Construct the lab environment ConfigMap.  This is constructed from
        # configuration settings and doesn't use a resource template like
        # other resources.  This has to be done last, becuase the namespace is
        # created from the user resources template.
        await self._create_lab_environment_configmap(spawner, template_values)

        # Wait for the service account to generate a token before proceeding.
        # Otherwise, we may try to create the pod before the service account
        # token exists and Kubernetes will object.
        if service_account:
            await exponential_backoff(
                partial(
                    self._wait_for_service_account_token,
                    spawner,
                    service_account,
                    spawner.namespace,
                ),
                f"Service account {service_account} has no token",
                timeout=spawner.k8s_api_request_retry_timeout,
            )