Пример #1
0
def test_pod_apis(obj_name):
    client = Client()

    # list kube-system namespace
    pods = [pod.metadata.name for pod in client.list(Pod, namespace='kube-system')]
    assert len(pods) > 0
    assert any(name.startswith('metrics-server') for name in pods)

    # create a pod
    pod = client.create(create_pod(obj_name, "while true;do echo 'this is a test';sleep 5; done"))
    try:
        assert pod.metadata.name == obj_name
        assert pod.metadata.namespace == client.namespace
        assert pod.status.phase


        wait_pod(client, pod)

        # read pod logs
        for l in client.log(obj_name, follow=True):
            assert l == 'this is a test\n'
            break
    finally:
        # delete the pod
        client.delete(Pod, obj_name)
Пример #2
0
def test_namespaced_methods(obj_name):
    client = Client()
    config = ConfigMap(
        metadata=ObjectMeta(name=obj_name, namespace='default'),
        data={'key1': 'value1', 'key2': 'value2'}
    )

    # create
    config = client.create(config)
    try:
        assert config.metadata.name == obj_name
        assert config.data['key1'] == 'value1'
        assert config.data['key2'] == 'value2'

        # replace
        config.data['key1'] = 'new value'
        config = client.replace(config)
        assert config.data['key1'] == 'new value'
        assert config.data['key2'] == 'value2'

        # patch with PatchType.STRATEGIC
        patch = {'metadata': {'labels': {'app': 'xyz'}}}
        config = client.patch(ConfigMap, name=obj_name, obj=patch)
        assert config.metadata.labels['app'] == 'xyz'

        # get
        config2 = client.get(ConfigMap, name=obj_name)
        assert config.metadata.creationTimestamp == config2.metadata.creationTimestamp

        # list
        configs = [config.metadata.name for config in client.list(ConfigMap)]
        assert obj_name in configs

    finally:
        client.delete(ConfigMap, name=obj_name)
Пример #3
0
def test_deletecollection(obj_name):
    client = Client()

    config = ConfigMap(
        metadata=ObjectMeta(name=obj_name, namespace=obj_name),
        data={'key1': 'value1', 'key2': 'value2'}
    )

    client.create(Namespace(metadata=ObjectMeta(name=obj_name)))

    try:
        # create
        client.create(config)
        config.metadata.name = f"{obj_name}-2"
        client.create(config)

        # k3s automatically create/recreate one extra configmap.
        maps = names(client.list(ConfigMap, namespace=obj_name))
        assert obj_name in maps
        assert f"{obj_name}-2" in maps

        client.deletecollection(ConfigMap, namespace=obj_name)
        maps = names(client.list(ConfigMap, namespace=obj_name))
        assert obj_name not in maps
        assert f"{obj_name}-2" not in maps

    finally:
        client.delete(Namespace, name=obj_name)
async def kubernetes(ops_test):
    kubeconfig_path = ops_test.tmp_path / "kubeconfig"
    retcode, stdout, stderr = await ops_test.run(
        "juju",
        "scp",
        "-m",
        ops_test.model_full_name,
        "kubernetes-control-plane/leader:config",
        kubeconfig_path,
    )
    if retcode != 0:
        log.error(f"retcode: {retcode}")
        log.error(f"stdout:\n{stdout.strip()}")
        log.error(f"stderr:\n{stderr.strip()}")
        pytest.fail("Failed to copy kubeconfig from kubernetes-control-plane")

    namespace = (
        "test-kubernetes-control-plane-integration-"
        + random.choice(string.ascii_lowercase + string.digits) * 5
    )
    config = KubeConfig.from_file(kubeconfig_path)
    kubernetes = Client(
        config=config.get(context_name="juju-context"),
        namespace=namespace,
        trust_env=False,
    )
    namespace_obj = Namespace(metadata=ObjectMeta(name=namespace))
    kubernetes.create(namespace_obj)
    yield kubernetes
    kubernetes.delete(Namespace, namespace)
Пример #5
0
def test_pod_already_exist(obj_name):
    client = Client()
    client.create(create_pod(obj_name, "sleep 5"))
    try:
        with pytest.raises(ApiError) as exc_info:
            client.create(create_pod(obj_name, "sleep 5"))
        status = exc_info.value.status
        assert status.code == 409
        assert status.reason == 'AlreadyExists'
        assert status.status == 'Failure'
    finally:
        # delete the pod
        client.delete(Pod, obj_name)
Пример #6
0
def test_wait_namespaced(resource, for_condition, spec):
    client = Client()

    requested = resource.from_dict(
        {"metadata": {"generateName": "e2e-test-"}, "spec": spec}
    )
    created = client.create(requested)
    client.wait(
        resource,
        created.metadata.name,
        for_conditions=[for_condition],
    )
    client.delete(resource, created.metadata.name)
Пример #7
0
def test_patching(obj_name):
    client = Client()
    service = Service(
        metadata=ObjectMeta(name=obj_name),
        spec=ServiceSpec(
            ports=[ServicePort(name='a', port=80, targetPort=8080)],
            selector={'app': 'not-existing'}
        )
    )

    # create
    client.create(service)
    try:
        # patch with PatchType.STRATEGIC
        patch = {'spec': {'ports': [{'name': 'b', 'port':81, 'targetPort': 8081}]}}
        service = client.patch(Service, name=obj_name, obj=patch)
        assert len(service.spec.ports) == 2
        assert {port.name for port in service.spec.ports} == {'a', 'b'}

        # strategic - patch merge key: port
        # we also try to send a Resource type for patching
        patch = Service(spec=ServiceSpec(ports=[ServicePort(name='b', port=81, targetPort=8082)]))
        service = client.patch(Service, name=obj_name, obj=patch)
        assert len(service.spec.ports) == 2

        for port in service.spec.ports:
            if port.port == 81:
                assert port.targetPort == 8082

        # patch with PatchType.MERGE
        # merge will replace the full list
        patch = {'spec': {'ports': [{'name': 'b', 'port': 81, 'targetPort': 8081}]}}
        service = client.patch(Service, name=obj_name, obj=patch, patch_type=PatchType.MERGE)
        assert len(service.spec.ports) == 1
        assert service.spec.ports[0].port == 81

        # patch with PatchType.JSON
        patch = [
            {'op': 'add', 'path': '/spec/ports/-', 'value': {'name': 'a', 'port': 80, 'targetPort': 8080}}
        ]
        service = client.patch(Service, name=obj_name, obj=patch, patch_type=PatchType.JSON)
        assert len(service.spec.ports) == 2
        assert service.spec.ports[1].port == 80

    finally:
        client.delete(Service, name=obj_name)
Пример #8
0
def delete_all_from_yaml(yaml_file: str,
                         lightkube_client: lightkube.Client = None):
    """Deletes all k8s resources listed in a YAML file via lightkube.

    Args:
        yaml_file (str or Path): Either a string filename or a string of valid YAML.  Will attempt
                                 to open a filename at this path, failing back to interpreting the
                                 string directly as YAML.
        lightkube_client: Instantiated lightkube client or None
    """
    yaml_text = _safe_load_file_to_text(yaml_file)

    if lightkube_client is None:
        lightkube_client = lightkube.Client()

    for obj in codecs.load_all_yaml(yaml_text):
        lightkube_client.delete(type(obj), obj.metadata.name)
Пример #9
0
def test_apply(obj_name):
    client = Client(field_manager='lightkube')
    config = ConfigMap(
        apiVersion='v1',  # apiVersion and kind are required for server-side apply
        kind='ConfigMap',
        metadata=ObjectMeta(name=obj_name, namespace='default'),
        data={'key1': 'value1', 'key2': 'value2'}
    )

    # create with apply
    c = client.apply(config)
    try:
        assert c.metadata.name == obj_name
        assert c.data['key1'] == 'value1'
        assert c.data['key2'] == 'value2'

        # modify
        config.data['key2'] = 'new value'
        del config.data['key1']
        config.data['key3'] = 'value3'
        c = client.apply(config)
        assert c.data['key2'] == 'new value'
        assert c.data['key3'] == 'value3'
        assert 'key1' not in c.data

        # remove all keys
        config.data.clear()
        c = client.apply(config)
        assert not c.data

        # use the patch equivalent
        config.data['key1'] = 'new value'
        c = client.patch(ConfigMap, obj_name, config.to_dict(), patch_type=PatchType.APPLY)
        assert c.data['key1'] == 'new value'

    finally:
        client.delete(ConfigMap, name=obj_name)
Пример #10
0
def test_list_all_ns(obj_name):
    client = Client()
    ns1 = obj_name
    ns2 = f"{obj_name}-2"

    config = ConfigMap(
        metadata=ObjectMeta(name=obj_name),
        data={'key1': 'value1', 'key2': 'value2'}
    )

    client.create(Namespace(metadata=ObjectMeta(name=ns1)))
    client.create(Namespace(metadata=ObjectMeta(name=ns2)))

    try:
        client.create(config, namespace=ns1)
        client.create(config, namespace=ns2)

        maps = [f"{cm.metadata.namespace}/{cm.metadata.name}" for cm in client.list(ConfigMap, namespace='*')]
        assert f"{ns1}/{obj_name}" in maps
        assert f"{ns2}/{obj_name}" in maps

    finally:
        client.delete(Namespace, name=ns1)
        client.delete(Namespace, name=ns2)
Пример #11
0
    def remove_manifest(manifest):
        client = Client()

        for resource in manifest:
            client.delete(type(resource), resource.metadata.name)
Пример #12
0
def test_delete_global(client: lightkube.Client):
    respx.delete("https://localhost:9443/api/v1/nodes/xx")
    client.delete(Node, name="xx")
Пример #13
0
def test_delete_namespaced(client: lightkube.Client):
    respx.delete("https://localhost:9443/api/v1/namespaces/default/pods/xx")
    client.delete(Pod, name="xx")

    respx.delete("https://localhost:9443/api/v1/namespaces/other/pods/xx")
    client.delete(Pod, name="xx", namespace="other")
Пример #14
0
class ResourceHandler:
    def __init__(self, app_name, model_name):
        """A Lightkube API interface.

        Args:
           - app_name: name of the application
           - model_name: name of the Juju model this charm is deployed to
        """

        self.app_name = app_name
        self.model_name = model_name

        self.log = logging.getLogger(__name__)

        # Every lightkube API call will use the model name as the namespace by default
        self.lightkube_client = Client(namespace=self.model_name,
                                       field_manager="lightkube")

        self.env = Environment(loader=FileSystemLoader('src'))

    def delete_resource(self,
                        obj,
                        namespace=None,
                        ignore_not_found=False,
                        ignore_unauthorized=False):
        try:
            self.lightkube_client.delete(type(obj),
                                         obj.metadata.name,
                                         namespace=namespace)
        except ApiError as err:
            self.log.exception(
                "ApiError encountered while attempting to delete resource.")
            if err.status.message is not None:
                if "not found" in err.status.message and ignore_not_found:
                    self.log.error(
                        f"Ignoring not found error:\n{err.status.message}")
                elif "(Unauthorized)" in err.status.message and ignore_unauthorized:
                    # Ignore error from https://bugs.launchpad.net/juju/+bug/1941655
                    self.log.error(
                        f"Ignoring unauthorized error:\n{err.status.message}")
                else:
                    self.log.error(err.status.message)
                    raise
            else:
                raise

    def delete_existing_resources(
        self,
        resource,
        namespace=None,
        ignore_not_found=False,
        ignore_unauthorized=False,
        labels=None,
    ):
        if labels is None:
            labels = {}
        for obj in self.lightkube_client.list(
                resource,
                labels={"app.juju.is/created-by":
                        f"{self.app_name}"}.update(labels),
                namespace=namespace,
        ):
            self.delete_resource(
                obj,
                namespace=namespace,
                ignore_not_found=ignore_not_found,
                ignore_unauthorized=ignore_unauthorized,
            )

    def apply_manifest(self, manifest, namespace=None):
        for obj in codecs.load_all_yaml(manifest):
            self.lightkube_client.apply(obj, namespace=namespace)

    def delete_manifest(self,
                        manifest,
                        namespace=None,
                        ignore_not_found=False,
                        ignore_unauthorized=False):
        for obj in codecs.load_all_yaml(manifest):
            self.delete_resource(
                obj,
                namespace=namespace,
                ignore_not_found=ignore_not_found,
                ignore_unauthorized=ignore_unauthorized,
            )

    def get_custom_resource_class_from_filename(self, filename: str):
        """Returns a class representing a namespaced K8s resource.

        Args:
            - filename: name of the manifest file defining the resource
        """

        # TODO: this is a generic context that is used for rendering
        # the manifest files. We should improve how we do this
        # and make it more generic.
        context = {
            'namespace': 'namespace',
            'app_name': 'name',
            'name': 'generic_resource',
            'request_headers': 'request_headers',
            'response_headers': 'response_headers',
            'port': 'port',
            'service': 'service',
        }
        manifest = self.env.get_template(filename).render(context)
        manifest_dict = yaml.safe_load(manifest)
        ns_resource = codecs.from_dict(manifest_dict,
                                       client=self.lightkube_client)
        return type(ns_resource)

    def reconcile_desired_resources(
        self,
        resource,
        desired_resources: Union[str, TextIO, None],
        namespace: str = None,
    ) -> None:
        """Reconciles the desired list of resources of any kind.

        Args:
            resource: resource kind (e.g. Service, Pod)
            desired_resources: all desired resources in manifest form as str
            namespace: namespace of the resource
        """
        existing_resources = self.lightkube_client.list(
            resource,
            labels={
                "app.juju.is/created-by": f"{self.app_name}",
                f"app.{self.app_name}.io/is-workload-entity": "true",
            },
            namespace=namespace,
        )

        if desired_resources is not None:
            desired_resources_list = codecs.load_all_yaml(desired_resources)
            diff_obj = in_left_not_right(left=existing_resources,
                                         right=desired_resources_list)
            for obj in diff_obj:
                self.delete_resource(obj)
            self.apply_manifest(desired_resources, namespace=namespace)