Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
53 changes: 53 additions & 0 deletions .github/workflows/integration-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2026 Canonical Ltd.
# See LICENSE file for licensing details.

name: Integration tests

on:
pull_request:
schedule:
- cron: "0 15 * * SAT"
workflow_dispatch:

concurrency:
group: integration-test-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build charm
uses: ./.github/workflows/build.yaml
secrets: inherit

integration-tests:
name: Integration tests
needs: build
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Download charm artifact
uses: actions/download-artifact@v4
with:
pattern: ${{ needs.build.outputs.artifact-prefix }}-*--platform-ubuntu@24.04-amd64
merge-multiple: true

- name: Set up LXD and Juju
uses: charmed-kubernetes/actions-operator@main
with:
provider: lxd
juju-channel: 3/stable

- name: Enable LXD container internet access
run: |
sudo sysctl -w net.ipv4.ip_forward=1
IFACE=$(ip route | grep default | awk '{print $5}' | head -1)
sudo iptables -t nat -A POSTROUTING -o "$IFACE" -j MASQUERADE

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Run integration tests
run: uv run --group integration pytest -v --tb native tests/integration
2 changes: 1 addition & 1 deletion tests/integration/bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ applications:
enable_hostagent_messenger: true
enable_ubuntu_installer_attach: true
root_url: https://landscape.local/
redirect_https: none
redirect_https: default
haproxy:
charm: ch:haproxy
channel: 2.8/edge
Expand Down
135 changes: 34 additions & 101 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,16 @@

import os
import pathlib
import uuid
import tempfile

import jubilant
import pytest

from tests.integration.helpers import has_haproxy_route_provider

BUNDLE_NAME = "bundle.yaml"
"""
The name of the bundle used for integration testing.
"""


WAIT_TIMEOUT_SECONDS = 60 * 20 # Landscape takes a long time to deploy.


USE_HOST_JUJU_MODEL = os.getenv("LANDSCAPE_CHARM_USE_HOST_JUJU_MODEL", False)
"""
If `True`, return a reference the current Juju model on the host instead of a temporary
Expand Down Expand Up @@ -100,120 +94,59 @@ def bundle(juju: jubilant.Juju) -> None:

def bundle_path() -> pathlib.Path:
"""
Return the full absolute path to the landscape-server integration test bundle.
Return the path to the landscape-server integration test bundle, with the
local charm path rewritten to an absolute path.

Juju copies the bundle YAML into its own snap temp directory before parsing,
so relative charm paths are resolved from there rather than from the original
bundle location. Writing an absolute path avoids this.
"""
path = pathlib.Path(__file__).parent / BUNDLE_NAME
assert path.exists(), f"{path} not found."
return path
src = pathlib.Path(__file__).parent / BUNDLE_NAME
assert src.exists(), f"{src} not found."

content = src.read_text()
for line in content.splitlines():
stripped = line.strip()
if stripped.startswith("charm:"):
charm_val = stripped[len("charm:") :].strip().strip('"').strip("'")
if charm_val and not charm_val.startswith(("ch:", "local:")):
abs_path = (src.parent / charm_val).resolve()
content = content.replace(charm_val, str(abs_path))

tmp = pathlib.Path(tempfile.mkstemp(suffix=".yaml")[1])
tmp.write_text(content)
return tmp


@pytest.fixture(scope="module")
def lbaas(juju: jubilant.Juju):
def lbaas(juju: jubilant.Juju, bundle: None):
"""
Set up external HAProxy in a separate model for LBaaS testing.

This fixture can either:
- Return the existing juju model if USE_HOST_JUJU_MODEL is True
(haproxy already local)
- Use an existing lbaas model (if USE_HOST_LBAAS_MODEL is True)
- Create a temporary model and deploy haproxy + self-signed-certificates
Provide a reference to the HAProxy model for tests that need it.

Environment variables:
- LANDSCAPE_CHARM_USE_HOST_JUJU_MODEL: Return local model directly
(haproxy co-deployed)
- LANDSCAPE_CHARM_USE_HOST_LBAAS_MODEL: Set to use existing lbaas deployment
- LBAAS_MODEL_NAME: Name of the lbaas model (default: "lbaas")
- LANDSCAPE_CHARM_USE_HOST_JUJU_MODEL: Yield local model directly when haproxy
is co-deployed.
- LANDSCAPE_CHARM_USE_HOST_LBAAS_MODEL: Use an existing separate lbaas model.
- LBAAS_MODEL_NAME: Name of the lbaas model (default: "lbaas").

Yields None when no separate lbaas model is configured; tests that require a
distinct lbaas model skip themselves via their own `lbaas is None` guards.
"""
if (
USE_HOST_JUJU_MODEL
and not USE_HOST_LBAAS_MODEL
and "haproxy" in juju.status().apps
):
haproxy_in_local_model = "haproxy" in juju.status().apps
if USE_HOST_JUJU_MODEL and not USE_HOST_LBAAS_MODEL and haproxy_in_local_model:
yield juju
return

status = juju.status()
app_status = status.apps.get("landscape-server")

if not app_status or not has_haproxy_route_provider(juju, "landscape-server"):
pytest.skip("HAProxy route not configured, skipping...")

if USE_HOST_LBAAS_MODEL:
lbaas_model = LBAAS_MODEL_NAME
lbaas_juju = jubilant.Juju(model=lbaas_model)

try:
lbaas_status = lbaas_juju.status()
assert "haproxy" in lbaas_status.apps, "haproxy not found in lbaas model"
except Exception as e:
pytest.fail(
f"Failed to connect to existing lbaas model '{lbaas_model}': {e}"
)

yield lbaas_juju
else:
lbaas_model = str(uuid.uuid4())

juju.add_model(lbaas_model)
lbaas_juju = jubilant.Juju(model=lbaas_model)

try:
lbaas_juju.deploy("haproxy", channel="2.8/edge")
lbaas_juju.config(
"haproxy",
values={"external-hostname": "landscape.local", "enable-hsts": "false"},
)
lbaas_juju.deploy("self-signed-certificates", channel="1/stable")
lbaas_juju.wait(jubilant.all_active, timeout=600)

lbaas_juju.integrate(
"haproxy:certificates", "self-signed-certificates:certificates"
)
lbaas_juju.integrate(
"haproxy:receive-ca-certs", "self-signed-certificates:send-ca-cert"
)
lbaas_juju.wait(jubilant.all_active, timeout=300)

lbaas_juju.offer("haproxy", endpoint="haproxy-route")

offer_app_name = "lbaas-haproxy"
juju.consume(f"admin/{lbaas_model}.haproxy", offer_app_name)

juju.integrate(
f"{offer_app_name}:haproxy-route",
"landscape-server:appserver-haproxy-route",
)
juju.wait(
lambda status: has_haproxy_route_provider(
juju, "appserver-haproxy-route"
),
timeout=300,
)

juju.integrate(
f"{offer_app_name}:haproxy-route",
"landscape-server:hostagent-messenger-haproxy-route",
)
juju.wait(
lambda status: has_haproxy_route_provider(
juju, "hostagent-messenger-haproxy-route"
),
timeout=300,
)

juju.integrate(
f"{offer_app_name}:haproxy-route",
"landscape-server:ubuntu-installer-attach-haproxy-route",
)
juju.wait(
lambda status: has_haproxy_route_provider(
juju, "ubuntu-installer-attach-haproxy-route"
),
timeout=300,
)

juju.wait(jubilant.all_active, timeout=600)

yield lbaas_juju
finally:
juju.destroy_model(lbaas_model, destroy_storage=True, force=True)
yield None
Loading