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
16 changes: 16 additions & 0 deletions tests/mesh/role-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Select the image used by the ARC manifests. An explicitly supplied image is pulled by k3s.
# shellcheck disable=SC2034 # The caller reads both selected values.
grtest_select_role_image() {
if [ "${GRTEST_ROLE_IMAGE+x}" ]; then
if [ -z "$GRTEST_ROLE_IMAGE" ]; then
printf '%s\n' 'GRTEST_ROLE_IMAGE must not be empty' >&2
return 1
fi
role_controller_image="$GRTEST_ROLE_IMAGE"
role_image_published=1
else
role_controller_image=grtest/fleet-role:test
role_image_published=0
fi
}
34 changes: 20 additions & 14 deletions tests/mesh/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# With GRTEST_PIN_CONTEXT=1, the cluster scenarios run the role, and the in_cluster recovery playbook, with github_runner_cluster_docker_context pinned while the current Docker context they see points at nothing (tests/lib/docker_context.sh), so any docker call that ignored the pin fails the run; afterwards the current context must be unchanged. context_checks the role refuses a pinned context that does not exist, and a pinned context alongside DOCKER_HOST, before writing anything (not in the default set).
#
# Usage: tests/mesh/run.sh <scenario>... (default: all three). Needs Docker with Compose, and Python 3 with ansible-core (ANSIBLE_PLAYBOOK overrides which ansible-playbook runs). Creates only Docker objects named grtest-*, and removes them again on exit unless GRTEST_KEEP=1.
# Usage: tests/mesh/run.sh <scenario>... (default: all three). Needs Docker with Compose, and Python 3 with ansible-core (ANSIBLE_PLAYBOOK overrides which ansible-playbook runs). Creates only Docker objects named grtest-*, and removes them again on exit unless GRTEST_KEEP=1. Set GRTEST_ROLE_IMAGE to a published image reference to have k3s pull it for role_controller and role_swap scenarios instead of building and importing the checkout's image; the registry must be reachable without a pull secret.
set -euo pipefail

repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
Expand Down Expand Up @@ -1190,7 +1190,9 @@ scenario_native_server() {

# The slow link the role_controller scenario puts on a voter, in milliseconds each way on its mesh interface: above the unfit round trip limit, a tenth of the election timeout the inventory sets.
role_controller_delay_ms=$((etcd_election_ms / 10 + 100))
role_controller_image=grtest/fleet-role:test
# shellcheck source=tests/mesh/role-image.sh
. "${repo_root}/tests/mesh/role-image.sh"
grtest_select_role_image
platform_namespace=github-runner-platform

role_status() { kubectl_on "$api_node" --namespace "$platform_namespace" get configmap fleet-role-status -o 'jsonpath={.data.status\.json}' 2>/dev/null; }
Expand All @@ -1217,20 +1219,24 @@ keep_role_image_loaded() {
done
}

# Runs the role controller's probe and controller in the cluster, the probes at $1 seconds (role_controller_sample_seconds by default): the image built from this checkout is loaded into every node (the native host's containerd too, when the scenario has one), the ARC role renders its manifests, and they are applied and rolled out.
# Runs the role controller's probe and controller in the cluster, the probes at $1 seconds (role_controller_sample_seconds by default): the ARC role renders its manifests for the selected image, which is built and loaded into every node by default or pulled by k3s when GRTEST_ROLE_IMAGE is set.
deploy_role_controller() {
local index image_file manifests sample_seconds=${1:-$role_controller_sample_seconds}
log "Building the role controller image from this checkout and loading it into every node"
docker build --quiet --file "${repo_root}/fleet-role/Dockerfile" --tag "$role_controller_image" "$repo_root" >/dev/null
image_file="${work}/fleet-role-image.tar"
docker save --output "$image_file" "$role_controller_image"
for index in $(seq 1 "$node_count"); do
docker exec -i "$(container "$index")" /bin/ctr --address /run/k3s/containerd/containerd.sock --namespace k8s.io images import - < "$image_file" >/dev/null
done
if [ -n "$native_role" ]; then native_exec k3s ctr --namespace k8s.io images import - < "$image_file" >/dev/null; fi
# A host that changes role starts a container whose containerd has no copy of the image (the server and agent services keep separate data volumes, and a real host pulls the image from its registry), so a loop loads it into each node container as one comes up, which keeps the new voter's probe running.
keep_role_image_loaded "$image_file" &
image_loader_pid=$!
if [ "$role_image_published" = 1 ]; then
log "Using published role controller image ${role_controller_image}; k3s pulls it on each node"
else
log "Building the role controller image from this checkout and loading it into every node"
docker build --quiet --file "${repo_root}/fleet-role/Dockerfile" --tag "$role_controller_image" "$repo_root" >/dev/null
image_file="${work}/fleet-role-image.tar"
docker save --output "$image_file" "$role_controller_image"
for index in $(seq 1 "$node_count"); do
docker exec -i "$(container "$index")" /bin/ctr --address /run/k3s/containerd/containerd.sock --namespace k8s.io images import - < "$image_file" >/dev/null
done
if [ -n "$native_role" ]; then native_exec k3s ctr --namespace k8s.io images import - < "$image_file" >/dev/null; fi
# A host that changes role starts a container whose containerd has no copy of the image (the server and agent services keep separate data volumes, and a real host pulls the image from its registry), so a loop loads it into each node container as one comes up, which keeps the new voter's probe running.
keep_role_image_loaded "$image_file" &
image_loader_pid=$!
fi

log "Rendering the ARC role's manifests for the controller and the probe, and applying them"
manifests="${work}/fleet-role.yaml"
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_mesh_role_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Image selection for the disposable mesh role-controller scenarios."""

import os
import subprocess
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
SELECTOR = ROOT / "tests/mesh/role-image.sh"


class MeshRoleImageTest(unittest.TestCase):
def select(self, image: str | None) -> subprocess.CompletedProcess[str]:
env = os.environ.copy()
env.pop("GRTEST_ROLE_IMAGE", None)
if image is not None:
env["GRTEST_ROLE_IMAGE"] = image
return subprocess.run(
["bash", "-c", 'set -eu; . "$1"; grtest_select_role_image; printf "%s\\n%s\\n" "$role_controller_image" "$role_image_published"', "bash", str(SELECTOR)],
env=env, text=True, capture_output=True, check=False,
)

def test_default_builds_the_local_image(self) -> None:
result = self.select(None)
self.assertEqual(result.returncode, 0)
self.assertEqual(result.stdout, "grtest/fleet-role:test\n0\n")

def test_explicit_tag_or_digest_is_passed_through(self) -> None:
for image in ("ghcr.io/example/fleet-role:1.2.3", "ghcr.io/example/fleet-role@sha256:" + "a" * 64):
with self.subTest(image=image):
result = self.select(image)
self.assertEqual(result.returncode, 0)
self.assertEqual(result.stdout, f"{image}\n1\n")

def test_empty_explicit_image_is_rejected(self) -> None:
result = self.select("")
self.assertNotEqual(result.returncode, 0)
self.assertIn("must not be empty", result.stderr)


if __name__ == "__main__":
unittest.main()
Loading