From fcb9d6151e1c6889011db71b5f3b5f4cd33fcf0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:48:18 +0000 Subject: [PATCH 1/6] chore: bump minio from 7.2.18 to 7.2.20 Bumps [minio](https://github.com/minio/minio-py) from 7.2.18 to 7.2.20. - [Release notes](https://github.com/minio/minio-py/releases) - [Commits](https://github.com/minio/minio-py/compare/7.2.18...7.2.20) --- updated-dependencies: - dependency-name: minio dependency-version: 7.2.20 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- uv.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index a2f79bc5e..ba1505dc9 100644 --- a/uv.lock +++ b/uv.lock @@ -453,7 +453,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -700,7 +700,7 @@ wheels = [ [[package]] name = "minio" -version = "7.2.18" +version = "7.2.20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argon2-cffi" }, @@ -709,9 +709,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/99/1ad8733fa3f2fa82726e470f8c321e17f9321083b234ab45ad6b59d80d9f/minio-7.2.18.tar.gz", hash = "sha256:173402a5716099159c5659f9de75be204ebe248557b9f1cc9cf45aa70e9d3024", size = 135544, upload-time = "2025-09-29T17:00:28.238Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl", hash = "sha256:f23a6edbff8d0bc4b5c1a61b2628a01c5a3342aefc613ff9c276012e6321108f", size = 93120, upload-time = "2025-09-29T17:00:26.86Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" }, ] [[package]] From 54d5d5a03acfe887c9389e51fa95b2a4c292d1f5 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Thu, 2 Jul 2026 21:24:30 +1200 Subject: [PATCH 2/6] mBump jubilant --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 63522bb9a..b00c8edec 100644 --- a/uv.lock +++ b/uv.lock @@ -561,14 +561,14 @@ wheels = [ [[package]] name = "jubilant" -version = "1.5.0" +version = "1.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/af/2c7d2a677389eb4de3bb841f399b749ca2fd4c6c1b70313e21249536e6be/jubilant-1.5.0.tar.gz", hash = "sha256:055c65a662586191939a1a3d3e2b6d08b71ecf0c7f403a1c7ba0cde6ecaf3bbd", size = 28433, upload-time = "2025-10-10T01:08:06.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/0a/4112c6a16440efa7047b58ea7cd3751a75ef4b73e75fa4b4b4aadf7cbf1c/jubilant-1.11.0.tar.gz", hash = "sha256:69d13d2a0c9604a146fb7172732b9344f7aa14a35cdc29a37189831a724dc80a", size = 34642, upload-time = "2026-06-29T04:23:29.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/c9/05e5aa65baa7b71b17c1314b8f25744f36fd9aad60ef9fb46fd84347885a/jubilant-1.5.0-py3-none-any.whl", hash = "sha256:eec58340b9d3d478f31e18e33281638fdf0d9b84608c8935368f7a1bb1972255", size = 28275, upload-time = "2025-10-10T01:08:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/8ba367bc63f03b9f0a31c7975fec8e7f3c9da3516a8d1db668164fc1801b/jubilant-1.11.0-py3-none-any.whl", hash = "sha256:900427d616c75f1cfb6ee11212fefe9c7877111209630a87b2b2556a37d626e7", size = 36640, upload-time = "2026-06-29T04:23:28.667Z" }, ] [[package]] From 905cc786d19ba3039455727771eb6a671e2d271e Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Fri, 3 Jul 2026 17:22:50 +1200 Subject: [PATCH 3/6] ci(integration): Let posargs override the default test path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The integration workflow's matrix passes a file path (e.g. test/integration/test_secrets.py) as posargs to tox. Because tox.ini hardcoded test/integration/ before {posargs}, pytest received both paths and collected the whole suite — so every matrix leg ran every test, and test_tracing's function-scoped tracing_juju fixture failed on the second tempo deploy into the shared module-scoped juju model ("application "tempo" already exists"). Move the default target into {posargs:...} so 'tox -e integration' alone still runs the full suite, but posargs replace the target when supplied. Co-Authored-By: Claude Opus 4.7 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d046fd315..783ea243e 100644 --- a/tox.ini +++ b/tox.ini @@ -193,7 +193,7 @@ commands = [testenv:integration] description = Run a suite of integration tests. dependency_groups = integration -commands = pytest -vvv --tb native {toxinidir}/test/integration/ {posargs} +commands = pytest -vvv --tb native {posargs:{toxinidir}/test/integration/} [testenv:draft-release] description = Make a draft release From 982b2d9d605c016a1e17a532eaf4796f5ddb0a82 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 5 Jul 2026 16:23:11 +1200 Subject: [PATCH 4/6] ci: give each tracing integration test its own model The tracing integration tests were re-enabled with a matrix entry per test file, but the run that validated them used an entry per test function, so each test ran on its own runner with a fresh model. With both tests in one pytest invocation, the function-scoped tracing_juju fixture redeployed the tempo stack into the module-scoped model on the second test and failed with "application already exists". Have tracing_juju create its own temporary model instead of reusing the module-scoped one: both tests deploy the same charm under test and test_with_tls reconfigures tempo (enabling TLS), so a model cannot be shared between them. Extend the job timeout for the tracing file to cover the second stack deployment. Co-Authored-By: Claude Fable 5 --- .github/workflows/integration.yaml | 5 ++++- test/integration/conftest.py | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index e9c137a14..313e5ac21 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -67,4 +67,7 @@ jobs: - run: tox -e integration -- --log-cli-level=INFO -s "$MATRIX_TEST" env: MATRIX_TEST: ${{ matrix.test }} - timeout-minutes: 20 # Juju 4 gets stuck while destroying the model + # A backstop for Juju 4 getting stuck while destroying the model. The + # tracing tests each deploy the tempo stack into their own fresh + # model, so that file needs longer than the others. + timeout-minutes: ${{ matrix.test == 'test/integration/test_tracing.py' && 40 || 20 }} diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 53dccdf92..7bce06aff 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -111,8 +111,23 @@ def _xfail_on_k8s_juju4(juju: jubilant.Juju, reason: str) -> None: @pytest.fixture -def tracing_juju(juju: jubilant.Juju) -> Generator[jubilant.Juju]: - """Make a Juju model with the tracing part of COS ready.""" +def tracing_juju() -> Generator[jubilant.Juju]: + """Make a fresh Juju model with the tracing part of COS ready. + + Unlike the module-scoped `juju` fixture, this makes a new model for each + test: the tracing tests each deploy the same charm under test and + reconfigure the tempo stack (for example, enabling TLS), so a model cannot + be shared between them. + """ + with jubilant.temp_model() as juju: + juju.wait_timeout = 900 + _deploy_tracing_stack(juju) + yield juju + print(juju.debug_log()) + + +def _deploy_tracing_stack(juju: jubilant.Juju) -> None: + """Deploy tempo, its worker, and the minio/s3 storage it requires.""" deploy_tempo(juju) deploy_tempo_worker(juju) minio_config = {'access-key': 'accesskey', 'secret-key': 'mysoverysecretkey'} @@ -182,8 +197,6 @@ def tracing_juju(juju: jubilant.Juju) -> Generator[jubilant.Juju]: juju.wait(jubilant.all_active) - yield juju - @pytest.fixture(scope='session') def tracing_charm_dir(pytestconfig: pytest.Config) -> Generator[pathlib.Path]: From 57163b5115f789e9b180eabbcaa1687e00aec28f Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 5 Jul 2026 16:55:00 +1200 Subject: [PATCH 5/6] test: make tracing and relation integration tests robust to slow settling The first run with per-test tracing models surfaced two timing races: test_with_tls got zero spans on Juju 3: after TLS is enabled, tempo's ingester ring takes minutes to re-form and the collector rejects spans with 503 "empty ring" until it does. The charm buffers rejected data and only retries on a later dispatch, but wait_spans gave up after 60 seconds. Raise that test's span timeout to 600 seconds and set a one minute update-status interval on the tracing model so retry dispatches arrive promptly. test_relation_units hit KeyError: 'peer' on machine Juju 4.1: relation membership propagates to each unit asynchronously, even after all units report active, so the action can observe a relation with no remote units yet (the endpoint is then absent from the results). Poll the action until the expected unit sets appear, with a five minute deadline. Co-Authored-By: Claude Fable 5 --- test/integration/conftest.py | 4 ++++ test/integration/test_relation.py | 34 +++++++++++++++++++++---------- test/integration/test_tracing.py | 9 +++++++- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 7bce06aff..63b7a320f 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -121,6 +121,10 @@ def tracing_juju() -> Generator[jubilant.Juju]: """ with jubilant.temp_model() as juju: juju.wait_timeout = 900 + # The charm under test only retries sending buffered trace data on a + # later dispatch, so make update-status fire frequently to bound how + # long the tests wait for spans to arrive. + juju.cli('model-config', 'update-status-hook-interval=1m') _deploy_tracing_stack(juju) yield juju print(juju.debug_log()) diff --git a/test/integration/test_relation.py b/test/integration/test_relation.py index d5754bc93..23c5d5184 100644 --- a/test/integration/test_relation.py +++ b/test/integration/test_relation.py @@ -15,6 +15,7 @@ from __future__ import annotations import json +import time from collections.abc import Callable import jubilant @@ -58,15 +59,26 @@ def test_relation_units(build_relation_charm: Callable[[], str], juju: jubilant. peer_units.remove(f'{charm_name}/0') db_units = set(status.get_units(db)) ingress_units = set(status.get_units(ingress)) - task = juju.run(f'{charm_name}/0', 'get-units') - assert task.success, task.message - ops_units = json.loads(task.results['units']) - # The keys in the action results are the endpoint names, and the values are - # the unit names received from Juju. - assert all(unit.startswith('test-db/') for unit in ops_units['db']) - assert all(unit.startswith('test-ingress/') for unit in ops_units['ingress']) - assert all(unit.startswith('test-relation/') for unit in ops_units['peer']) - assert set(ops_units['db']) == db_units - assert set(ops_units['ingress']) == ingress_units - assert set(ops_units['peer']) == peer_units + # Relation membership propagates to each unit asynchronously, even after + # all units report active, so retry until Juju has caught up. + deadline = time.time() + 300 + while True: + task = juju.run(f'{charm_name}/0', 'get-units') + assert task.success, task.message + # The keys in the action results are the endpoint names, and the + # values are the unit names received from Juju. An endpoint whose + # relation has no remote units yet is absent entirely. + ops_units: dict[str, list[str]] = json.loads(task.results['units']) + complete = ( + set(ops_units.get('db', [])) == db_units + and set(ops_units.get('ingress', [])) == ingress_units + and set(ops_units.get('peer', [])) == peer_units + ) + if complete or time.time() > deadline: + break + time.sleep(10) + + assert set(ops_units.get('db', [])) == db_units + assert set(ops_units.get('ingress', [])) == ingress_units + assert set(ops_units.get('peer', [])) == peer_units diff --git a/test/integration/test_tracing.py b/test/integration/test_tracing.py index b55537791..a1880d529 100644 --- a/test/integration/test_tracing.py +++ b/test/integration/test_tracing.py @@ -77,8 +77,15 @@ def test_with_tls(build_tracing_charm: Callable[[], str], tracing_juju: jubilant # Query the tempo coordinator: it terminates TLS (self-signed-certificates # is related to tempo, not tempo-worker), so the worker's 3200 still # speaks plain HTTP and causes a TLS handshake failure on a TLS client. + # + # After TLS is enabled, tempo's ingester ring can take minutes to re-form, + # and until it does the collector rejects spans ("empty ring"). The charm + # buffers rejected spans and only retries on a later dispatch, such as + # update-status, so allow enough time for a retry to land. with kubectl_port_forward(tracing_juju.model, 'svc/tempo', 3200) as endpoint: - spans = wait_spans(endpoint, ready=lambda spans: 'ops.main' in str(spans), https=True) + spans = wait_spans( + endpoint, ready=lambda spans: 'ops.main' in str(spans), https=True, timeout=600 + ) assert 'ops.main' in [span['name'] for span in spans] event_names = [event['name'] for event in get_events(spans)] From e2b4cde2f83f4a8f9f826be223637b0eba02ba20 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Sun, 5 Jul 2026 23:30:55 +1200 Subject: [PATCH 6/6] test: do not require StartEvent spans in the TLS tracing test The previous run showed test_with_tls receiving spans over TLS (the ops.main assertion passed), but the start dispatch's trace was missing while both earlier and later dispatches were present. That is expected: the exporter persists a batch to the buffer only when the export thread picks it up, and ops bounds the end-of-dispatch flush wait, so batches queued while the collector rejects requests ("empty ring", while tempo's ingester ring re-forms after enabling TLS) can be dropped. Asserting StartEvent here demands a guarantee ops-tracing does not make. Receiving ops.main spans over HTTPS is what the test is about; test_direct_connection still checks StartEvent against a healthy collector. Co-Authored-By: Claude Fable 5 --- test/integration/test_tracing.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/integration/test_tracing.py b/test/integration/test_tracing.py index a1880d529..9707fb988 100644 --- a/test/integration/test_tracing.py +++ b/test/integration/test_tracing.py @@ -88,8 +88,12 @@ def test_with_tls(build_tracing_charm: Callable[[], str], tracing_juju: jubilant ) assert 'ops.main' in [span['name'] for span in spans] - event_names = [event['name'] for event in get_events(spans)] - assert 'StartEvent' in event_names + # Unlike test_direct_connection, don't assert on events from specific + # early dispatches (such as StartEvent): the exporter only waits a bounded + # time at the end of each dispatch, so trace data produced while the + # collector is rejecting requests ("empty ring") can be legitimately + # dropped. Receiving any ops.main span over HTTPS is what this test is + # about. def wait_spans(