From d987b2b05767ffce03ea42a6ef84ff7503460b8f Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Thu, 29 Feb 2024 13:45:36 -0600 Subject: [PATCH 1/7] Produce apis to add a k8s cloud and tear down its models first --- .gitignore | 3 +- pytest_operator/plugin.py | 133 ++++++++++++++++++++-- tests/integration/test_opstest_add_k8s.py | 15 +++ tests/unit/test_pytest_operator.py | 2 +- 4 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 tests/integration/test_opstest_add_k8s.py diff --git a/.gitignore b/.gitignore index 768454d..2d331f3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ build dist/ *.orig report -.coverage \ No newline at end of file +.coverage +juju-crashdump* \ No newline at end of file diff --git a/pytest_operator/plugin.py b/pytest_operator/plugin.py index 62e80be..7559e6e 100644 --- a/pytest_operator/plugin.py +++ b/pytest_operator/plugin.py @@ -1,4 +1,5 @@ import asyncio +import base64 import contextlib import dataclasses import enum @@ -43,6 +44,8 @@ import yaml from _pytest.config import Config from _pytest.config.argparsing import Parser +from kubernetes.client import Configuration as K8sConfiguration +from juju.client import client from juju.client.jujudata import FileJujuData from juju.exceptions import DeadEntityException from juju.errors import JujuError @@ -399,6 +402,10 @@ class ModelInUseError(Exception): """Raise when trying to add a model alias which already exists.""" +BundleOpt = TypeVar("BundleOpt", str, Path, "OpsTest.Bundle") +Timeout = TypeVar("Timeout", float, int) + + @dataclasses.dataclass class ModelState: model: Model @@ -408,14 +415,18 @@ class ModelState: model_name: str config: Optional[dict] = None tmp_path: Optional[Path] = None + timeout: Optional[Timeout] = None @property def full_name(self) -> str: return f"{self.controller_name}:{self.model_name}" -BundleOpt = TypeVar("BundleOpt", str, Path, "OpsTest.Bundle") -Timeout = TypeVar("Timeout", float, int) +@dataclasses.dataclass +class CloudState: + cloud_name: str + models: List[str] = dataclasses.field(default_factory=list) + timeout: Optional[Timeout] = None class OpsTest: @@ -510,6 +521,7 @@ def __init__(self, request, tmp_path_factory): # use an OrderedDict so that the first model made is destroyed last. self._current_alias = None self._models: MutableMapping[str, ModelState] = OrderedDict() + self._clouds: MutableMapping[str, CloudState] = OrderedDict() @contextlib.contextmanager def model_context(self, alias: str) -> Generator[Model, None, None]: @@ -597,14 +609,16 @@ def keep_model(self) -> bool: current_state = self.current_alias and self._models.get(self.current_alias) return current_state.keep if current_state else self._init_keep_model - def _generate_model_name(self) -> str: + def _generate_name(self, kind: str) -> str: module_name = self.request.module.__name__.rpartition(".")[-1] suffix = "".join(choices(ascii_lowercase + digits, k=4)) + if kind != "model": + suffix = "-".join((kind, suffix)) return f"{module_name.replace('_', '-')}-{suffix}" @cached_property def default_model_name(self) -> str: - return self._generate_model_name() + return self._generate_name(kind="model") async def run( self, @@ -670,23 +684,33 @@ async def _add_model(self, cloud_name, model_name, keep=False, **kwargs): """ controller = self._controller controller_name = controller.controller_name + credential_name = None + timeout = None if not cloud_name: # if not provided, try the default cloud name cloud_name = self._init_cloud_name if not cloud_name: # if not provided, use the controller's default cloud cloud_name = await controller.get_cloud() + if ops_cloud := self._clouds.get(cloud_name): + credential_name = cloud_name + timeout = ops_cloud.timeout + model_full_name = f"{controller_name}:{model_name}" log.info(f"Adding model {model_full_name} on cloud {cloud_name}") - model = await controller.add_model(model_name, cloud_name, **kwargs) + model = await controller.add_model( + model_name, cloud_name, credential_name=credential_name, **kwargs + ) # NB: This call to `juju models` is needed because libjuju's # `add_model` doesn't update the models.yaml cache that the Juju # CLI depends on with the model's UUID, which the CLI requires to # connect. Calling `juju models` beforehand forces the CLI to # update the cache from the controller. await self.juju("models") - state = ModelState(model, keep, controller_name, cloud_name, model_name) + state = ModelState( + model, keep, controller_name, cloud_name, model_name, timeout=timeout + ) state.config = await model.get_config() return state @@ -820,11 +844,13 @@ async def track_model( ) else: cloud_name = cloud_name or self.cloud_name - model_name = model_name or self._generate_model_name() + model_name = model_name or self._generate_name(kind="model") model_state = await self._add_model( cloud_name, model_name, keep_val, **kwargs ) self._models[alias] = model_state + if ops_cloud := self._clouds.get(cloud_name): + ops_cloud.models.append(alias) return model_state.model async def log_model(self): @@ -886,6 +912,10 @@ async def forget_model( if alias not in self.models: raise ModelNotFoundError(f"{alias} not found") + model_state: ModelState = self._models[alias] + if timeout is None and model_state.timeout: + timeout = model_state.timeout + with self.model_context(alias) as model: await self.log_model() model_name = model.info.name @@ -896,13 +926,26 @@ async def forget_model( if not self.keep_model: await self._reset(model, allow_failure, timeout=timeout) await self._controller.destroy_model( - model_name, force=True, destroy_storage=destroy_storage + model_name, + force=True, + destroy_storage=destroy_storage, + max_wait=timeout, ) await model.disconnect() + async def model_alive(): + return model_name in await self._controller.list_models() + + if timeout and await model_alive(): + log.warning("Waiting for model %s to leave...", model_name) + while await model_alive(): + asyncio.sleep(5) + # stop managing this model now - log.info(f"Forgetting {alias}...") + log.info(f"Forgetting model {alias}...") self._models.pop(alias) + if ops_cloud := self._clouds.get(model_state.cloud_name): + ops_cloud.models.remove(alias) if alias is self.current_alias: self._current_alias = None @@ -933,7 +976,9 @@ async def _destroy(entity_name: str, **kwargs): try: await model.block_until( - lambda: len(model.machines) == 0 and len(model.applications) == 0, + lambda: len(model.units) == 0 + and len(model.machines) == 0 + and len(model.applications) == 0, timeout=timeout, ) except asyncio.TimeoutError: @@ -948,10 +993,15 @@ async def _destroy(entity_name: str, **kwargs): log.info(f"Reset {model.info.name} completed successfully.") async def _cleanup_models(self): + # remove clouds from most recently made, to first made + # each model in the cloud will be forgotten + for cloud in reversed(self._clouds): + await self.forget_cloud(cloud) + # remove models from most recently made, to first made aliases = list(reversed(self._models.keys())) - for models in aliases: - await self.forget_model(models) + for model in aliases: + await self.forget_model(model) await self._controller.disconnect() @@ -1491,3 +1541,62 @@ def is_crash_dump_enabled(self) -> bool: return True else: return False + + ### Add K8S + async def add_k8s(self, config: K8sConfiguration, **kwargs) -> str: + controller = self._controller + cloud_name = self._generate_name("k8s-cloud") + log.info(f"Adding k8s cloud {cloud_name}") + + cloud_def = client.Cloud( + auth_types=[ + "certificate", + "clientcertificate", + "oauth2", + "oauth2withcert", + "userpass", + ], + ca_certificates=[Path(config.ssl_ca_cert).read_text()], + endpoint=config.host, + host_cloud_region="kubernetes/ops-test", + regions=[client.CloudRegion(endpoint=config.host, name="k8s")], + skip_tls_verify=not config.verify_ssl, + type_="kubernetes", + ) + + if config.cert_file and config.key_file: + auth_type = "clientcertificate" + attrs = dict( + ClientCertificateData=Path(config.cert_file).read_text(), + ClientKeyData=Path(config.key_file).read_text(), + ) + elif token := config.api_key["authorization"]: + if token.startswith("Bearer "): + auth_type = "oauth2" + attrs = {"Token": token.split(" ")[1]} + elif token.startswith("Basic "): + auth_type, userpass = "userpass", token.split(" ")[1] + user, passwd = base64.b64decode(userpass).decode().split(":") + attrs = {"username": user, "password": passwd} + else: + raise ValueError("Failed to find credentials in authorization token") + else: + raise ValueError("Failed to find credentials in kubernetes.Configuration") + + await controller.add_cloud(cloud_name, cloud_def) + await controller.add_credential( + cloud_name, + credential=client.CloudCredential(attrs, auth_type), + cloud=cloud_name, + ) + self._clouds[cloud_name] = CloudState(cloud_name, timeout=5 * 60) + return cloud_name + + async def forget_cloud(self, cloud_name: str): + if cloud_name not in self._clouds: + raise KeyError(f"{cloud_name} not in clouds") + for model in reversed(self._clouds[cloud_name].models): + await self.forget_model(model) + log.info(f"Forgetting cloud: {cloud_name}...") + await self._controller.remove_cloud(cloud_name) + del self._clouds[cloud_name] diff --git a/tests/integration/test_opstest_add_k8s.py b/tests/integration/test_opstest_add_k8s.py new file mode 100644 index 0000000..ec9a1f0 --- /dev/null +++ b/tests/integration/test_opstest_add_k8s.py @@ -0,0 +1,15 @@ +# test that pytest operator supports adding a k8s to an existing controller +# This is a new k8s cloud created/managed by pytest-operator + +from pytest_operator.plugin import OpsTest +from kubernetes import config as k8s_config +from kubernetes.client import Configuration + +async def test_add_k8s_cloud(ops_test: OpsTest): + config = type.__call__(Configuration) + k8s_config.load_config(client_configuration=config) + k8s_cloud = await ops_test.add_k8s(config, skip_storage=True, storage_class=None) + k8s_model = await ops_test.track_model("secondary", cloud_name=k8s_cloud, keep=ops_test.ModelKeep.NEVER) + with ops_test.model_context("secondary"): + await k8s_model.deploy("coredns", trust=True) + await k8s_model.wait_for_idle(apps=["coredns"], status="active") diff --git a/tests/unit/test_pytest_operator.py b/tests/unit/test_pytest_operator.py index 68a48e7..9182058 100644 --- a/tests/unit/test_pytest_operator.py +++ b/tests/unit/test_pytest_operator.py @@ -594,7 +594,7 @@ async def test_fixture_set_up_automatic_model( await ops_test._setup_model() mock_juju.controller.add_model.assert_called_with( - model_name, "this-cloud", config=None + model_name, "this-cloud", credential_name=None, config=None ) juju_cmd.assert_called_with(ops_test, "models") assert ops_test.model == mock_juju.model From fb8b155aa707f5e61ff6d2bc617058d279617413 Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Thu, 29 Feb 2024 13:59:42 -0600 Subject: [PATCH 2/7] linting --- pytest_operator/plugin.py | 2 +- tests/data/test_lib/setup.py | 10 +++++----- tests/integration/test_opstest_add_k8s.py | 5 ++++- tox.ini | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pytest_operator/plugin.py b/pytest_operator/plugin.py index 7559e6e..7ddfd48 100644 --- a/pytest_operator/plugin.py +++ b/pytest_operator/plugin.py @@ -1542,7 +1542,7 @@ def is_crash_dump_enabled(self) -> bool: else: return False - ### Add K8S + # Add K8S async def add_k8s(self, config: K8sConfiguration, **kwargs) -> str: controller = self._controller cloud_name = self._generate_name("k8s-cloud") diff --git a/tests/data/test_lib/setup.py b/tests/data/test_lib/setup.py index 7812eff..662d31d 100644 --- a/tests/data/test_lib/setup.py +++ b/tests/data/test_lib/setup.py @@ -3,9 +3,9 @@ from setuptools import setup setup( - name="pytest-operator-test-lib", - version="1.0.0", - description="Pytest-operator test library.", - author="tester", - packages=[], + name="pytest-operator-test-lib", + version="1.0.0", + description="Pytest-operator test library.", + author="tester", + packages=[], ) diff --git a/tests/integration/test_opstest_add_k8s.py b/tests/integration/test_opstest_add_k8s.py index ec9a1f0..a403f60 100644 --- a/tests/integration/test_opstest_add_k8s.py +++ b/tests/integration/test_opstest_add_k8s.py @@ -5,11 +5,14 @@ from kubernetes import config as k8s_config from kubernetes.client import Configuration + async def test_add_k8s_cloud(ops_test: OpsTest): config = type.__call__(Configuration) k8s_config.load_config(client_configuration=config) k8s_cloud = await ops_test.add_k8s(config, skip_storage=True, storage_class=None) - k8s_model = await ops_test.track_model("secondary", cloud_name=k8s_cloud, keep=ops_test.ModelKeep.NEVER) + k8s_model = await ops_test.track_model( + "secondary", cloud_name=k8s_cloud, keep=ops_test.ModelKeep.NEVER + ) with ops_test.model_context("secondary"): await k8s_model.deploy("coredns", trust=True) await k8s_model.wait_for_idle(apps=["coredns"], status="active") diff --git a/tox.ini b/tox.ini index bac2c5d..ab26540 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps = [testenv:reformat] commands = - black pytest_operator tests/integration/test_pytest_operator.py tests/unit/test_pytest_operator.py + black pytest_operator tests/ deps = black From c4c2d172a2297ed4246ff54e8c16d562ac653412 Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Thu, 29 Feb 2024 14:50:43 -0600 Subject: [PATCH 3/7] Don't test if there's no k8s around, only wait for model to disappear when we're not keeping it --- pytest_operator/plugin.py | 16 ++++++++-------- tests/integration/test_opstest_add_k8s.py | 12 +++++++++--- tests/integration/test_pytest_operator.py | 3 +++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pytest_operator/plugin.py b/pytest_operator/plugin.py index 7ddfd48..6188c7b 100644 --- a/pytest_operator/plugin.py +++ b/pytest_operator/plugin.py @@ -916,6 +916,9 @@ async def forget_model( if timeout is None and model_state.timeout: timeout = model_state.timeout + async def is_model_alive(): + return model_name in await self._controller.list_models() + with self.model_context(alias) as model: await self.log_model() model_name = model.info.name @@ -931,15 +934,12 @@ async def forget_model( destroy_storage=destroy_storage, max_wait=timeout, ) - await model.disconnect() + if timeout and await is_model_alive(): + log.warning("Waiting for model %s to die...", model_name) + while await is_model_alive(): + asyncio.sleep(5) - async def model_alive(): - return model_name in await self._controller.list_models() - - if timeout and await model_alive(): - log.warning("Waiting for model %s to leave...", model_name) - while await model_alive(): - asyncio.sleep(5) + await model.disconnect() # stop managing this model now log.info(f"Forgetting model {alias}...") diff --git a/tests/integration/test_opstest_add_k8s.py b/tests/integration/test_opstest_add_k8s.py index a403f60..2172787 100644 --- a/tests/integration/test_opstest_add_k8s.py +++ b/tests/integration/test_opstest_add_k8s.py @@ -1,14 +1,20 @@ # test that pytest operator supports adding a k8s to an existing controller # This is a new k8s cloud created/managed by pytest-operator -from pytest_operator.plugin import OpsTest from kubernetes import config as k8s_config from kubernetes.client import Configuration +from kubernetes.config.config_exception import ConfigException +import pytest + +from pytest_operator.plugin import OpsTest -async def test_add_k8s_cloud(ops_test: OpsTest): +async def test_add_k8s(ops_test: OpsTest): config = type.__call__(Configuration) - k8s_config.load_config(client_configuration=config) + try: + k8s_config.load_config(client_configuration=config) + except ConfigException: + pytest.skip("No Kubernetes config found to add-k8s") k8s_cloud = await ops_test.add_k8s(config, skip_storage=True, storage_class=None) k8s_model = await ops_test.track_model( "secondary", cloud_name=k8s_cloud, keep=ops_test.ModelKeep.NEVER diff --git a/tests/integration/test_pytest_operator.py b/tests/integration/test_pytest_operator.py index 9650d55..03d2260 100644 --- a/tests/integration/test_pytest_operator.py +++ b/tests/integration/test_pytest_operator.py @@ -91,6 +91,9 @@ async def test_2_create_delete_new_model(self, ops_test): } # track the main model with a second alias, don't do this other than testing + # this will forget the duplicate without resetting/deleting the main model + # because "duplicate" will be in "keep_model" mode since ops_tests believes + # it's an existing model. model_name = prior_model.info.name duplicate = await ops_test.track_model("duplicate", model_name=model_name) assert duplicate.info.uuid == prior_model.info.uuid From 096105c2afb1cef1731451996eeeafb8aa8ee918 Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Thu, 29 Feb 2024 22:26:14 -0600 Subject: [PATCH 4/7] Successfully tested with storage --- pytest_operator/plugin.py | 94 ++++++++++++++++--- ...est_opstest_add_k8s.py => test_add_k8s.py} | 12 +-- 2 files changed, 84 insertions(+), 22 deletions(-) rename tests/integration/{test_opstest_add_k8s.py => test_add_k8s.py} (58%) diff --git a/pytest_operator/plugin.py b/pytest_operator/plugin.py index 6188c7b..3fae6c3 100644 --- a/pytest_operator/plugin.py +++ b/pytest_operator/plugin.py @@ -39,11 +39,13 @@ from zipfile import Path as ZipPath import jinja2 +import kubernetes.config import pytest import pytest_asyncio.plugin import yaml from _pytest.config import Config from _pytest.config.argparsing import Parser +from kubernetes import client as k8s_client from kubernetes.client import Configuration as K8sConfiguration from juju.client import client from juju.client.jujudata import FileJujuData @@ -937,7 +939,7 @@ async def is_model_alive(): if timeout and await is_model_alive(): log.warning("Waiting for model %s to die...", model_name) while await is_model_alive(): - asyncio.sleep(5) + await asyncio.sleep(5) await model.disconnect() @@ -1542,10 +1544,73 @@ def is_crash_dump_enabled(self) -> bool: else: return False - # Add K8S - async def add_k8s(self, config: K8sConfiguration, **kwargs) -> str: + async def add_k8s( + self, + cloud_name: Optional[str] = None, + kubeconfig: Optional[K8sConfiguration] = None, + context: Optional[str] = None, + skip_storage: bool = True, + storage_class: Optional[str] = None, + ) -> str: + """ + Add a new k8s cloud in the existing controller. + + @param Optional[str] cloud_name: + Name for the new cloud + None will autogenerate a name + @param Optional[kubernetes.client.configuration.Configuration] kubeconfig: + Configuration object from kubernetes.config.load_config + None will read from the usual kubeconfig locations like + os.environ.get('KUBECONFIG', '$HOME/.kube/config') + @param Optional[str] context: + context to use within the kubeconfig + None will use the default context + @param bool skip_storage: + True will not use cloud storage, + False either finds storage or uses storage_class + @param Optional[str] skip_storage: + cluster storage-class to use for juju storage + None will look for a default storage class within the cluster + + @returns str: cloud_name + + Common Examples: + ---------------------------------- + # make a new k8s cloud with any juju name and destroy it when the tests are over + await ops_test.add_k8s() + + # make a cloud known to juju as "bob" + await ops_test.add_k8s(cloud_name="my-k8s") + ---------------------------------- + """ + + if kubeconfig is None: + # kubeconfig should be auto-detected from the usual places + kubeconfig = type.__call__(K8sConfiguration) + kubernetes.config.load_config( + client_configuration=kubeconfig, + context=context, + temp_file_path=self.tmp_path, + ) + juju_cloud_config = {} + if not skip_storage and storage_class is None: + # lookup default storage-class + api_client = kubernetes.client.ApiClient(configuration=kubeconfig) + cluster = k8s_client.StorageV1Api(api_client=api_client) + for sc in cluster.list_storage_class().items: + if ( + sc.metadata.annotations.get( + "storageclass.kubernetes.io/is-default-class" + ) + == "true" + ): + storage_class = sc.metadata.name + if not skip_storage and storage_class: + juju_cloud_config["workload-storage"] = storage_class + juju_cloud_config["operator-storage"] = storage_class + controller = self._controller - cloud_name = self._generate_name("k8s-cloud") + cloud_name = cloud_name or self._generate_name("k8s-cloud") log.info(f"Adding k8s cloud {cloud_name}") cloud_def = client.Cloud( @@ -1556,27 +1621,28 @@ async def add_k8s(self, config: K8sConfiguration, **kwargs) -> str: "oauth2withcert", "userpass", ], - ca_certificates=[Path(config.ssl_ca_cert).read_text()], - endpoint=config.host, + ca_certificates=[Path(kubeconfig.ssl_ca_cert).read_text()], + endpoint=kubeconfig.host, host_cloud_region="kubernetes/ops-test", - regions=[client.CloudRegion(endpoint=config.host, name="k8s")], - skip_tls_verify=not config.verify_ssl, + regions=[client.CloudRegion(endpoint=kubeconfig.host, name="default")], + skip_tls_verify=not kubeconfig.verify_ssl, type_="kubernetes", + config=juju_cloud_config, ) - if config.cert_file and config.key_file: + if kubeconfig.cert_file and kubeconfig.key_file: auth_type = "clientcertificate" attrs = dict( - ClientCertificateData=Path(config.cert_file).read_text(), - ClientKeyData=Path(config.key_file).read_text(), + ClientCertificateData=Path(kubeconfig.cert_file).read_text(), + ClientKeyData=Path(kubeconfig.key_file).read_text(), ) - elif token := config.api_key["authorization"]: + elif token := kubeconfig.api_key["authorization"]: if token.startswith("Bearer "): auth_type = "oauth2" attrs = {"Token": token.split(" ")[1]} elif token.startswith("Basic "): auth_type, userpass = "userpass", token.split(" ")[1] - user, passwd = base64.b64decode(userpass).decode().split(":") + user, passwd = base64.b64decode(userpass).decode().split(":", 1) attrs = {"username": user, "password": passwd} else: raise ValueError("Failed to find credentials in authorization token") @@ -1596,7 +1662,7 @@ async def forget_cloud(self, cloud_name: str): if cloud_name not in self._clouds: raise KeyError(f"{cloud_name} not in clouds") for model in reversed(self._clouds[cloud_name].models): - await self.forget_model(model) + await self.forget_model(model, destroy_storage=True) log.info(f"Forgetting cloud: {cloud_name}...") await self._controller.remove_cloud(cloud_name) del self._clouds[cloud_name] diff --git a/tests/integration/test_opstest_add_k8s.py b/tests/integration/test_add_k8s.py similarity index 58% rename from tests/integration/test_opstest_add_k8s.py rename to tests/integration/test_add_k8s.py index 2172787..d23d20e 100644 --- a/tests/integration/test_opstest_add_k8s.py +++ b/tests/integration/test_add_k8s.py @@ -1,24 +1,20 @@ # test that pytest operator supports adding a k8s to an existing controller # This is a new k8s cloud created/managed by pytest-operator -from kubernetes import config as k8s_config -from kubernetes.client import Configuration from kubernetes.config.config_exception import ConfigException import pytest - from pytest_operator.plugin import OpsTest async def test_add_k8s(ops_test: OpsTest): - config = type.__call__(Configuration) try: - k8s_config.load_config(client_configuration=config) + k8s_cloud = await ops_test.add_k8s(skip_storage=False) except ConfigException: pytest.skip("No Kubernetes config found to add-k8s") - k8s_cloud = await ops_test.add_k8s(config, skip_storage=True, storage_class=None) + k8s_model = await ops_test.track_model( "secondary", cloud_name=k8s_cloud, keep=ops_test.ModelKeep.NEVER ) with ops_test.model_context("secondary"): - await k8s_model.deploy("coredns", trust=True) - await k8s_model.wait_for_idle(apps=["coredns"], status="active") + await k8s_model.deploy("grafana-k8s", trust=True) + await k8s_model.wait_for_idle(apps=["grafana-k8s"], status="active") From 9817d3a8ae002fc842b055827b5869c48928898f Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Fri, 1 Mar 2024 09:27:49 -0600 Subject: [PATCH 5/7] workaround issue https://github.com/kubernetes-client/python/issues/2203 --- tests/integration/test_add_k8s.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_add_k8s.py b/tests/integration/test_add_k8s.py index d23d20e..e990e84 100644 --- a/tests/integration/test_add_k8s.py +++ b/tests/integration/test_add_k8s.py @@ -9,7 +9,7 @@ async def test_add_k8s(ops_test: OpsTest): try: k8s_cloud = await ops_test.add_k8s(skip_storage=False) - except ConfigException: + except (ConfigException, TypeError): pytest.skip("No Kubernetes config found to add-k8s") k8s_model = await ops_test.track_model( From 063ebd093b60220634d5e563e981d7709dc738d3 Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Fri, 22 Mar 2024 10:37:38 -0500 Subject: [PATCH 6/7] Adjust linting for BundleOpt --- .gitignore | 3 ++- pytest_operator/plugin.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2d331f3..336015c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ dist/ *.orig report .coverage -juju-crashdump* \ No newline at end of file +juju-crashdump* + diff --git a/pytest_operator/plugin.py b/pytest_operator/plugin.py index 3fae6c3..a11e674 100644 --- a/pytest_operator/plugin.py +++ b/pytest_operator/plugin.py @@ -404,7 +404,7 @@ class ModelInUseError(Exception): """Raise when trying to add a model alias which already exists.""" -BundleOpt = TypeVar("BundleOpt", str, Path, "OpsTest.Bundle") +BundleOpt = Union[str, Path, "OpsTest.Bundle"] Timeout = TypeVar("Timeout", float, int) From 7ada8cf5a2e0ef0921b1e132ee46f724f1cad73d Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Mon, 29 Apr 2024 09:02:47 -0500 Subject: [PATCH 7/7] test with updated inclusivity workflow --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4f262a8..12ef4b8 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -6,7 +6,7 @@ on: jobs: call-inclusive-naming-check: name: Inclusive naming - uses: canonical-web-and-design/Inclusive-naming/.github/workflows/woke.yaml@main + uses: canonical/inclusive-naming/.github/workflows/woke.yaml@main with: fail-on-error: "true"