Skip to content

Commit

Permalink
Implement integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
berkayoz committed Jul 9, 2024
1 parent 81192c4 commit a650389
Show file tree
Hide file tree
Showing 14 changed files with 1,289 additions and 19 deletions.
135 changes: 135 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#
# Copyright 2024 Canonical, Ltd.
#
import logging
from pathlib import Path
from typing import Generator, List

import pytest
from test_util import config, harness, util

LOG = logging.getLogger(__name__)


def _harness_clean(h: harness.Harness):
"Clean up created instances within the test harness."

if config.SKIP_CLEANUP:
LOG.warning(
"Skipping harness cleanup. "
"It is your job now to clean up cloud resources"
)
else:
LOG.debug("Cleanup")
h.cleanup()


@pytest.fixture(scope="session")
def h() -> harness.Harness:
LOG.debug("Create harness for %s", config.SUBSTRATE)
if config.SUBSTRATE == "local":
h = harness.LocalHarness()
elif config.SUBSTRATE == "lxd":
h = harness.LXDHarness()
elif config.SUBSTRATE == "multipass":
h = harness.MultipassHarness()
elif config.SUBSTRATE == "juju":
h = harness.JujuHarness()
else:
raise harness.HarnessError(
"TEST_SUBSTRATE must be one of: local, lxd, multipass, juju"
)

yield h

_harness_clean(h)


def pytest_configure(config):
config.addinivalue_line(
"markers",
"node_count: Mark a test to specify how many instance nodes need to be created\n"
"disable_k8s_bootstrapping: By default, the first k8s node is bootstrapped. This marker disables that.",
)


@pytest.fixture(scope="function")
def node_count(request) -> int:
node_count_marker = request.node.get_closest_marker("node_count")
if not node_count_marker:
return 1
node_count_arg, *_ = node_count_marker.args
return int(node_count_arg)


@pytest.fixture(scope="function")
def disable_k8s_bootstrapping(request) -> int:
return bool(request.node.get_closest_marker("disable_k8s_bootstrapping"))


@pytest.fixture(scope="function")
def instances(
h: harness.Harness, node_count: int, tmp_path: Path, disable_k8s_bootstrapping: bool
) -> Generator[List[harness.Instance], None, None]:
"""Construct instances for a cluster.
Bootstrap and setup networking on the first instance, if `disable_k8s_bootstrapping` marker is not set.
"""
if not config.SNAP_CHANNEL:
pytest.fail("Set TEST_SNAP_CHANNEL to the channel of the k8s snap to install.")

if node_count <= 0:
pytest.xfail("Test requested 0 or fewer instances, skip this test.")

LOG.info(f"Creating {node_count} instances")
instances: List[harness.Instance] = []

for _ in range(node_count):
# Create <node_count> instances and setup the k8s snap in each.
instance = h.new_instance()
instances.append(instance)
util.setup_k8s_snap(instance)

if not disable_k8s_bootstrapping:
first_node, *_ = instances
first_node.exec(["k8s", "bootstrap"])

yield instances

if config.SKIP_CLEANUP:
LOG.warning("Skipping clean-up of instances, delete them on your own")
return

# Cleanup after each test.
# We cannot execute _harness_clean() here as this would also
# remove the session_instance. The harness ensures that everything is cleaned up
# at the end of the test session.
for instance in instances:
h.delete_instance(instance.id)


@pytest.fixture(scope="session")
def session_instance(
h: harness.Harness, tmp_path_factory: pytest.TempPathFactory
) -> Generator[harness.Instance, None, None]:
"""Constructs and bootstraps an instance that persists over a test session.
Bootstraps the instance with all k8sd features enabled to reduce testing time.
"""
LOG.info("Setup node and enable all features")

instance = h.new_instance()
util.setup_k8s_snap(instance)

bootstrap_config_path = "/home/ubuntu/bootstrap-session.yaml"
instance.send_file(
(config.MANIFESTS_DIR / "bootstrap-session.yaml").as_posix(),
bootstrap_config_path,
)

instance.exec(["k8s", "bootstrap", "--file", bootstrap_config_path])
util.wait_until_k8s_ready(instance, [instance])
util.wait_for_network(instance)
util.wait_for_dns(instance)

yield instance
101 changes: 99 additions & 2 deletions tests/integration/test_certmanager.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,99 @@
def test_integration_certmanager():
assert True == False, "Integration tests are not yet implemented"
#
# Copyright 2024 Canonical, Ltd.
#
import logging
from pathlib import Path
import os

from test_util import harness, util
from test_util.config import MANIFESTS_DIR

LOG = logging.getLogger(__name__)


def test_integration_certmanager(session_instance: harness.Instance):
images = [
{"variable": "ROCK_CERT_MANAGER_CONTROLLER", "prefix": None},
{"variable": "ROCK_CERT_MANAGER_WEBHOOK", "prefix": "webhook"},
{"variable": "ROCK_CERT_MANAGER_CAINJECTOR", "prefix": "cainjector"},
{"variable": "ROCK_CERT_MANAGER_ACMESOLVER", "prefix": "acmesolver"},
]

helm_command = [
"k8s",
"helm",
"install",
"cert-manager",
"--repo",
"https://charts.jetstack.io",
"cert-manager",
"--namespace",
"cert-manager",
"--create-namespace",
"--version",
"v1.12.2",
"--set",
"installCRDs=true",
]

for image in images:
image_uri = os.getenv(image["variable"])
assert image_uri is not None, f"{image['variable']} is not set"
image_split = image_uri.split(":")

if image["prefix"]:
helm_command += [
"--set",
f"{image['prefix']}.image.repository={image_split[0]}",
"--set",
f"{image['prefix']}.image.tag={image_split[1]}",
"--set",
f"{image['prefix']}.securityContext.runAsUser=584792",
]
else:
helm_command += [
"--set",
f"image.repository={image_split[0]}",
"--set",
f"image.tag={image_split[1]}",
"--set",
"securityContext.runAsUser=584792",
]

session_instance.exec(helm_command)

manifest = MANIFESTS_DIR / "cert-manager-test.yaml"
session_instance.exec(
["k8s", "kubectl", "apply", "-f", "-"],
input=Path(manifest).read_bytes(),
)

util.stubbornly(retries=3, delay_s=1).on(session_instance).exec(
[
"k8s",
"kubectl",
"wait",
"--for=condition=ready",
"certificate",
"selfsigned-cert",
"--namespace",
"cert-manager-test",
"--timeout",
"180s",
]
)

util.stubbornly(retries=5, delay_s=10).on(session_instance).until(
lambda p: "selfsigned-cert-tls" in p.stdout.decode()
).exec(
[
"k8s",
"kubectl",
"get",
"secret",
"--namespace",
"cert-manager-test",
"-o",
"json",
]
)
63 changes: 63 additions & 0 deletions tests/integration/test_util/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#
# Copyright 2024 Canonical, Ltd.
#
import os
from pathlib import Path

DIR = Path(__file__).absolute().parent

MANIFESTS_DIR = DIR / ".." / ".." / "templates"

# SNAP is the absolute path to the snap against which we run the integration tests.
SNAP_CHANNEL = os.getenv("TEST_SNAP_CHANNEL")

# SUBSTRATE is the substrate to use for running the integration tests.
# One of 'local' (default), 'lxd', 'juju', or 'multipass'.
SUBSTRATE = os.getenv("TEST_SUBSTRATE") or "local"

# SKIP_CLEANUP can be used to prevent machines to be automatically destroyed
# after the tests complete.
SKIP_CLEANUP = (os.getenv("TEST_SKIP_CLEANUP") or "") == "1"

# LXD_PROFILE_NAME is the profile name to use for LXD containers.
LXD_PROFILE_NAME = os.getenv("TEST_LXD_PROFILE_NAME") or "k8s-integration"

# LXD_PROFILE is the profile to use for LXD containers.
LXD_PROFILE = (
os.getenv("TEST_LXD_PROFILE")
or (DIR / ".." / ".." / "lxd-profile.yaml").read_text()
)

# LXD_IMAGE is the image to use for LXD containers.
LXD_IMAGE = os.getenv("TEST_LXD_IMAGE") or "ubuntu:22.04"

# LXD_SIDELOAD_IMAGES_DIR is an optional directory with OCI images from the host
# that will be mounted at /var/snap/k8s/common/images on the LXD containers.
LXD_SIDELOAD_IMAGES_DIR = os.getenv("TEST_LXD_SIDELOAD_IMAGES_DIR") or ""

# MULTIPASS_IMAGE is the image to use for Multipass VMs.
MULTIPASS_IMAGE = os.getenv("TEST_MULTIPASS_IMAGE") or "22.04"

# MULTIPASS_CPUS is the number of cpus for Multipass VMs.
MULTIPASS_CPUS = os.getenv("TEST_MULTIPASS_CPUS") or "2"

# MULTIPASS_MEMORY is the memory for Multipass VMs.
MULTIPASS_MEMORY = os.getenv("TEST_MULTIPASS_MEMORY") or "2G"

# MULTIPASS_DISK is the disk size for Multipass VMs.
MULTIPASS_DISK = os.getenv("TEST_MULTIPASS_DISK") or "10G"

# JUJU_MODEL is the Juju model to use.
JUJU_MODEL = os.getenv("TEST_JUJU_MODEL")

# JUJU_CONTROLLER is the Juju controller to use.
JUJU_CONTROLLER = os.getenv("TEST_JUJU_CONTROLLER")

# JUJU_CONSTRAINTS is the constraints to use when creating Juju machines.
JUJU_CONSTRAINTS = os.getenv("TEST_JUJU_CONSTRAINTS", "mem=4G cores=2 root-disk=20G")

# JUJU_BASE is the base OS to use when creating Juju machines.
JUJU_BASE = os.getenv("TEST_JUJU_BASE") or "[email protected]"

# JUJU_MACHINES is a list of existing Juju machines to use.
JUJU_MACHINES = os.getenv("TEST_JUJU_MACHINES") or ""
18 changes: 18 additions & 0 deletions tests/integration/test_util/harness/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# Copyright 2024 Canonical, Ltd.
#
from test_util.harness.base import Harness, HarnessError, Instance
from test_util.harness.juju import JujuHarness
from test_util.harness.local import LocalHarness
from test_util.harness.lxd import LXDHarness
from test_util.harness.multipass import MultipassHarness

__all__ = [
HarnessError,
Harness,
Instance,
JujuHarness,
LocalHarness,
LXDHarness,
MultipassHarness,
]
Loading

0 comments on commit a650389

Please sign in to comment.