示例#1
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)
示例#2
0
def test_patch_namespaced(client: lightkube.Client):
    # Default PatchType.STRATEGIC
    req = respx.patch("https://localhost:9443/api/v1/namespaces/default/pods/xx").respond(json={'metadata': {'name': 'xx'}})
    pod = client.patch(Pod, "xx", Pod(metadata=ObjectMeta(labels={'l': 'ok'})))
    assert pod.metadata.name == 'xx'
    assert req.calls[0][0].headers['Content-Type'] == "application/strategic-merge-patch+json"

    # PatchType.MERGE
    req = respx.patch("https://localhost:9443/api/v1/namespaces/other/pods/xx").respond(json={'metadata': {'name': 'xx'}})
    pod = client.patch(Pod, "xx", Pod(metadata=ObjectMeta(labels={'l': 'ok'})), namespace='other',
                       patch_type=types.PatchType.MERGE, force=True)
    assert pod.metadata.name == 'xx'
    assert req.calls[0][0].headers['Content-Type'] == "application/merge-patch+json"
    assert 'force' not in str(req.calls[0][0].url)  # force is ignored for non APPLY patch types

    # PatchType.APPLY
    req = respx.patch("https://localhost:9443/api/v1/namespaces/other/pods/xy?fieldManager=test").respond(
        json={'metadata': {'name': 'xy'}})
    pod = client.patch(Pod, "xy", Pod(metadata=ObjectMeta(labels={'l': 'ok'})), namespace='other',
                       patch_type=types.PatchType.APPLY, field_manager='test')
    assert pod.metadata.name == 'xy'
    assert req.calls[0][0].headers['Content-Type'] == "application/apply-patch+yaml"

    # PatchType.APPLY + force
    req = respx.patch("https://localhost:9443/api/v1/namespaces/other/pods/xz?fieldManager=test&force=true").respond(
        json={'metadata': {'name': 'xz'}})
    pod = client.patch(Pod, "xz", Pod(metadata=ObjectMeta(labels={'l': 'ok'})), namespace='other',
                       patch_type=types.PatchType.APPLY, field_manager='test', force=True)
    assert pod.metadata.name == 'xz'
    assert req.calls[0][0].headers['Content-Type'] == "application/apply-patch+yaml"

    # PatchType.APPLY without field_manager
    with pytest.raises(ValueError, match="field_manager"):
        client.patch(Pod, "xz", Pod(metadata=ObjectMeta(labels={'l': 'ok'})), namespace='other',
                     patch_type=types.PatchType.APPLY)
示例#3
0
def test_replace_namespaced(client: lightkube.Client):
    req = respx.put("https://localhost:9443/api/v1/namespaces/default/pods/xy").respond(json={'metadata': {'name': 'xy'}})
    pod = client.replace(Pod(metadata=ObjectMeta(name="xy")))
    assert req.calls[0][0].read() == b'{"metadata": {"name": "xy"}}'
    assert pod.metadata.name == 'xy'

    respx.put("https://localhost:9443/api/v1/namespaces/other/pods/xz").respond(json={'metadata': {'name': 'xz'}})
    pod = client.replace(Pod(metadata=ObjectMeta(name="xz")), namespace='other')
    assert pod.metadata.name == 'xz'

    # namespace inside object definition need to match with provided namespace parameter.
    with pytest.raises(ValueError):
        client.replace(Pod(metadata=ObjectMeta(name="xx", namespace='ns1')), namespace='ns2')
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
async def test_patch_global(client: lightkube.AsyncClient):
    req = respx.patch("https://localhost:9443/api/v1/nodes/xx").respond(
        json={'metadata': {
            'name': 'xx'
        }})
    pod = await client.patch(Node,
                             "xx", [{
                                 "op": "add",
                                 "path": "/metadata/labels/x",
                                 "value": "y"
                             }],
                             patch_type=types.PatchType.JSON)
    assert pod.metadata.name == 'xx'
    assert req.calls[0][0].headers[
        'Content-Type'] == "application/json-patch+json"

    # PatchType.APPLY + force
    req = respx.patch(
        "https://localhost:9443/api/v1/nodes/xy?fieldManager=test&force=true"
    ).respond(json={'metadata': {
        'name': 'xy'
    }})
    node = await client.patch(Node,
                              "xy",
                              Pod(metadata=ObjectMeta(labels={'l': 'ok'})),
                              patch_type=types.PatchType.APPLY,
                              field_manager='test',
                              force=True)
    assert node.metadata.name == 'xy'
    assert req.calls[0][0].headers[
        'Content-Type'] == "application/apply-patch+yaml"

    await client.close()
def generate_pod_resource_list(pod_names):
    resources = [
        Pod(kind="Pod", metadata=ObjectMeta(name=str(name)))
        for name in pod_names
    ]

    return resources
示例#7
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)
示例#8
0
async def test_apply_global(client: lightkube.AsyncClient):
    req = respx.patch(
        "https://localhost:9443/api/v1/nodes/xy?fieldManager=test").respond(
            json={'metadata': {
                'name': 'xy'
            }})
    node = await client.apply(Node(metadata=ObjectMeta(name='xy')),
                              field_manager='test')
    assert node.metadata.name == 'xy'
    assert req.calls[0][0].headers[
        'Content-Type'] == "application/apply-patch+yaml"

    # sub-resource + force
    req = respx.patch(
        "https://localhost:9443/api/v1/nodes/xx/status?fieldManager=a&force=true"
    ).respond(json={'metadata': {
        'name': 'xx'
    }})
    node = await client.apply(Node.Status(),
                              name='xx',
                              field_manager='a',
                              force=True)
    assert node.metadata.name == 'xx'
    assert req.calls[0][0].headers[
        'Content-Type'] == "application/apply-patch+yaml"
    await client.close()
    def _service_object(
            self, ports: List[Tuple[str, int, Optional[int]]]) -> Service:
        """Creates a valid Service representation for Alertmanager.

        Args:
            ports: a list of tuples of the form (name, port) or (name, port, targetPort) for every
                service port. If the 'targetPort' is omitted, it is assumed to be equal to 'port'.

        Returns:
            Service: A valid representation of a Kubernetes Service with the correct ports.
        """
        return Service(
            apiVersion="v1",
            kind="Service",
            metadata=ObjectMeta(
                namespace=self._namespace,
                name=self._app,
                labels={"app.kubernetes.io/name": self._app},
            ),
            spec=ServiceSpec(
                selector={"app.kubernetes.io/name": self._app},
                ports=[
                    ServicePort(name=p[0],
                                port=p[1],
                                targetPort=p[2] if len(p) > 2 else p[1])
                    for p in ports
                ],
            ),
        )
示例#10
0
def test_dump_all_yaml():
    cm = ConfigMap(
        apiVersion='v1', kind='ConfigMap',
        metadata=ObjectMeta(name='xyz', labels={'x': 'y'})
    )
    Mydb = create_namespaced_resource('myapp.com', 'v1', 'Mydb', 'mydbs')

    db = Mydb(
        apiVersion='myapp.com/v1', kind='Mydb',
        metadata=ObjectMeta(name='db1'), xyz={'a': 'b'}
    )

    res = codecs.dump_all_yaml([cm, db])
    expected = textwrap.dedent("""
        apiVersion: v1
        kind: ConfigMap
        metadata:
          labels:
            x: y
          name: xyz
        ---
        apiVersion: myapp.com/v1
        kind: Mydb
        metadata:
          name: db1
        xyz:
          a: b
    """).lstrip()
    assert res == expected

    res = codecs.dump_all_yaml([db, cm], indent=4)
    expected = textwrap.dedent("""
        apiVersion: myapp.com/v1
        kind: Mydb
        metadata:
            name: db1
        xyz:
            a: b
        ---
        apiVersion: v1
        kind: ConfigMap
        metadata:
            labels:
                x: y
            name: xyz
    """).lstrip()
    assert res == expected
示例#11
0
def test_field_manager(kubeconfig):
    config = KubeConfig.from_file(str(kubeconfig))
    client = lightkube.Client(config=config, field_manager='lightkube')
    respx.patch("https://localhost:9443/api/v1/nodes/xx?fieldManager=lightkube").respond(json={'metadata': {'name': 'xx'}})
    client.patch(Node, "xx", [{"op": "add", "path": "/metadata/labels/x", "value": "y"}],
                       patch_type=types.PatchType.JSON)

    respx.post("https://localhost:9443/api/v1/namespaces/default/pods?fieldManager=lightkube").respond(json={'metadata': {'name': 'xx'}})
    client.create(Pod(metadata=ObjectMeta(name="xx", labels={'l': 'ok'})))

    respx.put("https://localhost:9443/api/v1/namespaces/default/pods/xy?fieldManager=lightkube").respond(
        json={'metadata': {'name': 'xy'}})
    client.replace(Pod(metadata=ObjectMeta(name="xy")))

    respx.put("https://localhost:9443/api/v1/namespaces/default/pods/xy?fieldManager=override").respond(
        json={'metadata': {'name': 'xy'}})
    client.replace(Pod(metadata=ObjectMeta(name="xy")), field_manager='override')
示例#12
0
async def test_replace_global(client: lightkube.AsyncClient):
    req = respx.put("https://localhost:9443/api/v1/nodes/xx").respond(
        json={'metadata': {
            'name': 'xx'
        }})
    pod = await client.replace(Node(metadata=ObjectMeta(name="xx")))
    assert req.calls[0][0].read() == b'{"metadata": {"name": "xx"}}'
    assert pod.metadata.name == 'xx'
    await client.close()
示例#13
0
def test_create_namespaced(client: lightkube.Client):
    req = respx.post("https://localhost:9443/api/v1/namespaces/default/pods").respond(json={'metadata': {'name': 'xx'}})
    pod = client.create(Pod(metadata=ObjectMeta(name="xx", labels={'l': 'ok'})))
    assert req.calls[0][0].read() == b'{"metadata": {"labels": {"l": "ok"}, "name": "xx"}}'
    assert pod.metadata.name == 'xx'

    req2 = respx.post("https://localhost:9443/api/v1/namespaces/other/pods").respond(json={'metadata': {'name': 'yy'}})
    pod = client.create(Pod(metadata=ObjectMeta(name="xx", labels={'l': 'ok'})), namespace='other')
    assert pod.metadata.name == 'yy'
    assert req2.calls[0][0].read() == b'{"metadata": {"labels": {"l": "ok"}, "name": "xx"}}'

    respx.post("https://localhost:9443/api/v1/namespaces/ns2/pods").respond(
        json={'metadata': {'name': 'yy'}})
    pod = client.create(Pod(metadata=ObjectMeta(name="xx", labels={'l': 'ok'}, namespace='ns2')))
    assert pod.metadata.name == 'yy'

    # namespace inside object definition need to match with provided namespace parameter.
    with pytest.raises(ValueError):
        client.create(Pod(metadata=ObjectMeta(name="xx", namespace='ns1')), namespace='ns2')
示例#14
0
def test_apply_namespaced(client: lightkube.Client):
    req = respx.patch("https://localhost:9443/api/v1/namespaces/default/pods/xy?fieldManager=test").respond(
        json={'metadata': {'name': 'xy'}})
    pod = client.apply(Pod(metadata=ObjectMeta(name='xy')), field_manager='test')
    assert pod.metadata.name == 'xy'
    assert req.calls[0][0].headers['Content-Type'] == "application/apply-patch+yaml"

    # custom namespace, force
    req = respx.patch("https://localhost:9443/api/v1/namespaces/other/pods/xz?fieldManager=a&force=true").respond(
        json={'metadata': {'name': 'xz'}})
    pod = client.apply(Pod(metadata=ObjectMeta(name='xz', namespace='other')), field_manager='a', force=True)
    assert pod.metadata.name == 'xz'
    assert req.calls[0][0].headers['Content-Type'] == "application/apply-patch+yaml"

    # sub-resource
    req = respx.patch("https://localhost:9443/api/v1/namespaces/default/pods/xx/status?fieldManager=a").respond(
        json={'metadata': {'name': 'xx'}})
    pod = client.apply(Pod.Status(), name='xx', field_manager='a')
    assert pod.metadata.name == 'xx'
    assert req.calls[0][0].headers['Content-Type'] == "application/apply-patch+yaml"
示例#15
0
def create_pod(name, command) -> Pod:
    return Pod(
        metadata=ObjectMeta(name=name, labels={'app-name': name}),
        spec=PodSpec(containers=[Container(
            name='main',
            image='busybox',
            args=[
                "/bin/sh",
                "-c",
                command
            ],
        )], terminationGracePeriodSeconds=1)
    )
示例#16
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)
示例#17
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)
    def _service_object(
        self,
        ports: Sequence[PortDefinition],
        service_name: str = None,
        service_type: ServiceType = "ClusterIP",
    ) -> Service:
        """Creates a valid Service representation for Alertmanager.

        Args:
            ports: a list of tuples of the form (name, port) or (name, port, targetPort)
                or (name, port, targetPort, nodePort) for every service port. If the 'targetPort'
                is omitted, it is assumed to be equal to 'port', with the exception of NodePort
                and LoadBalancer services, where all port numbers have to be specified.
            service_name: allows setting custom name to the patched service. If none given,
                application name will be used.
            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
                default value.

        Returns:
            Service: A valid representation of a Kubernetes Service with the correct ports.
        """
        if not service_name:
            service_name = self._app
        return Service(
            apiVersion="v1",
            kind="Service",
            metadata=ObjectMeta(
                namespace=self._namespace,
                name=service_name,
                labels={"app.kubernetes.io/name": service_name},
            ),
            spec=ServiceSpec(
                selector={"app.kubernetes.io/name": service_name},
                ports=[
                    ServicePort(
                        name=p[0],
                        port=p[1],
                        targetPort=p[2]
                        if len(p) > 2 else p[1],  # type: ignore[misc]
                        nodePort=p[3] if len(p) > 3 else
                        None,  # type: ignore[arg-type, misc]
                    ) for p in ports
                ],
                type=service_type,
            ),
        )
示例#19
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)
示例#20
0
def test_create_global(client: lightkube.Client):
    req = respx.post("https://localhost:9443/api/v1/nodes").respond(json={'metadata': {'name': 'xx'}})
    pod = client.create(Node(metadata=ObjectMeta(name="xx")))
    assert req.calls[0][0].read() == b'{"metadata": {"name": "xx"}}'
    assert pod.metadata.name == 'xx'
    names_deleted = tuple(c.args[0].metadata.name for c in delete_calls)
    assert sorted(names_deleted) == sorted(expected_resources_deleted_names)

    # Assert apply called if it should have been called
    assert rh.apply_manifest.call_count == 1
    resources_applied = mocked_load_all_yaml.return_value
    if resources_applied is None:
        resources_applied = tuple()
    names_applied = tuple(r.metadata.name for r in resources_applied)
    assert sorted(names_applied) == sorted(desired_resource_names)


# Resources for below tests
POD_LIST_1 = [
    Pod(kind="Pod",
        metadata=ObjectMeta(name=f"pod-{n}", namespace="some-namespace"))
    for n in range(5)
]
POD_TUPLES_1 = [("Pod", f"pod-{n}") for n in range(0, 5)]
POD_LIST_2 = [
    Pod(kind="Pod",
        metadata=ObjectMeta(name=f"pod-{n}", namespace="some-namespace"))
    for n in range(3, 7)
]
POD_TUPLES_IN_1_NOT_2 = [("Pod", f"pod-{n}") for n in range(0, 3)]


@pytest.mark.parametrize(
    "left,right,expected_result",
    [
        (POD_LIST_1, POD_LIST_1, []),