You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
8.8 KiB
227 lines
8.8 KiB
import textwrap
|
|
import test_quadlet # noqa: F401
|
|
import pytest
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
@pytest.fixture(scope="module")
|
|
def dns_names() -> list[str]:
|
|
"""List of DNS names to be resolved by the VM (e.g. for ACME challenges)."""
|
|
return [ "secure", "ping" ]
|
|
|
|
# Extra files to inject into the FCOS image for the tests in this file.
|
|
@pytest.fixture(scope="module")
|
|
def fcos_extra_files(pebble_acme_server, top_level_domain) -> dict:
|
|
"""
|
|
Extra files to inject into the FCOS VM image.
|
|
"""
|
|
|
|
files = {
|
|
# Exposes the Traefik ping endpoint to localhost.
|
|
"/etc/quadlets/traefik/conf.d/ping.yaml": (
|
|
textwrap.dedent(f"""\
|
|
http:
|
|
routers:
|
|
traefik-ping:
|
|
rule: Host(`ping`) || Host(`ping.{top_level_domain}`)
|
|
entryPoints:
|
|
- http
|
|
service: "ping@internal"
|
|
middlewares:
|
|
- localhost-only
|
|
middlewares:
|
|
localhost-only:
|
|
ipAllowList:
|
|
sourceRange:
|
|
- "127.0.0.1/32"
|
|
"""),
|
|
10001,
|
|
10000,
|
|
0o644,
|
|
),
|
|
# Exposes the ping endpoint to the outside world over HTTPS only
|
|
"/etc/quadlets/traefik/conf.d/secure.yaml": (
|
|
textwrap.dedent(f"""\
|
|
http:
|
|
routers:
|
|
secure:
|
|
rule: Host(`secure.{top_level_domain}`)
|
|
entryPoints:
|
|
- https
|
|
service: "ping@internal"
|
|
tls:
|
|
certResolver: le
|
|
"""),
|
|
10001,
|
|
10000,
|
|
0o644,
|
|
),
|
|
"/etc/quadlets/traefik/pebble.pem": (
|
|
pebble_acme_server['ca_cert'],
|
|
10001,
|
|
10000,
|
|
0o644,
|
|
),
|
|
"/etc/quadlets/traefik/traefik.yaml": (
|
|
textwrap.dedent(f"""\
|
|
api:
|
|
dashboard: false
|
|
debug: false
|
|
ping:
|
|
manualRouting: true
|
|
|
|
log:
|
|
level: "INFO"
|
|
|
|
accesslog: false
|
|
|
|
global:
|
|
sendanonymoususage: false
|
|
checknewversion: false
|
|
|
|
entryPoints:
|
|
http:
|
|
address: ":80"
|
|
https:
|
|
address: ":443"
|
|
|
|
certificatesResolvers:
|
|
le:
|
|
acme:
|
|
email: "traefik@pytest.example.test"
|
|
caServer: "{pebble_acme_server['directory_url']}"
|
|
caCertificates: "/etc/traefik/pebble.pem"
|
|
keyType: "EC384"
|
|
httpChallenge:
|
|
entryPoint: http
|
|
storage: "/var/lib/traefik/acme.json"
|
|
|
|
providers:
|
|
file:
|
|
directory: /etc/traefik/conf.d/
|
|
watch: true
|
|
"""),
|
|
10001,
|
|
10000,
|
|
0o644,
|
|
),
|
|
}
|
|
|
|
return files
|
|
|
|
"""
|
|
Verify that the Traefik Quadlet is correctly installed and configured on a fresh VM boot.
|
|
"""
|
|
class TestTraefikQuadlet(test_quadlet.TestQuadlet):
|
|
expected_services = [
|
|
{ "name": "traefik.target", "state": "active", "exists": True },
|
|
{ "name": "traefik.service", "state": "active", "exists": True },
|
|
]
|
|
|
|
expected_sockets = [
|
|
{ "uri": "tcp://127.0.0.1:80", "state": "listening" },
|
|
{ "uri": "tcp://127.0.0.1:443", "state": "listening" },
|
|
]
|
|
|
|
expected_ports = [
|
|
{ "number": 80, "protocol": "tcp", "state": "open" },
|
|
{ "number": 443, "protocol": "tcp", "state": "open" },
|
|
{ "number": 22, "protocol": "tcp", "state": "open" },
|
|
]
|
|
|
|
expected_files = [
|
|
{ "path": "/var/lib/quadlets/traefik", "type": "directory", "owner": "traefik", "group": "itix-svc", "mode": 0o755 },
|
|
{ "path": "/etc/quadlets/traefik", "type": "directory", "owner": "traefik", "group": "itix-svc", "mode": 0o755 },
|
|
{ "path": "/etc/quadlets/traefik/traefik.yaml", "type": "file", "owner": "traefik", "group": "itix-svc", "mode": 0o644 },
|
|
]
|
|
|
|
expected_podman_images = [
|
|
{ "name": "docker.io/library/traefik", "tag": "v3.4", "state": "present" },
|
|
]
|
|
|
|
expected_podman_containers = [
|
|
{ "name": "traefik", "state": "present", "pid1": { "owner": "10001", "group": "10000" } },
|
|
]
|
|
|
|
expected_main_service = "traefik.target"
|
|
expected_main_service_timeout = 300
|
|
|
|
def test_clean_traefik_state(self, fcos_host, keep):
|
|
if keep:
|
|
# Stop the traefik.target to ensure a clean state for the tests, but only if --keep is set because otherwise the VM is not reused across runs and is already in a clean state.
|
|
result = fcos_host.run("systemctl stop traefik.target")
|
|
assert result.rc == 0, f"Failed to stop traefik.target: {result.stderr}"
|
|
fcos_host.run("rm -rf /var/lib/quadlets/traefik/acme.json")
|
|
result = fcos_host.run("systemctl start traefik.target")
|
|
assert result.rc == 0, f"Failed to start traefik.target: {result.stderr}"
|
|
else:
|
|
pytest.skip("Skipping clean Traefik state test because --keep is not set.")
|
|
|
|
@pytest.mark.flaky(reruns=6, reruns_delay=5)
|
|
def test_traefik_ping_localhost(self, fcos_host):
|
|
"""Traefik must respond to the ping endpoint with HTTP 200."""
|
|
result = fcos_host.run("curl -sSf -o /dev/null -w '%{http_code}' -H 'Host: ping' http://127.0.0.1/")
|
|
assert result.rc == 0, f"curl failed with exit code {result.rc}: {result.stderr}"
|
|
assert result.stdout.strip() == "200", f"Expected HTTP 200 from ping endpoint, got: {result.stdout.strip()}"
|
|
|
|
def test_traefik_ping_external(self, fcos_vm, top_level_domain):
|
|
"""Traefik must NOT respond to the ping endpoint outside localhost."""
|
|
result = subprocess.run(
|
|
[
|
|
"curl",
|
|
"-sSf",
|
|
"-o", "/dev/null",
|
|
"--resolve", f"ping.{top_level_domain}:80:{fcos_vm.ip}",
|
|
"-w", "%{http_code}",
|
|
f"http://ping.{top_level_domain}/"
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
)
|
|
assert result.returncode == 22, f"curl failed with exit code {result.returncode}: {result.stderr}"
|
|
assert int(result.stdout.strip()) == 403, f"Expected HTTP 403 from ping endpoint, got: {result.stdout.strip()}"
|
|
|
|
@pytest.mark.flaky(reruns=12, reruns_delay=5)
|
|
def test_traefik_tls(self, fcos_vm, pebble_acme_server, top_level_domain):
|
|
"""Traefik must respond to the secure endpoint with HTTP 200."""
|
|
|
|
# On the host running pytest, create a temporary dir in /tmp and write the Pebble CA certificate in the pebble.pem file.
|
|
tmpdir = tempfile.TemporaryDirectory(delete=True)
|
|
d = Path(tmpdir.name)
|
|
pebble_ca_bundle_path = d / "pebble.pem"
|
|
pebble_ca_bundle_path.write_text(pebble_acme_server['ca_bundle'])
|
|
|
|
result = subprocess.run(
|
|
[
|
|
"curl",
|
|
"-sSf",
|
|
"-o", "/dev/null",
|
|
"--cacert", str(pebble_ca_bundle_path),
|
|
"--resolve", f"secure.{top_level_domain}:443:{fcos_vm.ip}",
|
|
"-w", "%{http_code}",
|
|
f"https://secure.{top_level_domain}/"
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
)
|
|
assert result.returncode == 0, f"curl failed with exit code {result.returncode}: {result.stderr}"
|
|
assert int(result.stdout.strip()) == 200, f"Expected HTTP 200 from ping endpoint, got: {result.stdout.strip()}"
|
|
|
|
def test_traefik_restart(self, fcos_host):
|
|
"""Restarting traefik.target must keep Traefik running and the ping endpoint must still respond."""
|
|
result = fcos_host.run("systemctl restart traefik.target")
|
|
assert result.rc == 0, f"Failed to restart traefik.target: {result.stderr}"
|
|
|
|
# Wait for traefik.target to become active again after the restart
|
|
self.wait_for_service(fcos_host, "traefik.target", timeout=120)
|
|
|
|
# traefik.service must still be running after the restart
|
|
self.check_expected_services(fcos_host, [
|
|
{ "name": "traefik.service", "state": "active", "exists": True },
|
|
])
|
|
|
|
# Ping endpoint must still respond after the restart
|
|
result = fcos_host.run("curl -sSf -H 'Host: ping' http://127.0.0.1/")
|
|
assert result.rc == 0, f"curl failed after restart: {result.stderr}"
|
|
|
|
|