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)
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)
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)
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
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)
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 ], ), )
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
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')
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()
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')
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"
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) )
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)
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, ), )
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)
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, []),