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_list_namespaced(client: lightkube.Client): resp = {'items':[{'metadata': {'name': 'xx'}}, {'metadata': {'name': 'yy'}}]} respx.get("https://localhost:9443/api/v1/namespaces/default/pods").respond(json=resp) pods = client.list(Pod) assert [pod.metadata.name for pod in pods] == ['xx', 'yy'] respx.get("https://localhost:9443/api/v1/namespaces/other/pods?labelSelector=k%3Dv").respond(json=resp) pods = client.list(Pod, namespace="other", labels={'k': 'v'}) assert [pod.metadata.name for pod in pods] == ['xx', 'yy']
def test_list_global(client: lightkube.Client): resp = {'items': [{'metadata': {'name': 'xx'}}, {'metadata': {'name': 'yy'}}]} respx.get("https://localhost:9443/api/v1/nodes").respond(json=resp) nodes = client.list(Node) assert [node.metadata.name for node in nodes] == ['xx', 'yy'] respx.get("https://localhost:9443/api/v1/pods?fieldSelector=k%3Dx").respond(json=resp) pods = client.list(Pod, namespace=lightkube.ALL_NS, fields={'k': 'x'}) assert [pod.metadata.name for pod in pods] == ['xx', 'yy'] # Binding doesn't support all namespaces with pytest.raises(ValueError): client.list(Binding, namespace=lightkube.ALL_NS)
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_global_methods(): client = Client() nodes = [node.metadata.name for node in client.list(Node)] assert len(nodes) > 0 node = client.get(Node, name=nodes[0]) assert node.metadata.name == nodes[0] assert node.metadata.labels['kubernetes.io/os'] == node.status.nodeInfo.operatingSystem
def test_list_chunk_size(client: lightkube.Client): resp = {'items': [{'metadata': {'name': 'xx'}}, {'metadata': {'name': 'yy'}}], 'metadata': {'continue': 'yes'}} respx.get("https://localhost:9443/api/v1/namespaces/default/pods?limit=3").respond(json=resp) resp = {'items': [{'metadata': {'name': 'zz'}}]} respx.get("https://localhost:9443/api/v1/namespaces/default/pods?limit=3&continue=yes").respond(json=resp) pods = client.list(Pod, chunk_size=3) assert [pod.metadata.name for pod in pods] == ['xx', 'yy', 'zz']
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_wait_global(resource): client = Client() for obj in client.list(resource): client.wait(resource, obj.metadata.name, for_conditions=["Ready"])
def test_list_crd(client: lightkube.Client): """CRD list seems to return always the 'continue' metadata attribute""" resp = {'items': [{'metadata': {'name': 'xx'}}, {'metadata': {'name': 'yy'}}], 'metadata': {'continue': ''}} respx.get("https://localhost:9443/api/v1/namespaces/default/pods").respond(json=resp) pods = client.list(Pod) assert [pod.metadata.name for pod in pods] == ['xx', 'yy']
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)