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)
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)
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)
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)
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)
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 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)
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_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 remove_manifest(manifest): client = Client() for resource in manifest: client.delete(type(resource), resource.metadata.name)
def test_delete_global(client: lightkube.Client): respx.delete("https://localhost:9443/api/v1/nodes/xx") client.delete(Node, name="xx")
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")
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)