From 5b9a3027d4e73c9203afb267381c1dc0298f2cf3 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 1 May 2026 10:31:46 -0400 Subject: [PATCH 01/12] feat: More cross-track itest assertions --- .../test_upgrade_cos_lite_tls_full.py | 13 ++-- tests/integration/helpers.py | 60 ++++++++++++++----- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py index 38225452..210e8c12 100644 --- a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py +++ b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py @@ -32,11 +32,16 @@ def test_deploy_from_track_2( def test_deploy_to_track_dev( tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju ): - # WHEN upgraded to track dev + jubilant_args = {"ca_model": ca_model.model, "cos_model": cos_model.model} + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) - - # THEN the model is upgraded and is healthy + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) + # THEN a single apply is sufficient (no further changes pending) + # * the model is upgraded and is healthy + assert not tf_manager.plan_has_changes(**jubilant_args) wait_for_active_idle_without_error([ca_model, cos_model]) tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") catalogue_apps_are_reachable(cos_model, tls_ctx) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 84f5a6e5..1f6a3ce4 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -31,30 +31,58 @@ def init(self, tf_file: str): def _args_str(target: Optional[str] = None, **kwargs) -> str: target_arg = f"-target module.{target}" if target else "" var_args = " ".join(f"-var {k}={v}" for k, v in kwargs.items()) - return "-auto-approve " + f"{target_arg} " + var_args + return f"{target_arg} " + var_args + + def plan_json(self, **kwargs) -> dict: + """Run terraform plan and return the JSON representation.""" + plan_file = os.path.join(self.dir, "tfplan") + extras = self._args_str(**kwargs) + plan_cmd = f"{self.tf_cmd} plan -out={plan_file} " + extras + subprocess.run(shlex.split(plan_cmd), check=True) + show_cmd = f"{self.tf_cmd} show -json {plan_file}" + result = subprocess.run( + shlex.split(show_cmd), check=True, capture_output=True, text=True + ) + return json.loads(result.stdout) + + def plan_has_changes(self, **kwargs) -> bool: + """Return True if terraform plan detects pending changes. + + Uses -detailed-exitcode: exit 0 means no changes, exit 2 means changes pending. + """ + extras = self._args_str(**kwargs) + plan_cmd = f"{self.tf_cmd} plan -detailed-exitcode " + extras + result = subprocess.run(shlex.split(plan_cmd)) + if result.returncode == 0: + return False + if result.returncode == 2: + return True + # Exit code 1 means error + raise subprocess.CalledProcessError(result.returncode, plan_cmd) + # TODO: Rename to apps_to_replace + def apps_to_replace(self, destroys: List[str], **kwargs): + plan = self.plan_json(**kwargs) + destroy_plan_apps = [ + rc["address"] + for rc in plan.get("resource_changes", []) + if "delete" in rc["change"]["actions"] + and rc.get("type", "") == "juju_application" + ] + assert set(destroys) == set(destroy_plan_apps), ( + f"Expected destroys: {destroys}, got: {destroy_plan_apps}" + ) def apply(self, target: Optional[str] = None, **kwargs): - cmd_str = f"{self.tf_cmd} apply " + self._args_str(target, **kwargs) + extras = self._args_str(target, **kwargs) + cmd_str = f"{self.tf_cmd} apply -auto-approve " + extras subprocess.run(shlex.split(cmd_str), check=True) def destroy(self, **kwargs): - cmd_str = f"{self.tf_cmd} destroy " + self._args_str(None, **kwargs) + extras = self._args_str(**kwargs) + cmd_str = f"{self.tf_cmd} destroy -auto-approve " + extras subprocess.run(shlex.split(cmd_str), check=True) -def refresh_o11y_apps(juju: jubilant.Juju, channel: str, base: Optional[str] = None): - """Temporary workaround for the issue: - - FIXME: https://github.com/juju/terraform-provider-juju/issues/967 - """ - for app in juju.status().apps: - if app in {"traefik", "ca"}: - continue - if "s3-integrator" in app: - continue - juju.refresh(app, channel=channel, base=base) - - def wait_for_active_idle_without_error( jujus: List[jubilant.Juju], timeout: int = 60 * 45 ): From 91604235adcf62a614a0e07acc5d07d1f7fe1c95 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Mon, 4 May 2026 09:42:09 -0400 Subject: [PATCH 02/12] chore --- .../cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py index 210e8c12..b65ea120 100644 --- a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py +++ b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py @@ -41,7 +41,7 @@ def test_deploy_to_track_dev( tf_manager.apply(**jubilant_args) # THEN a single apply is sufficient (no further changes pending) # * the model is upgraded and is healthy - assert not tf_manager.plan_has_changes(**jubilant_args) wait_for_active_idle_without_error([ca_model, cos_model]) tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") catalogue_apps_are_reachable(cos_model, tls_ctx) + assert not tf_manager.plan_has_changes(**jubilant_args) From 2d625235f97bc8fc1c1ad9249d5e253a7bd2ce78 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Thu, 7 May 2026 09:03:44 -0400 Subject: [PATCH 03/12] chore --- tests/integration/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 1f6a3ce4..2206e0b2 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -59,11 +59,11 @@ def plan_has_changes(self, **kwargs) -> bool: return True # Exit code 1 means error raise subprocess.CalledProcessError(result.returncode, plan_cmd) - # TODO: Rename to apps_to_replace + def apps_to_replace(self, destroys: List[str], **kwargs): plan = self.plan_json(**kwargs) destroy_plan_apps = [ - rc["address"] + rc["address"].split(".")[-1] for rc in plan.get("resource_changes", []) if "delete" in rc["change"]["actions"] and rc.get("type", "") == "juju_application" From f21ee03ff7f84c8792f7b0f0a7984cbf1224f99a Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Sat, 16 May 2026 09:11:53 +0200 Subject: [PATCH 04/12] chore --- .../tls_full/test_upgrade_cos_lite_tls_full.py | 15 ++++++--------- tests/integration/helpers.py | 7 +++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py index b65ea120..02cfe759 100644 --- a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py +++ b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py @@ -8,6 +8,7 @@ from helpers import ( catalogue_apps_are_reachable, + cos_assertions, get_tls_context, wait_for_active_idle_without_error, ) @@ -24,9 +25,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) - wait_for_active_idle_without_error([ca_model, cos_model], timeout=60 * 60) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + cos_assertions(tmp_path, ca_model.model, cos_model.model) def test_deploy_to_track_dev( @@ -39,9 +38,7 @@ def test_deploy_to_track_dev( tf_manager.apps_to_replace(["grafana"], **jubilant_args) # AND WHEN upgraded to track dev tf_manager.apply(**jubilant_args) - # THEN a single apply is sufficient (no further changes pending) - # * the model is upgraded and is healthy - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) - assert not tf_manager.plan_has_changes(**jubilant_args) + # THEN the product passes generic assertions + cos_assertions(tmp_path, ca_model.model, cos_model.model) + # AND a single apply is sufficient (no further changes pending) + # TODO: assert not tf_manager.plan_has_changes(**jubilant_args) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 2206e0b2..3e2d8960 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -83,6 +83,13 @@ def destroy(self, **kwargs): subprocess.run(shlex.split(cmd_str), check=True) +def cos_assertions(temp_path: Path, ca_model: jubilant.Juju, cos_model: jubilant.Juju): + # generic assertions that are shared between products: cos, cos-lite + wait_for_active_idle_without_error([ca_model, cos_model], timeout=60 * 60) + tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") + catalogue_apps_are_reachable(cos_model, tls_ctx) + + def wait_for_active_idle_without_error( jujus: List[jubilant.Juju], timeout: int = 60 * 45 ): From 3b2c30e35009895b473c8683aefaf3bd171120ae Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 19 May 2026 12:32:09 -0400 Subject: [PATCH 05/12] chore: assert generic assertions everywhere --- .../tls_external/test_upgrade_cos_tls_external.py | 15 +++------------ .../cos/tls_full/test_upgrade_cos_tls_full.py | 15 +++------------ .../tls_internal/test_upgrade_cos_tls_internal.py | 12 +++--------- .../cos/tls_none/test_upgrade_cos_tls_none.py | 12 +++--------- .../tls_external/test_cos_dev_tls_external.py | 15 +++++---------- .../cos_dev/tls_full/test_cos_dev_tls_full.py | 15 +++++---------- .../tls_internal/test_cos_dev_tls_internal.py | 9 ++------- .../cos_dev/tls_none/test_cos_dev_tls_none.py | 9 ++------- .../test_upgrade_cos_lite_tls_external.py | 14 +++----------- .../tls_full/test_upgrade_cos_lite_tls_full.py | 11 +++-------- .../test_upgrade_cos_lite_tls_internal.py | 8 +++----- .../tls_none/test_upgrade_cos_lite_tls_none.py | 8 +++----- tests/integration/helpers.py | 12 ++++++++++-- 13 files changed, 48 insertions(+), 107 deletions(-) diff --git a/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py b/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py index c360df87..33b46e17 100644 --- a/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py +++ b/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py @@ -8,12 +8,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -36,9 +31,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) - wait_for_active_idle_without_error([cos_model], timeout=5400) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) @@ -50,7 +43,5 @@ def test_deploy_to_track_dev( tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py b/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py index a817a76b..9a48df79 100644 --- a/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py +++ b/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py @@ -8,12 +8,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -36,9 +31,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) - wait_for_active_idle_without_error([cos_model], timeout=5400) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) @@ -50,7 +43,5 @@ def test_deploy_to_track_dev( tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py b/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py index d288c0e2..c0425362 100644 --- a/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py +++ b/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py @@ -8,11 +8,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -33,8 +29,7 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) - wait_for_active_idle_without_error([cos_model], timeout=5400) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) @@ -44,6 +39,5 @@ def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py b/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py index eaf985dd..7f33b146 100644 --- a/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py +++ b/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py @@ -8,11 +8,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -33,8 +29,7 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) - wait_for_active_idle_without_error([cos_model], timeout=5400) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) @@ -44,6 +39,5 @@ def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos_dev/tls_external/test_cos_dev_tls_external.py b/tests/integration/cos_dev/tls_external/test_cos_dev_tls_external.py index 79b79151..a2644591 100644 --- a/tests/integration/cos_dev/tls_external/test_cos_dev_tls_external.py +++ b/tests/integration/cos_dev/tls_external/test_cos_dev_tls_external.py @@ -7,20 +7,15 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" -def test_deploy(tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju): +def test_deploy( + tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju +): tf_manager.init(TRACK_DEV_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos_dev/tls_full/test_cos_dev_tls_full.py b/tests/integration/cos_dev/tls_full/test_cos_dev_tls_full.py index 1706a3f5..593f74e6 100644 --- a/tests/integration/cos_dev/tls_full/test_cos_dev_tls_full.py +++ b/tests/integration/cos_dev/tls_full/test_cos_dev_tls_full.py @@ -7,20 +7,15 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" -def test_deploy(tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju): +def test_deploy( + tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju +): tf_manager.init(TRACK_DEV_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos_dev/tls_internal/test_cos_dev_tls_internal.py b/tests/integration/cos_dev/tls_internal/test_cos_dev_tls_internal.py index 01bb44b5..3fd55e88 100644 --- a/tests/integration/cos_dev/tls_internal/test_cos_dev_tls_internal.py +++ b/tests/integration/cos_dev/tls_internal/test_cos_dev_tls_internal.py @@ -6,11 +6,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -18,6 +14,5 @@ def test_deploy(tf_manager, cos_model: jubilant.Juju): tf_manager.init(TRACK_DEV_TF_FILE) tf_manager.apply(model=cos_model.model) - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos_dev/tls_none/test_cos_dev_tls_none.py b/tests/integration/cos_dev/tls_none/test_cos_dev_tls_none.py index 336fe093..f9404a29 100644 --- a/tests/integration/cos_dev/tls_none/test_cos_dev_tls_none.py +++ b/tests/integration/cos_dev/tls_none/test_cos_dev_tls_none.py @@ -6,11 +6,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - no_errors_in_otelcol_logs, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions, no_errors_in_otelcol_logs TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -18,6 +14,5 @@ def test_deploy(tf_manager, cos_model: jubilant.Juju): tf_manager.init(TRACK_DEV_TF_FILE) tf_manager.apply(model=cos_model.model) - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py b/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py index bded1290..46e5285a 100644 --- a/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py +++ b/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py @@ -7,11 +7,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - get_tls_context, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -23,9 +19,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) - wait_for_active_idle_without_error([ca_model, cos_model], timeout=60 * 60) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) def test_deploy_to_track_dev( @@ -36,6 +30,4 @@ def test_deploy_to_track_dev( tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([ca_model, cos_model]) - tls_ctx = get_tls_context(tmp_path, ca_model, "self-signed-certificates") - catalogue_apps_are_reachable(cos_model, tls_ctx) + generic_assertions(cos_model, ca_model, tmp_path) diff --git a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py index e713dfa4..8afdbdf3 100644 --- a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py +++ b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py @@ -7,12 +7,7 @@ from pathlib import Path import jubilant -from helpers import ( - catalogue_apps_are_reachable, - cos_assertions, - get_tls_context, - wait_for_active_idle_without_error, -) +from helpers import generic_assertions TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -24,7 +19,7 @@ def test_deploy_from_track_2( # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) - cos_assertions(tmp_path, ca_model.model, cos_model.model) + generic_assertions(cos_model, ca_model, tmp_path) def test_deploy_to_track_dev( @@ -38,6 +33,6 @@ def test_deploy_to_track_dev( # AND WHEN upgraded to track dev tf_manager.apply(**jubilant_args) # THEN the product passes generic assertions - cos_assertions(tmp_path, ca_model.model, cos_model.model) + generic_assertions(cos_model, ca_model, tmp_path) # AND a single apply is sufficient (no further changes pending) # TODO: assert not tf_manager.plan_has_changes(**jubilant_args) diff --git a/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py b/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py index 9e49fea8..b5d91db6 100644 --- a/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py +++ b/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py @@ -7,7 +7,7 @@ from pathlib import Path import jubilant -from helpers import catalogue_apps_are_reachable, wait_for_active_idle_without_error +from helpers import generic_assertions TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -17,8 +17,7 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(model=cos_model.model) - wait_for_active_idle_without_error([cos_model], timeout=60 * 60) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): @@ -27,5 +26,4 @@ def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): tf_manager.apply(model=cos_model.model) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) diff --git a/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py b/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py index f253fa9b..0d9eae1c 100644 --- a/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py +++ b/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py @@ -7,7 +7,7 @@ from pathlib import Path import jubilant -from helpers import catalogue_apps_are_reachable, wait_for_active_idle_without_error +from helpers import generic_assertions TRACK_2_TF_FILE = Path(__file__).parent.resolve() / "track-2.tf" TRACK_DEV_TF_FILE = Path(__file__).parent.resolve() / "track-dev.tf" @@ -17,8 +17,7 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): # GIVEN a module deployed from track 2 tf_manager.init(TRACK_2_TF_FILE) tf_manager.apply(model=cos_model.model) - wait_for_active_idle_without_error([cos_model], timeout=60 * 60) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): @@ -27,5 +26,4 @@ def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): tf_manager.apply(model=cos_model.model) # THEN the model is upgraded and is healthy - wait_for_active_idle_without_error([cos_model]) - catalogue_apps_are_reachable(cos_model) + generic_assertions(cos_model) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 874ebac4..6e756175 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -84,10 +84,18 @@ def destroy(self, **kwargs): subprocess.run(shlex.split(cmd_str), check=True) -def cos_assertions(temp_path: Path, ca_model: jubilant.Juju, cos_model: jubilant.Juju): +def generic_assertions( + cos_model: jubilant.Juju, + ca_model: jubilant.Juju | None = None, + temp_path: Path | None = None, +): # generic assertions that are shared between products: cos, cos-lite wait_for_active_idle_without_error([ca_model, cos_model], timeout=60 * 60) - tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") + if ca_model: + assert temp_path is not None, "temp_path is required when ca_model is provided" + tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") + else: + tls_ctx = None catalogue_apps_are_reachable(cos_model, tls_ctx) From a271fb488bcd7cde0623d375831d2bae8a84b20d Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 19 May 2026 12:34:35 -0400 Subject: [PATCH 06/12] chore: remove extra itests --- .../tls_full/test_upgrade_cos_lite_tls_full.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py index 8afdbdf3..e834b153 100644 --- a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py +++ b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py @@ -25,14 +25,9 @@ def test_deploy_from_track_2( def test_deploy_to_track_dev( tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju ): - jubilant_args = {"ca_model": ca_model.model, "cos_model": cos_model.model} - # WHEN planning to upgrade to track dev + # WHEN upgraded to track dev tf_manager.init(TRACK_DEV_TF_FILE) - # THEN only expected apps are replaced - tf_manager.apps_to_replace(["grafana"], **jubilant_args) - # AND WHEN upgraded to track dev - tf_manager.apply(**jubilant_args) - # THEN the product passes generic assertions + tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) + + # THEN the model is upgraded and is healthy generic_assertions(cos_model, ca_model, tmp_path) - # AND a single apply is sufficient (no further changes pending) - # TODO: assert not tf_manager.plan_has_changes(**jubilant_args) From c609aa3f59a628875a85751c2aca744eafc7fb67 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 19 May 2026 12:36:28 -0400 Subject: [PATCH 07/12] chore: remove extra itests --- tests/integration/helpers.py | 59 +++--------------------------------- 1 file changed, 5 insertions(+), 54 deletions(-) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 6e756175..7b014f8f 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -32,70 +32,21 @@ def init(self, tf_file: str): def _args_str(target: Optional[str] = None, **kwargs) -> str: target_arg = f"-target module.{target}" if target else "" var_args = " ".join(f"-var {k}={v}" for k, v in kwargs.items()) - return f"{target_arg} " + var_args - - def plan_json(self, **kwargs) -> dict: - """Run terraform plan and return the JSON representation.""" - plan_file = os.path.join(self.dir, "tfplan") - extras = self._args_str(**kwargs) - plan_cmd = f"{self.tf_cmd} plan -out={plan_file} " + extras - subprocess.run(shlex.split(plan_cmd), check=True) - show_cmd = f"{self.tf_cmd} show -json {plan_file}" - result = subprocess.run( - shlex.split(show_cmd), check=True, capture_output=True, text=True - ) - return json.loads(result.stdout) - - def plan_has_changes(self, **kwargs) -> bool: - """Return True if terraform plan detects pending changes. - - Uses -detailed-exitcode: exit 0 means no changes, exit 2 means changes pending. - """ - extras = self._args_str(**kwargs) - plan_cmd = f"{self.tf_cmd} plan -detailed-exitcode " + extras - result = subprocess.run(shlex.split(plan_cmd)) - if result.returncode == 0: - return False - if result.returncode == 2: - return True - # Exit code 1 means error - raise subprocess.CalledProcessError(result.returncode, plan_cmd) - - def apps_to_replace(self, destroys: List[str], **kwargs): - plan = self.plan_json(**kwargs) - destroy_plan_apps = [ - rc["address"].split(".")[-1] - for rc in plan.get("resource_changes", []) - if "delete" in rc["change"]["actions"] - and rc.get("type", "") == "juju_application" - ] - assert set(destroys) == set(destroy_plan_apps), ( - f"Expected destroys: {destroys}, got: {destroy_plan_apps}" - ) + return "-auto-approve " + f"{target_arg} " + var_args def apply(self, target: Optional[str] = None, **kwargs): - extras = self._args_str(target, **kwargs) - cmd_str = f"{self.tf_cmd} apply -auto-approve " + extras + cmd_str = f"{self.tf_cmd} apply " + self._args_str(target, **kwargs) subprocess.run(shlex.split(cmd_str), check=True) def destroy(self, **kwargs): - extras = self._args_str(**kwargs) - cmd_str = f"{self.tf_cmd} destroy -auto-approve " + extras + cmd_str = f"{self.tf_cmd} destroy " + self._args_str(None, **kwargs) subprocess.run(shlex.split(cmd_str), check=True) -def generic_assertions( - cos_model: jubilant.Juju, - ca_model: jubilant.Juju | None = None, - temp_path: Path | None = None, -): +def cos_assertions(temp_path: Path, ca_model: jubilant.Juju, cos_model: jubilant.Juju): # generic assertions that are shared between products: cos, cos-lite wait_for_active_idle_without_error([ca_model, cos_model], timeout=60 * 60) - if ca_model: - assert temp_path is not None, "temp_path is required when ca_model is provided" - tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") - else: - tls_ctx = None + tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") catalogue_apps_are_reachable(cos_model, tls_ctx) From 01847393ee2236949c09d85e0e5b58b23f8a8916 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 19 May 2026 13:01:03 -0400 Subject: [PATCH 08/12] Apply suggestions from code review Co-authored-by: Michael Thamm Signed-off-by: Michael Thamm --- tests/integration/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 7b014f8f..98baf29c 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -43,8 +43,8 @@ def destroy(self, **kwargs): subprocess.run(shlex.split(cmd_str), check=True) -def cos_assertions(temp_path: Path, ca_model: jubilant.Juju, cos_model: jubilant.Juju): - # generic assertions that are shared between products: cos, cos-lite +def generic_assertions(cos_model: jubilant.Juju, ca_model: jubilant.Juju, temp_path: Path): + # generic assertions that are shared between all products e.g. cos, cos-lite, cos-deb wait_for_active_idle_without_error([ca_model, cos_model], timeout=60 * 60) tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") catalogue_apps_are_reachable(cos_model, tls_ctx) From ccb9f90454381b693f0bd95e6d808d78303fbd47 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 19 May 2026 13:06:00 -0400 Subject: [PATCH 09/12] Update generic_assertions function signature Refactor generic_assertions to accept optional parameters for ca_model and temp_path. Signed-off-by: Michael Thamm --- tests/integration/helpers.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 98baf29c..6c0f8ac9 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -43,10 +43,18 @@ def destroy(self, **kwargs): subprocess.run(shlex.split(cmd_str), check=True) -def generic_assertions(cos_model: jubilant.Juju, ca_model: jubilant.Juju, temp_path: Path): - # generic assertions that are shared between all products e.g. cos, cos-lite, cos-deb +def generic_assertions( + cos_model: jubilant.Juju, + ca_model: jubilant.Juju | None = None, + temp_path: Path | None = None, +): + # generic assertions that are shared between products: cos, cos-lite wait_for_active_idle_without_error([ca_model, cos_model], timeout=60 * 60) - tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") + if ca_model: + assert temp_path is not None, "temp_path is required when ca_model is provided" + tls_ctx = get_tls_context(temp_path, ca_model, "self-signed-certificates") + else: + tls_ctx = None catalogue_apps_are_reachable(cos_model, tls_ctx) From 6538b0d5cfd8a2bd7e547959cbf7bc0126eb89db Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 19 May 2026 13:03:36 -0400 Subject: [PATCH 10/12] feat: TF assertions in itests --- .../test_upgrade_cos_lite_tls_full.py | 14 ++++-- tests/integration/helpers.py | 47 +++++++++++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py index e834b153..c4d544ce 100644 --- a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py +++ b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py @@ -25,9 +25,15 @@ def test_deploy_from_track_2( def test_deploy_to_track_dev( tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju ): - # WHEN upgraded to track dev + jubilant_args = {"ca_model": ca_model.model, "cos_model": cos_model.model} + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) - - # THEN the model is upgraded and is healthy + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) + # THEN the product passes generic assertions generic_assertions(cos_model, ca_model, tmp_path) + # AND a single apply is sufficient (no further changes pending) + # TODO: Maybe this is too much work? + # TODO: assert not tf_manager.plan_has_changes(**jubilant_args) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 6c0f8ac9..6e756175 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -32,14 +32,55 @@ def init(self, tf_file: str): def _args_str(target: Optional[str] = None, **kwargs) -> str: target_arg = f"-target module.{target}" if target else "" var_args = " ".join(f"-var {k}={v}" for k, v in kwargs.items()) - return "-auto-approve " + f"{target_arg} " + var_args + return f"{target_arg} " + var_args + + def plan_json(self, **kwargs) -> dict: + """Run terraform plan and return the JSON representation.""" + plan_file = os.path.join(self.dir, "tfplan") + extras = self._args_str(**kwargs) + plan_cmd = f"{self.tf_cmd} plan -out={plan_file} " + extras + subprocess.run(shlex.split(plan_cmd), check=True) + show_cmd = f"{self.tf_cmd} show -json {plan_file}" + result = subprocess.run( + shlex.split(show_cmd), check=True, capture_output=True, text=True + ) + return json.loads(result.stdout) + + def plan_has_changes(self, **kwargs) -> bool: + """Return True if terraform plan detects pending changes. + + Uses -detailed-exitcode: exit 0 means no changes, exit 2 means changes pending. + """ + extras = self._args_str(**kwargs) + plan_cmd = f"{self.tf_cmd} plan -detailed-exitcode " + extras + result = subprocess.run(shlex.split(plan_cmd)) + if result.returncode == 0: + return False + if result.returncode == 2: + return True + # Exit code 1 means error + raise subprocess.CalledProcessError(result.returncode, plan_cmd) + + def apps_to_replace(self, destroys: List[str], **kwargs): + plan = self.plan_json(**kwargs) + destroy_plan_apps = [ + rc["address"].split(".")[-1] + for rc in plan.get("resource_changes", []) + if "delete" in rc["change"]["actions"] + and rc.get("type", "") == "juju_application" + ] + assert set(destroys) == set(destroy_plan_apps), ( + f"Expected destroys: {destroys}, got: {destroy_plan_apps}" + ) def apply(self, target: Optional[str] = None, **kwargs): - cmd_str = f"{self.tf_cmd} apply " + self._args_str(target, **kwargs) + extras = self._args_str(target, **kwargs) + cmd_str = f"{self.tf_cmd} apply -auto-approve " + extras subprocess.run(shlex.split(cmd_str), check=True) def destroy(self, **kwargs): - cmd_str = f"{self.tf_cmd} destroy " + self._args_str(None, **kwargs) + extras = self._args_str(**kwargs) + cmd_str = f"{self.tf_cmd} destroy -auto-approve " + extras subprocess.run(shlex.split(cmd_str), check=True) From 95b3a1f888ba7164ed732f8a39815e212f51033e Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Tue, 19 May 2026 13:13:14 -0400 Subject: [PATCH 11/12] chore --- .../tls_external/test_upgrade_cos_tls_external.py | 13 ++++++++++--- .../cos/tls_full/test_upgrade_cos_tls_full.py | 13 ++++++++++--- .../tls_internal/test_upgrade_cos_tls_internal.py | 9 ++++++--- .../cos/tls_none/test_upgrade_cos_tls_none.py | 9 ++++++--- .../test_upgrade_cos_lite_tls_external.py | 9 ++++++--- .../tls_full/test_upgrade_cos_lite_tls_full.py | 7 ++----- .../test_upgrade_cos_lite_tls_internal.py | 9 ++++++--- .../tls_none/test_upgrade_cos_lite_tls_none.py | 9 ++++++--- 8 files changed, 52 insertions(+), 26 deletions(-) diff --git a/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py b/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py index 33b46e17..6e04bb7b 100644 --- a/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py +++ b/tests/integration/cos/tls_external/test_upgrade_cos_tls_external.py @@ -38,10 +38,17 @@ def test_deploy_from_track_2( def test_deploy_to_track_dev( tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju ): - # WHEN upgraded to track dev + jubilant_args = { + "cos_model": cos_model.model, + "ca_model": ca_model.model, + **S3_ENDPOINT, + } + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) - + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) # THEN the model is upgraded and is healthy generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py b/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py index 9a48df79..5a4b7138 100644 --- a/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py +++ b/tests/integration/cos/tls_full/test_upgrade_cos_tls_full.py @@ -38,10 +38,17 @@ def test_deploy_from_track_2( def test_deploy_to_track_dev( tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju ): - # WHEN upgraded to track dev + jubilant_args = { + "cos_model": cos_model.model, + "ca_model": ca_model.model, + **S3_ENDPOINT, + } + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model, **S3_ENDPOINT) - + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) # THEN the model is upgraded and is healthy generic_assertions(cos_model, ca_model, tmp_path) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py b/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py index c0425362..bffcfe65 100644 --- a/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py +++ b/tests/integration/cos/tls_internal/test_upgrade_cos_tls_internal.py @@ -34,10 +34,13 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): - # WHEN upgraded to track dev + jubilant_args = {"cos_model": cos_model.model} + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) - + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) # THEN the model is upgraded and is healthy generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py b/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py index 7f33b146..edffaebf 100644 --- a/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py +++ b/tests/integration/cos/tls_none/test_upgrade_cos_tls_none.py @@ -34,10 +34,13 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): - # WHEN upgraded to track dev + jubilant_args = {"cos_model": cos_model.model} + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(model=cos_model.model, **S3_ENDPOINT) - + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) # THEN the model is upgraded and is healthy generic_assertions(cos_model) no_errors_in_otelcol_logs(cos_model) diff --git a/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py b/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py index 46e5285a..7e8e379f 100644 --- a/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py +++ b/tests/integration/cos_lite/tls_external/test_upgrade_cos_lite_tls_external.py @@ -25,9 +25,12 @@ def test_deploy_from_track_2( def test_deploy_to_track_dev( tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju ): - # WHEN upgraded to track dev + jubilant_args = {"cos_model": cos_model.model, "ca_model": ca_model.model} + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(ca_model=ca_model.model, cos_model=cos_model.model) - + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) # THEN the model is upgraded and is healthy generic_assertions(cos_model, ca_model, tmp_path) diff --git a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py index c4d544ce..b7ece07e 100644 --- a/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py +++ b/tests/integration/cos_lite/tls_full/test_upgrade_cos_lite_tls_full.py @@ -25,15 +25,12 @@ def test_deploy_from_track_2( def test_deploy_to_track_dev( tmp_path, tf_manager, ca_model: jubilant.Juju, cos_model: jubilant.Juju ): - jubilant_args = {"ca_model": ca_model.model, "cos_model": cos_model.model} + jubilant_args = {"cos_model": cos_model.model, "ca_model": ca_model.model} # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) # THEN only expected apps are replaced tf_manager.apps_to_replace(["grafana"], **jubilant_args) # AND WHEN upgraded to track dev tf_manager.apply(**jubilant_args) - # THEN the product passes generic assertions + # THEN the model is upgraded and is healthy generic_assertions(cos_model, ca_model, tmp_path) - # AND a single apply is sufficient (no further changes pending) - # TODO: Maybe this is too much work? - # TODO: assert not tf_manager.plan_has_changes(**jubilant_args) diff --git a/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py b/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py index b5d91db6..c379ff43 100644 --- a/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py +++ b/tests/integration/cos_lite/tls_internal/test_upgrade_cos_lite_tls_internal.py @@ -21,9 +21,12 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): - # WHEN upgraded to track dev + jubilant_args = {"cos_model": cos_model.model} + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(model=cos_model.model) - + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) # THEN the model is upgraded and is healthy generic_assertions(cos_model) diff --git a/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py b/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py index 0d9eae1c..ce2b18a6 100644 --- a/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py +++ b/tests/integration/cos_lite/tls_none/test_upgrade_cos_lite_tls_none.py @@ -21,9 +21,12 @@ def test_deploy_from_track_2(tf_manager, cos_model: jubilant.Juju): def test_deploy_to_track_dev(tf_manager, cos_model: jubilant.Juju): - # WHEN upgraded to track dev + jubilant_args = {"cos_model": cos_model.model} + # WHEN planning to upgrade to track dev tf_manager.init(TRACK_DEV_TF_FILE) - tf_manager.apply(model=cos_model.model) - + # THEN only expected apps are replaced + tf_manager.apps_to_replace(["grafana"], **jubilant_args) + # AND WHEN upgraded to track dev + tf_manager.apply(**jubilant_args) # THEN the model is upgraded and is healthy generic_assertions(cos_model) From 80d10b77f58885f44234bb5a1cb31c151f580beb Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Wed, 20 May 2026 11:55:22 -0400 Subject: [PATCH 12/12] chore --- terraform/cos/locals.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/terraform/cos/locals.tf b/terraform/cos/locals.tf index 526736a5..7310e061 100644 --- a/terraform/cos/locals.tf +++ b/terraform/cos/locals.tf @@ -1,3 +1,4 @@ +# TODO: Remove this when done testing locals { clouds = ["aws", "self-managed"] # list of k8s clouds where this COS module can be deployed. tls_termination = var.external_certificates_offer_url != null ? true : false