Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ charms:
resources: {}
platforms:
- arch: amd64
- arch: arm64
runner: [ubuntu-24.04-arm]
channel: latest/edge
- name: cloudflare-configurator
charmcraft-yaml: cloudflare-configurator-operator/charmcraft.yaml
resources: {}
platforms:
- arch: amd64
- arch: arm64
runner: [ubuntu-24.04-arm]
channel: latest/edge
snaps:
- name: charmed-cloudflared
Expand Down
1 change: 1 addition & 0 deletions cloudflare-configurator-operator/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ base: ubuntu@24.04
build-base: ubuntu@24.04
platforms:
amd64:
arm64:
parts:
charm:
source: .
Expand Down
1 change: 1 addition & 0 deletions cloudflared-operator/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ base: ubuntu@24.04
build-base: ubuntu@24.04
platforms:
amd64:
arm64:
parts:
charm:
source: .
Expand Down
16 changes: 15 additions & 1 deletion cloudflared-operator/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
logger = logging.getLogger(__name__)

CLOUDFLARED_ROUTE_INTEGRATION_NAME = "cloudflared-route"
JUJU_INFO_INTEGRATION_NAME = "juju-info"
# this is not a hardcoded password
TUNNEL_TOKEN_CONFIG_NAME = "tunnel-token" # nosec
CHARMED_CLOUDFLARED_SNAP_NAME = "charmed-cloudflared"
Expand Down Expand Up @@ -59,11 +60,12 @@ def __init__(self, *args: typing.Any):
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.config_changed, self._reconcile)
self.framework.observe(self.on.secret_changed, self._reconcile)
self.framework.observe(self.on.stop, self._on_stop)
self.framework.observe(self.on["cloudflared-route"].relation_changed, self._reconcile)
self.framework.observe(self.on["cloudflared-route"].relation_departed, self._reconcile)
self.framework.observe(self.on["juju-info"].relation_changed, self._reconcile)
self.framework.observe(self.on["juju-info"].relation_departed, self._reconcile)
self.framework.observe(self.on.stop, self._on_stop)
self.framework.observe(self.on["juju-info"].relation_broken, self._on_stop)
self._snap_client = snap.SnapClient()
self._cloudflared_route = CloudflaredRouteRequirer(self)
self._grafana_agent = COSAgentProvider(
Expand All @@ -86,6 +88,16 @@ def _on_stop(self, _: ops.EventBase) -> None:
for instance in self._get_installed_cloudflared_snaps():
snap.remove(instance)

def _has_principal(self) -> bool:
"""Check whether the charm is still related to a principal via juju-info.

Returns:
True if an active juju-info relation exists.
"""
return any(
relation.active for relation in self.model.relations[JUJU_INFO_INTEGRATION_NAME]
)

def _reconcile(self, _: ops.EventBase) -> None:
"""Handle changed configuration."""
try:
Expand All @@ -97,6 +109,8 @@ def _reconcile(self, _: ops.EventBase) -> None:
self.unit.status = ops.BlockedStatus(str(exc))
return
required_snap_instances = set(metrics_ports.keys())
if not self._has_principal():
required_snap_instances = set()
if not required_snap_instances:
self.unit.status = ops.WaitingStatus("waiting for tunnel token")
return
Expand Down
6 changes: 4 additions & 2 deletions cloudflared-operator/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ def cloudflared_route_provider_1_fixture(juju: jubilant.Juju, cloudflared_charm:
"any-charm",
app=CLOUDFLARED_ROUTE_PROVIDER_1,
config={"src-overwrite": SRC_OVERWRITE},
channel="latest/edge",
channel="latest/beta",
base="ubuntu@24.04",
)
juju.integrate(f"{cloudflared_charm}:cloudflared-route", CLOUDFLARED_ROUTE_PROVIDER_1)
return CLOUDFLARED_ROUTE_PROVIDER_1
Expand All @@ -232,7 +233,8 @@ def cloudflared_route_provider_2_fixture(juju: jubilant.Juju, cloudflared_charm:
"any-charm",
app=CLOUDFLARED_ROUTE_PROVIDER_2,
config={"src-overwrite": SRC_OVERWRITE},
channel="latest/edge",
channel="latest/beta",
base="ubuntu@24.04",
)
juju.integrate(f"{cloudflared_charm}:cloudflared-route", CLOUDFLARED_ROUTE_PROVIDER_2)
return CLOUDFLARED_ROUTE_PROVIDER_2
Expand Down
49 changes: 35 additions & 14 deletions cloudflared-operator/tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

"""Integration tests."""

import contextlib
import json
import logging
import subprocess # nosec
import time

import jubilant
Expand Down Expand Up @@ -37,15 +37,33 @@ def wait_for_tunnel_healthy(cloudflare_api, tunnel_token):


def reboot_application(juju: jubilant.Juju, app: str) -> None:
"""Reboot all units of an application (required for deploying in LXD containers).
"""Reboot the LXD containers hosting an application.

Args:
juju: Jubilant juju instance.
app: Application name.
"""
# Rebooting terminates the exec session, which is expected.
with contextlib.suppress(jubilant.CLIError):
juju.cli("exec", "--application", app, "--", "sudo", "reboot")
status = json.loads(juju.cli("status", "--format", "json"))
machines = status.get("machines", {})
applications = status.get("applications", {})

machine_ids: set[str] = set()
for unit in applications.get(app, {}).get("units", {}).values():
if "machine" in unit:
machine_ids.add(unit["machine"])
if not machine_ids:
for principal in applications.values():
for unit in principal.get("units", {}).values():
subordinates = unit.get("subordinates", {})
if any(sub.split("/")[0] == app for sub in subordinates) and "machine" in unit:
machine_ids.add(unit["machine"])

for machine in machine_ids:
container = machines.get(machine, {}).get("instance-id")
if container is None:
continue
logger.info("restarting LXD container %s for %s", container, app)
subprocess.run(["lxc", "restart", container], check=True) # nosec


def test_tunnel_token_config(juju, cloudflare_api, cloudflared_charm):
Expand All @@ -54,9 +72,9 @@ def test_tunnel_token_config(juju, cloudflare_api, cloudflared_charm):
act: provide the tunnel-token charm config.
assume: cloudflared tunnels provided in the charm config is up and healthy
"""
base_app = "chrony"
juju.deploy(base_app, channel="latest/edge", config={"sources": "ntp://ntp.ubuntu.com"})
juju.integrate(base_app, cloudflared_charm)
base_app = "any-charm"
juju.deploy("any-charm", app=base_app, channel="latest/beta", base="ubuntu@24.04")
juju.integrate(f"{base_app}:juju-info", f"{cloudflared_charm}:juju-info")
tunnel_token = cloudflare_api.create_tunnel_token()
secret_uri = juju.add_secret("test-tunnel-token", {"tunnel-token": tunnel_token})
juju.grant_secret("test-tunnel-token", cloudflared_charm)
Expand Down Expand Up @@ -110,7 +128,7 @@ def test_update_snap_channel(juju, cloudflared_charm):
"""
juju.config(cloudflared_charm, {"charmed-cloudflared-snap-channel": "latest/edge"})
juju.wait(jubilant.all_agents_idle, error=jubilant.any_error)
snap_list = juju.cli("exec", "--unit", "chrony/0", "--", "snap", "list")
snap_list = juju.cli("exec", "--unit", "any-charm/0", "--", "snap", "list")
assert "charmed-cloudflared_" in snap_list
for line in snap_list.splitlines():
if "charmed-cloudflared_" in line:
Expand Down Expand Up @@ -160,20 +178,23 @@ def test_remove(juju, cloudflared_charm):
act: remove the cloudflared charm.
assume: cloudflared charm should uninstall all charmed-cloudflared snap instances.
"""
snap_list = juju.cli("exec", "--unit", "chrony/0", "--", "snap", "list")
snap_list = juju.cli("exec", "--unit", "any-charm/0", "--", "snap", "list")
assert "charmed-cloudflared_" in snap_list
logger.info("snap list before removal: %s", snap_list)
juju.remove_relation(cloudflared_charm, "chrony")
juju.wait(lambda status: not status.apps[cloudflared_charm].units)
juju.remove_relation(f"{cloudflared_charm}:juju-info", "any-charm:juju-info")
juju.wait(
lambda status: not status.apps[cloudflared_charm].units
and "juju-info" not in status.apps[cloudflared_charm].relations
)
deadline = time.time() + 300
while True:
snap_list = juju.cli("exec", "--unit", "chrony/0", "--", "snap", "list")
snap_list = juju.cli("exec", "--unit", "any-charm/0", "--", "snap", "list")
if "charmed-cloudflared_" not in snap_list or time.time() > deadline:
break
time.sleep(5)
assert "charmed-cloudflared_" not in snap_list
logger.info("snap list after removal: %s", snap_list)
juju.integrate("chrony", cloudflared_charm)
juju.integrate("any-charm:juju-info", f"{cloudflared_charm}:juju-info")


def test_secret_config_permission(
Expand Down
13 changes: 13 additions & 0 deletions concierge-juju4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

juju:
channel: 4.0/stable
model-defaults:
test-mode: "true"
automatically-retry-hooks: "false"

providers:
lxd:
enable: true
bootstrap: true
17 changes: 14 additions & 3 deletions spread.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ backends:
type: integration-test
systems:
- ubuntu-24.04:
- ubuntu-24.04-arm64:
runner: [ubuntu-24.04-arm]
arch: arm64
environment:
OPCLI_GIT_REF: '$(HOST: echo "${OPCLI_GIT_REF:-main}")'
CONCIERGE: concierge.yaml
CONCIERGE/juju3: '$(HOST: echo "${CONCIERGE:-concierge.yaml}")'
CONCIERGE/juju4: concierge-juju4.yaml
exclude:
- .git
- .tox
Expand All @@ -21,10 +25,13 @@ integration-suites:
cloudflared-operator/tests/integration/:
summary: cloudflared charm integration tests
working-dir: cloudflared-operator/
auto-discover: false
backends:
- integration-test
systems: [ubuntu-24.04]
systems: [ubuntu-24.04, ubuntu-24.04-arm64]
environment:
MODULE/juju3: tests/integration/test_charm.py
MODULE/juju4: tests/integration/test_charm.py
CLOUDFLARE_ACCOUNT_ID: '$(HOST: echo "${CLOUDFLARE_ACCOUNT_ID:-}")'
CLOUDFLARE_API_TOKEN: '$(HOST: echo "${CLOUDFLARE_API_TOKEN:-}")'
pytest-environment-template: |
Expand All @@ -36,7 +43,11 @@ integration-suites:
cloudflare-configurator-operator/tests/integration/:
summary: cloudflare-configurator charm integration tests
working-dir: cloudflare-configurator-operator/
auto-discover: false
backends:
- integration-test
systems: [ubuntu-24.04]
systems: [ubuntu-24.04, ubuntu-24.04-arm64]
environment:
MODULE/juju3: tests/integration/test_charm.py
MODULE/juju4: tests/integration/test_charm.py
pytest-arguments-template: *default-pytest-args
2 changes: 0 additions & 2 deletions tests/__init__.py

This file was deleted.

2 changes: 0 additions & 2 deletions tests/integration/__init__.py

This file was deleted.

Loading