2026-07-17 18:30:34 +09:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
|
|
TESTBED="$ROOT/tests/kind"
|
|
|
|
|
RUNTIME="$TESTBED/.runtime"
|
|
|
|
|
BIN="$RUNTIME/bin"
|
|
|
|
|
KUBECONFIG_PATH="$RUNTIME/kubeconfig"
|
|
|
|
|
FIXTURE_LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp/nuthatch-$(id -u)}"
|
|
|
|
|
FIXTURE_LOCK_PATH="$FIXTURE_LOCK_DIR/nuthatch-testbed-kind.lock"
|
|
|
|
|
CONTEXT=kind-nuthatch-testbed
|
|
|
|
|
CLUSTER=nuthatch-testbed
|
|
|
|
|
NERDCTL_NAMESPACE=nuthatch-testbed
|
|
|
|
|
FIXTURE_NAMESPACE=nuthatch-testbed
|
|
|
|
|
|
|
|
|
|
set -a
|
|
|
|
|
source "$TESTBED/versions.env"
|
|
|
|
|
set +a
|
|
|
|
|
|
|
|
|
|
user_kubeconfig_digest() {
|
|
|
|
|
local config_value="${KUBECONFIG:-${HOME:?}/.kube/config}"
|
|
|
|
|
local path
|
|
|
|
|
local paths
|
|
|
|
|
IFS=: read -r -a paths <<< "$config_value"
|
|
|
|
|
{
|
|
|
|
|
for path in "${paths[@]}"; do
|
|
|
|
|
if [[ -f "$path" ]]; then
|
|
|
|
|
sha256sum "$path"
|
|
|
|
|
else
|
|
|
|
|
printf 'missing %s\n' "$path"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
} | sha256sum | cut -d' ' -f1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
download_tool() {
|
|
|
|
|
local destination="$1"
|
|
|
|
|
local checksum="$2"
|
|
|
|
|
local url="$3"
|
|
|
|
|
if [[ -x "$destination" ]] &&
|
|
|
|
|
[[ "$(sha256sum "$destination" | cut -d' ' -f1)" = "$checksum" ]]; then
|
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
curl --fail --location --silent --show-error "$url" --output "$destination.tmp"
|
|
|
|
|
test "$(sha256sum "$destination.tmp" | cut -d' ' -f1)" = "$checksum"
|
|
|
|
|
chmod 0755 "$destination.tmp"
|
|
|
|
|
mv "$destination.tmp" "$destination"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prepare_tools() {
|
|
|
|
|
mkdir -p "$BIN"
|
|
|
|
|
download_tool \
|
|
|
|
|
"$BIN/kind" \
|
|
|
|
|
"$KIND_SHA256" \
|
|
|
|
|
"https://github.com/kubernetes-sigs/kind/releases/download/$KIND_VERSION/kind-linux-amd64"
|
|
|
|
|
download_tool \
|
|
|
|
|
"$BIN/kubectl" \
|
|
|
|
|
"$KUBECTL_SHA256" \
|
|
|
|
|
"https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl"
|
|
|
|
|
test "$("$BIN/kind" version | awk '{print $2}')" = "$KIND_VERSION"
|
|
|
|
|
test "$("$BIN/kubectl" --kubeconfig "$KUBECONFIG_PATH" --context "$CONTEXT" version --client=true -o json | jq -r .clientVersion.gitVersion)" = "$KUBECTL_VERSION"
|
|
|
|
|
test "$(nerdctl --version | awk '{print $3}')" = "$NERDCTL_VERSION"
|
|
|
|
|
test "$(runtime_ctr version | awk '/Client:/{client=1} client && $1=="Version:"{print $2; exit}')" = "v$CONTAINERD_VERSION"
|
|
|
|
|
test "$(runtime_ctr version | awk '/Server:/{server=1} server && $1=="Version:"{print $2; exit}')" = "v$CONTAINERD_VERSION"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acquire_fixture_lock() {
|
|
|
|
|
mkdir -p "$RUNTIME" "$FIXTURE_LOCK_DIR"
|
|
|
|
|
exec 9>"$FIXTURE_LOCK_PATH"
|
|
|
|
|
if ! flock --nonblock 9; then
|
|
|
|
|
printf 'kind test fixture is already in use\n' >&2
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
configure_provider() {
|
|
|
|
|
if [[ -z "${NUTHATCH_REAL_NERDCTL:-}" ]]; then
|
2026-07-18 00:54:47 +09:00
|
|
|
NUTHATCH_REAL_NERDCTL="$(command -v nerdctl)" || return
|
2026-07-17 18:30:34 +09:00
|
|
|
fi
|
|
|
|
|
export NUTHATCH_REAL_NERDCTL
|
|
|
|
|
export CONTAINERD_NAMESPACE="$NERDCTL_NAMESPACE"
|
|
|
|
|
export KIND_EXPERIMENTAL_PROVIDER=nerdctl
|
|
|
|
|
export PATH="$TESTBED/provider:$PATH"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runtime_ctr() {
|
|
|
|
|
ctr --address "${CONTAINERD_ADDRESS:-${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/containerd/containerd.sock}" "$@"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runtime_namespace_exists() {
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" namespace list --quiet | grep -Fx "$NERDCTL_NAMESPACE" >/dev/null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cluster_exists() {
|
|
|
|
|
runtime_namespace_exists &&
|
|
|
|
|
"$BIN/kind" get clusters | grep -Fx "$CLUSTER" >/dev/null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete_cluster() {
|
|
|
|
|
local container
|
|
|
|
|
local containers
|
|
|
|
|
if cluster_exists; then
|
|
|
|
|
"$BIN/kind" delete cluster --name "$CLUSTER" || true
|
|
|
|
|
fi
|
|
|
|
|
containers="$("$NUTHATCH_REAL_NERDCTL" --namespace "$NERDCTL_NAMESPACE" ps \
|
|
|
|
|
--all \
|
|
|
|
|
--quiet \
|
2026-07-18 00:54:47 +09:00
|
|
|
--filter "label=io.x-k8s.kind.cluster=$CLUSTER")" || return
|
2026-07-17 18:30:34 +09:00
|
|
|
while IFS= read -r container; do
|
|
|
|
|
[[ -z "$container" ]] ||
|
2026-07-18 00:54:47 +09:00
|
|
|
"$NUTHATCH_REAL_NERDCTL" --namespace "$NERDCTL_NAMESPACE" rm --force "$container" ||
|
|
|
|
|
return
|
2026-07-17 18:30:34 +09:00
|
|
|
done <<< "$containers"
|
|
|
|
|
if cluster_exists; then
|
|
|
|
|
"$BIN/kind" delete cluster --name "$CLUSTER" || true
|
|
|
|
|
fi
|
|
|
|
|
if cluster_exists; then
|
|
|
|
|
printf 'kind cluster %s still exists after teardown\n' "$CLUSTER" >&2
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remove_runtime_namespace() {
|
|
|
|
|
local status=0
|
|
|
|
|
local digest
|
|
|
|
|
local digests
|
|
|
|
|
local lease
|
|
|
|
|
local leases
|
|
|
|
|
local network
|
|
|
|
|
for network in kind nuthatch-kind; do
|
|
|
|
|
if "$NUTHATCH_REAL_NERDCTL" --namespace "$NERDCTL_NAMESPACE" network inspect "$network" >/dev/null 2>&1; then
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" --namespace "$NERDCTL_NAMESPACE" network remove "$network" >/dev/null || status=1
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
if ! runtime_namespace_exists; then
|
|
|
|
|
return "$status"
|
|
|
|
|
fi
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" --namespace "$NERDCTL_NAMESPACE" image prune --all --force >/dev/null || status=1
|
|
|
|
|
leases="$(runtime_ctr --namespace "$NERDCTL_NAMESPACE" leases list --quiet)" || status=1
|
|
|
|
|
while IFS= read -r lease; do
|
|
|
|
|
[[ -z "$lease" ]] || runtime_ctr --namespace "$NERDCTL_NAMESPACE" leases delete "$lease" >/dev/null || status=1
|
|
|
|
|
done <<< "$leases"
|
|
|
|
|
digests="$(runtime_ctr --namespace "$NERDCTL_NAMESPACE" content list --quiet)" || status=1
|
|
|
|
|
while IFS= read -r digest; do
|
|
|
|
|
[[ -z "$digest" ]] || runtime_ctr --namespace "$NERDCTL_NAMESPACE" content remove "$digest" >/dev/null || status=1
|
|
|
|
|
done <<< "$digests"
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" namespace remove "$NERDCTL_NAMESPACE" >/dev/null || status=1
|
|
|
|
|
runtime_namespace_exists && status=1
|
|
|
|
|
return "$status"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prepare_provider() {
|
|
|
|
|
configure_provider
|
|
|
|
|
delete_cluster
|
|
|
|
|
remove_runtime_namespace
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" namespace create "$NERDCTL_NAMESPACE" >/dev/null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
load_external_image() {
|
|
|
|
|
local name="$1"
|
|
|
|
|
local source="$2"
|
|
|
|
|
local digest="${source##*@}"
|
|
|
|
|
local image="nuthatch-testbed/$name:sha256-${digest#sha256:}"
|
|
|
|
|
local archive="$RUNTIME/image-archives/$digest.tar"
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" image pull --platform linux/amd64 --unpack=false --quiet "$source" >&2
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" image tag "$source" "$image"
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" image save --platform linux/amd64 --output "$archive" "$image"
|
|
|
|
|
printf 'Loading %s from %s\n' "$image" "$source" >&2
|
|
|
|
|
"$BIN/kind" load image-archive "$archive" --name "$CLUSTER" >&2
|
|
|
|
|
printf '%s\n' "$image"
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 00:54:47 +09:00
|
|
|
load_local_image() {
|
|
|
|
|
local image="$1"
|
|
|
|
|
local namespace="${2:-default}"
|
|
|
|
|
local archive="$RUNTIME/image-archives/nuthatch-app.tar"
|
|
|
|
|
rm -f "$archive"
|
|
|
|
|
"$NUTHATCH_REAL_NERDCTL" --namespace "$namespace" image save \
|
|
|
|
|
--platform linux/amd64 \
|
|
|
|
|
--output "$archive" \
|
|
|
|
|
"$image"
|
|
|
|
|
"$BIN/kind" load image-archive "$archive" --name "$CLUSTER"
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 18:30:34 +09:00
|
|
|
render_manifests() {
|
|
|
|
|
local postgres="$1" redis="$2" dragonfly="$3" keycloak="$4" zitadel="$5"
|
|
|
|
|
local garage="$6" versitygw="$7" coredns="$8" curl="$9" aws_cli="${10}"
|
2026-07-18 00:54:47 +09:00
|
|
|
local postgres_replicas="${11}" redis_replicas="${12}" dragonfly_replicas="${13}"
|
|
|
|
|
local keycloak_replicas="${14}" zitadel_replicas="${15}"
|
|
|
|
|
local garage_replicas="${16}" versitygw_replicas="${17}" coredns_replicas="${18}"
|
2026-07-17 18:30:34 +09:00
|
|
|
sed \
|
|
|
|
|
-e "s|@@POSTGRES_IMAGE@@|$postgres|g" \
|
|
|
|
|
-e "s|@@REDIS_IMAGE@@|$redis|g" \
|
|
|
|
|
-e "s|@@DRAGONFLY_IMAGE@@|$dragonfly|g" \
|
|
|
|
|
-e "s|@@KEYCLOAK_IMAGE@@|$keycloak|g" \
|
|
|
|
|
-e "s|@@ZITADEL_IMAGE@@|$zitadel|g" \
|
|
|
|
|
-e "s|@@GARAGE_IMAGE@@|$garage|g" \
|
|
|
|
|
-e "s|@@VERSITYGW_IMAGE@@|$versitygw|g" \
|
|
|
|
|
-e "s|@@COREDNS_IMAGE@@|$coredns|g" \
|
|
|
|
|
-e "s|@@AWS_CLI_IMAGE@@|$aws_cli|g" \
|
2026-07-18 00:54:47 +09:00
|
|
|
-e "s|@@POSTGRES_REPLICAS@@|$postgres_replicas|g" \
|
|
|
|
|
-e "s|@@REDIS_REPLICAS@@|$redis_replicas|g" \
|
|
|
|
|
-e "s|@@DRAGONFLY_REPLICAS@@|$dragonfly_replicas|g" \
|
|
|
|
|
-e "s|@@KEYCLOAK_REPLICAS@@|$keycloak_replicas|g" \
|
|
|
|
|
-e "s|@@ZITADEL_REPLICAS@@|$zitadel_replicas|g" \
|
|
|
|
|
-e "s|@@GARAGE_REPLICAS@@|$garage_replicas|g" \
|
|
|
|
|
-e "s|@@VERSITYGW_REPLICAS@@|$versitygw_replicas|g" \
|
|
|
|
|
-e "s|@@COREDNS_REPLICAS@@|$coredns_replicas|g" \
|
2026-07-17 18:30:34 +09:00
|
|
|
"$TESTBED/dependencies.yaml.in" > "$RUNTIME/dependencies.yaml"
|
|
|
|
|
sed \
|
|
|
|
|
-e "s|@@POSTGRES_IMAGE@@|$postgres|g" \
|
|
|
|
|
-e "s|@@REDIS_IMAGE@@|$redis|g" \
|
|
|
|
|
-e "s|@@CURL_IMAGE@@|$curl|g" \
|
|
|
|
|
-e "s|@@AWS_CLI_IMAGE@@|$aws_cli|g" \
|
|
|
|
|
"$TESTBED/probe.yaml.in" > "$RUNTIME/probe.yaml"
|
2026-07-18 00:54:47 +09:00
|
|
|
{
|
|
|
|
|
printf 'POSTGRES_LOCAL_IMAGE=%q\n' "$postgres"
|
|
|
|
|
printf 'REDIS_LOCAL_IMAGE=%q\n' "$redis"
|
|
|
|
|
printf 'DRAGONFLY_LOCAL_IMAGE=%q\n' "$dragonfly"
|
|
|
|
|
printf 'KEYCLOAK_LOCAL_IMAGE=%q\n' "$keycloak"
|
|
|
|
|
printf 'ZITADEL_LOCAL_IMAGE=%q\n' "$zitadel"
|
|
|
|
|
printf 'GARAGE_LOCAL_IMAGE=%q\n' "$garage"
|
|
|
|
|
printf 'VERSITYGW_LOCAL_IMAGE=%q\n' "$versitygw"
|
|
|
|
|
printf 'COREDNS_LOCAL_IMAGE=%q\n' "$coredns"
|
|
|
|
|
printf 'CURL_LOCAL_IMAGE=%q\n' "$curl"
|
|
|
|
|
printf 'AWS_CLI_LOCAL_IMAGE=%q\n' "$aws_cli"
|
|
|
|
|
} > "$RUNTIME/fixture.env"
|
2026-07-17 18:30:34 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
kube() {
|
|
|
|
|
"$BIN/kubectl" \
|
|
|
|
|
--kubeconfig "$KUBECONFIG_PATH" \
|
|
|
|
|
--context "$CONTEXT" \
|
|
|
|
|
"$@"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wait_for_job() {
|
|
|
|
|
local job="$1"
|
|
|
|
|
local deadline=$((SECONDS + 300))
|
|
|
|
|
local complete
|
|
|
|
|
local failed
|
|
|
|
|
while ((SECONDS < deadline)); do
|
|
|
|
|
complete="$(kube --namespace "$FIXTURE_NAMESPACE" get "job/$job" --output jsonpath='{.status.conditions[?(@.type=="Complete")].status}')"
|
|
|
|
|
if [[ "$complete" = True ]]; then
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" logs "job/$job" --all-containers
|
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
failed="$(kube --namespace "$FIXTURE_NAMESPACE" get "job/$job" --output jsonpath='{.status.conditions[?(@.type=="Failed")].status}')"
|
|
|
|
|
if [[ "$failed" = True ]]; then
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" logs "job/$job" --all-containers >&2 || true
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
sleep 1
|
|
|
|
|
done
|
|
|
|
|
printf 'job/%s did not complete within 300 seconds\n' "$job" >&2
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 00:54:47 +09:00
|
|
|
select_fixture_families() {
|
|
|
|
|
local requested=("$@")
|
|
|
|
|
local family
|
|
|
|
|
local candidate
|
|
|
|
|
local found
|
|
|
|
|
local use_all=false
|
|
|
|
|
FIXTURE_FAMILIES=()
|
|
|
|
|
if [[ "${#requested[@]}" = 0 ]]; then
|
|
|
|
|
requested=(all)
|
|
|
|
|
fi
|
|
|
|
|
for family in "${requested[@]}"; do
|
|
|
|
|
case "$family" in
|
|
|
|
|
all) use_all=true ;;
|
|
|
|
|
base | durable | coordination | oidc | target) ;;
|
|
|
|
|
*)
|
|
|
|
|
printf 'unknown fixture family: %s\n' "$family" >&2
|
|
|
|
|
return 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
if [[ "$use_all" = true ]]; then
|
|
|
|
|
requested=(durable coordination oidc target)
|
|
|
|
|
fi
|
|
|
|
|
for candidate in durable coordination oidc target; do
|
|
|
|
|
found=false
|
|
|
|
|
for family in "${requested[@]}"; do
|
|
|
|
|
if [[ "$family" = "$candidate" ]]; then
|
|
|
|
|
found=true
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
if [[ "$found" = true ]]; then
|
|
|
|
|
FIXTURE_FAMILIES+=("$candidate")
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fixture_has_family() {
|
|
|
|
|
local expected="$1"
|
|
|
|
|
local family
|
|
|
|
|
for family in "${FIXTURE_FAMILIES[@]}"; do
|
|
|
|
|
if [[ "$family" = "$expected" ]]; then
|
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expand_fixture_matrix() {
|
|
|
|
|
local coordination_providers=(-)
|
|
|
|
|
local oidc_providers=(-)
|
|
|
|
|
local target_providers=(-)
|
|
|
|
|
local coordination
|
|
|
|
|
local oidc
|
|
|
|
|
local target
|
|
|
|
|
local lane=0
|
|
|
|
|
select_fixture_families "$@" || return
|
|
|
|
|
if fixture_has_family coordination; then
|
|
|
|
|
coordination_providers=(redis dragonfly)
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family oidc; then
|
|
|
|
|
oidc_providers=(keycloak zitadel)
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family target; then
|
|
|
|
|
target_providers=(garage versitygw)
|
|
|
|
|
fi
|
|
|
|
|
for coordination in "${coordination_providers[@]}"; do
|
|
|
|
|
for oidc in "${oidc_providers[@]}"; do
|
|
|
|
|
for target in "${target_providers[@]}"; do
|
|
|
|
|
lane=$((lane + 1))
|
|
|
|
|
printf '%03d\t%s\t%s\t%s\n' "$lane" "$coordination" "$oidc" "$target"
|
|
|
|
|
done
|
|
|
|
|
done
|
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 18:30:34 +09:00
|
|
|
setup_fixture() {
|
2026-07-18 00:54:47 +09:00
|
|
|
select_fixture_families "$@" || return
|
2026-07-17 18:30:34 +09:00
|
|
|
mkdir -p "$RUNTIME"
|
|
|
|
|
rm -rf "$RUNTIME/image-archives"
|
|
|
|
|
mkdir -p "$RUNTIME/image-archives"
|
|
|
|
|
prepare_tools
|
|
|
|
|
prepare_provider
|
|
|
|
|
"$BIN/kind" create cluster \
|
|
|
|
|
--name "$CLUSTER" \
|
|
|
|
|
--image "$NODE_IMAGE" \
|
|
|
|
|
--config "$TESTBED/cluster.yaml" \
|
|
|
|
|
--kubeconfig "$KUBECONFIG_PATH" \
|
|
|
|
|
--wait 180s
|
2026-07-18 00:54:47 +09:00
|
|
|
local postgres=nuthatch-testbed/unused:postgres
|
|
|
|
|
local redis=nuthatch-testbed/unused:redis
|
|
|
|
|
local dragonfly=nuthatch-testbed/unused:dragonfly
|
|
|
|
|
local keycloak=nuthatch-testbed/unused:keycloak
|
|
|
|
|
local zitadel=nuthatch-testbed/unused:zitadel
|
|
|
|
|
local garage=nuthatch-testbed/unused:garage
|
|
|
|
|
local versitygw=nuthatch-testbed/unused:versitygw
|
|
|
|
|
local coredns=nuthatch-testbed/unused:coredns
|
|
|
|
|
local curl=nuthatch-testbed/unused:curl
|
|
|
|
|
local aws_cli=nuthatch-testbed/unused:aws-cli
|
|
|
|
|
local postgres_replicas=0 redis_replicas=0 dragonfly_replicas=0
|
|
|
|
|
local keycloak_replicas=0 zitadel_replicas=0
|
|
|
|
|
local garage_replicas=0 versitygw_replicas=0 coredns_replicas=0
|
|
|
|
|
if fixture_has_family durable || fixture_has_family oidc; then
|
|
|
|
|
postgres="$(load_external_image postgres "$POSTGRES_IMAGE")"
|
|
|
|
|
postgres_replicas=1
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family coordination; then
|
|
|
|
|
redis="$(load_external_image redis "$REDIS_IMAGE")"
|
|
|
|
|
dragonfly="$(load_external_image dragonfly "$DRAGONFLY_IMAGE")"
|
|
|
|
|
redis_replicas=1
|
|
|
|
|
dragonfly_replicas=1
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family oidc; then
|
|
|
|
|
keycloak="$(load_external_image keycloak "$KEYCLOAK_IMAGE")"
|
|
|
|
|
zitadel="$(load_external_image zitadel "$ZITADEL_IMAGE")"
|
|
|
|
|
coredns="$(load_external_image coredns "$COREDNS_IMAGE")"
|
|
|
|
|
curl="$(load_external_image curl "$CURL_IMAGE")"
|
|
|
|
|
keycloak_replicas=1
|
|
|
|
|
zitadel_replicas=1
|
|
|
|
|
coredns_replicas=1
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family target; then
|
|
|
|
|
garage="$(load_external_image garage "$GARAGE_IMAGE")"
|
|
|
|
|
versitygw="$(load_external_image versitygw "$VERSITYGW_IMAGE")"
|
|
|
|
|
aws_cli="$(load_external_image aws-cli "$AWS_CLI_IMAGE")"
|
|
|
|
|
if [[ "$curl" = nuthatch-testbed/unused:curl ]]; then
|
|
|
|
|
curl="$(load_external_image curl "$CURL_IMAGE")"
|
|
|
|
|
fi
|
|
|
|
|
garage_replicas=1
|
|
|
|
|
versitygw_replicas=1
|
|
|
|
|
fi
|
2026-07-17 18:30:34 +09:00
|
|
|
render_manifests "$postgres" "$redis" "$dragonfly" "$keycloak" "$zitadel" \
|
2026-07-18 00:54:47 +09:00
|
|
|
"$garage" "$versitygw" "$coredns" "$curl" "$aws_cli" \
|
|
|
|
|
"$postgres_replicas" "$redis_replicas" "$dragonfly_replicas" \
|
|
|
|
|
"$keycloak_replicas" "$zitadel_replicas" \
|
|
|
|
|
"$garage_replicas" "$versitygw_replicas" "$coredns_replicas"
|
2026-07-17 18:30:34 +09:00
|
|
|
kube apply --filename "$RUNTIME/dependencies.yaml"
|
2026-07-18 00:54:47 +09:00
|
|
|
if fixture_has_family durable || fixture_has_family oidc; then
|
|
|
|
|
wait_for_deployment postgres
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family coordination; then
|
|
|
|
|
wait_for_deployment redis
|
|
|
|
|
wait_for_deployment dragonfly
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family oidc; then
|
|
|
|
|
wait_for_deployment keycloak
|
|
|
|
|
wait_for_deployment zitadel
|
|
|
|
|
wait_for_deployment controlled-dns
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family target; then
|
|
|
|
|
wait_for_deployment garage
|
|
|
|
|
wait_for_deployment versitygw
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wait_for_deployment() {
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" rollout status "deployment/$1" --timeout 240s
|
2026-07-17 18:30:34 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verify_dependencies() {
|
|
|
|
|
kube apply --filename "$RUNTIME/probe.yaml"
|
|
|
|
|
local job
|
|
|
|
|
for job in state-probe oidc-probe garage-probe versitygw-probe; do
|
|
|
|
|
wait_for_job "$job"
|
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 00:54:47 +09:00
|
|
|
reset_lane_durable_store() {
|
|
|
|
|
local lane="$1"
|
|
|
|
|
local database="nuthatch_lane_$lane"
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/postgres -- \
|
|
|
|
|
env PGPASSWORD=synthetic-postgres psql \
|
|
|
|
|
--username nuthatch \
|
|
|
|
|
--dbname postgres \
|
|
|
|
|
--set ON_ERROR_STOP=1 \
|
|
|
|
|
--command "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$database'" \
|
|
|
|
|
--command "DROP DATABASE IF EXISTS $database" \
|
|
|
|
|
--command "CREATE DATABASE $database" >&2 || return
|
|
|
|
|
printf '%s\n' "$database"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reset_lane_coordination_store() {
|
|
|
|
|
local provider="$1"
|
|
|
|
|
local lane="$2"
|
|
|
|
|
local database=$((10#$lane))
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$provider" -- \
|
|
|
|
|
redis-cli --raw --no-auth-warning -n "$database" FLUSHDB >/dev/null || return
|
|
|
|
|
printf '%s\n' "$database"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
target_endpoint() {
|
|
|
|
|
case "$1" in
|
|
|
|
|
garage) printf 'http://garage:3900\n' ;;
|
|
|
|
|
versitygw) printf 'http://versitygw:10000\n' ;;
|
|
|
|
|
*) return 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
target_loopback_endpoint() {
|
|
|
|
|
case "$1" in
|
|
|
|
|
garage) printf 'http://127.0.0.1:3900\n' ;;
|
|
|
|
|
versitygw) printf 'http://127.0.0.1:10000\n' ;;
|
|
|
|
|
*) return 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 16:16:24 +09:00
|
|
|
ensure_target_scope_canary() {
|
2026-07-18 00:54:47 +09:00
|
|
|
local provider="$1"
|
|
|
|
|
local endpoint
|
|
|
|
|
endpoint="$(target_loopback_endpoint "$provider")" || return
|
|
|
|
|
if ! kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$provider" --container s3-readiness -- \
|
2026-07-18 16:16:24 +09:00
|
|
|
aws --endpoint-url "$endpoint" s3api create-bucket --bucket nuthatch-fixture >&2; then
|
2026-07-18 00:54:47 +09:00
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$provider" --container s3-readiness -- \
|
2026-07-18 16:16:24 +09:00
|
|
|
aws --endpoint-url "$endpoint" s3api head-bucket --bucket nuthatch-fixture >/dev/null || return
|
2026-07-18 00:54:47 +09:00
|
|
|
fi
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$provider" --container s3-readiness -- \
|
2026-07-18 16:16:24 +09:00
|
|
|
sh -ec 'printf scope-canary >/tmp/scope-canary && aws --endpoint-url "$1" s3api put-object --bucket nuthatch-fixture --key scope-canary --body /tmp/scope-canary >/dev/null' \
|
|
|
|
|
sh "$endpoint"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
provision_lane_target_principal() {
|
|
|
|
|
local provider="$1"
|
|
|
|
|
local lane="$2"
|
|
|
|
|
local bucket="$3"
|
|
|
|
|
local access_key_var="$4"
|
|
|
|
|
local secret_key_var="$5"
|
|
|
|
|
local endpoint
|
|
|
|
|
local access_key
|
|
|
|
|
local secret_key
|
|
|
|
|
endpoint="$(target_loopback_endpoint "$provider")" || return
|
|
|
|
|
case "$provider" in
|
|
|
|
|
garage)
|
|
|
|
|
printf -v access_key 'GKWP05%028d' "$((10#$lane))"
|
|
|
|
|
printf -v secret_key '%064d' "$((10#$lane))"
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/garage --container garage -- \
|
|
|
|
|
/garage bucket create "$bucket" >/dev/null || return
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/garage --container garage -- \
|
|
|
|
|
/garage bucket allow \
|
|
|
|
|
--read \
|
|
|
|
|
--write \
|
|
|
|
|
--owner \
|
|
|
|
|
"$bucket" \
|
|
|
|
|
--key GK00000000000000000000000000000000 >/dev/null || return
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/garage --container garage -- \
|
|
|
|
|
/garage key import --yes -n "nuthatch-$lane" "$access_key" "$secret_key" >/dev/null || return
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/garage --container garage -- \
|
|
|
|
|
/garage bucket allow --read --write "$bucket" --key "$access_key" >/dev/null || return
|
|
|
|
|
;;
|
|
|
|
|
versitygw)
|
|
|
|
|
access_key="nuthatch-wp05-$lane"
|
|
|
|
|
printf -v secret_key 'synthetic-wp05-%049d' "$((10#$lane))"
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/versitygw --container versitygw -- \
|
|
|
|
|
/usr/local/bin/versitygw admin \
|
|
|
|
|
--endpoint-url http://127.0.0.1:10001 \
|
|
|
|
|
create-user \
|
|
|
|
|
--access "$access_key" \
|
|
|
|
|
--secret "$secret_key" \
|
|
|
|
|
--role user \
|
|
|
|
|
--user-id "$((10#$lane))" \
|
|
|
|
|
--group-id "$((10#$lane))" >/dev/null || return
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/versitygw --container versitygw -- \
|
|
|
|
|
/usr/local/bin/versitygw admin \
|
|
|
|
|
--endpoint-url http://127.0.0.1:10001 \
|
|
|
|
|
create-bucket \
|
|
|
|
|
--bucket "$bucket" \
|
|
|
|
|
--owner "$access_key" >/dev/null || return
|
|
|
|
|
;;
|
|
|
|
|
*) return 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
printf -v "$access_key_var" '%s' "$access_key"
|
|
|
|
|
printf -v "$secret_key_var" '%s' "$secret_key"
|
2026-07-18 00:54:47 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
write_lane_profile() {
|
|
|
|
|
local lane="$1"
|
|
|
|
|
local coordination="$2"
|
|
|
|
|
local oidc="$3"
|
|
|
|
|
local target="$4"
|
|
|
|
|
local profile="nuthatch-fixture-lane-$lane"
|
|
|
|
|
local profile_file="$RUNTIME/$profile.env"
|
|
|
|
|
local database=
|
|
|
|
|
local coordination_database=
|
|
|
|
|
local oidc_issuer=
|
|
|
|
|
local target_url=
|
|
|
|
|
local target_bucket=
|
|
|
|
|
local target_region=
|
|
|
|
|
local target_access_key=
|
|
|
|
|
local target_secret_key=
|
|
|
|
|
if fixture_has_family durable; then
|
|
|
|
|
database="$(reset_lane_durable_store "$lane")" || return
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family coordination; then
|
|
|
|
|
coordination_database="$(reset_lane_coordination_store "$coordination" "$lane")" || return
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family oidc; then
|
|
|
|
|
case "$oidc" in
|
|
|
|
|
keycloak) oidc_issuer=http://keycloak.nuthatch.test:8080/realms/master ;;
|
|
|
|
|
zitadel) oidc_issuer=http://zitadel.nuthatch.test:8080 ;;
|
|
|
|
|
*) return 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family target; then
|
|
|
|
|
target_bucket="nuthatch-lane-$lane"
|
|
|
|
|
target_url="$(target_endpoint "$target")" || return
|
2026-07-18 16:16:24 +09:00
|
|
|
ensure_target_scope_canary "$target" || return
|
|
|
|
|
provision_lane_target_principal \
|
|
|
|
|
"$target" \
|
|
|
|
|
"$lane" \
|
|
|
|
|
"$target_bucket" \
|
|
|
|
|
target_access_key \
|
|
|
|
|
target_secret_key || return
|
2026-07-18 00:54:47 +09:00
|
|
|
case "$target" in
|
2026-07-18 16:16:24 +09:00
|
|
|
garage) target_region=garage ;;
|
|
|
|
|
versitygw) target_region=us-east-1 ;;
|
2026-07-18 00:54:47 +09:00
|
|
|
*) return 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
fi
|
|
|
|
|
{
|
|
|
|
|
printf 'NUTHATCH_FIXTURE_PROFILE=%s\n' "$profile"
|
|
|
|
|
printf 'NUTHATCH_FIXTURE_LANE=%s\n' "$lane"
|
|
|
|
|
printf 'NUTHATCH_COORDINATION_PROVIDER=%s\n' "$coordination"
|
|
|
|
|
printf 'NUTHATCH_OIDC_PROVIDER=%s\n' "$oidc"
|
|
|
|
|
printf 'NUTHATCH_TARGET_PROVIDER=%s\n' "$target"
|
|
|
|
|
printf 'NUTHATCH_PG_CONN=host=postgres port=5432 user=nuthatch password=synthetic-postgres dbname=%s\n' "$database"
|
|
|
|
|
printf 'NUTHATCH_REDIS_URL=redis://%s:6379/%s\n' "$coordination" "$coordination_database"
|
|
|
|
|
printf 'NUTHATCH_OIDC_ISSUER=%s\n' "$oidc_issuer"
|
|
|
|
|
printf 'NUTHATCH_S3_ENDPOINT=%s\n' "$target_url"
|
|
|
|
|
printf 'NUTHATCH_S3_BUCKET=%s\n' "$target_bucket"
|
|
|
|
|
printf 'NUTHATCH_S3_REGION=%s\n' "$target_region"
|
|
|
|
|
printf 'NUTHATCH_S3_ACCESS_KEY=%s\n' "$target_access_key"
|
|
|
|
|
printf 'NUTHATCH_S3_SECRET_KEY=%s\n' "$target_secret_key"
|
|
|
|
|
printf 'NUTHATCH_S3_PATH_STYLE=true\n'
|
|
|
|
|
} > "$profile_file" || return
|
|
|
|
|
kube create configmap "$profile" \
|
|
|
|
|
--namespace "$FIXTURE_NAMESPACE" \
|
|
|
|
|
--from-env-file="$profile_file" \
|
|
|
|
|
--dry-run=client \
|
|
|
|
|
--output yaml |
|
|
|
|
|
kube apply --filename - >/dev/null || return
|
|
|
|
|
kube create configmap "$profile-file" \
|
|
|
|
|
--namespace "$FIXTURE_NAMESPACE" \
|
|
|
|
|
--from-file="profile.env=$profile_file" \
|
|
|
|
|
--dry-run=client \
|
|
|
|
|
--output yaml |
|
|
|
|
|
kube apply --filename - >/dev/null || return
|
|
|
|
|
printf '%s\n' "$profile"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render_lane_job() {
|
|
|
|
|
local job="$1"
|
|
|
|
|
local manifest="$2"
|
|
|
|
|
local image="$3"
|
|
|
|
|
local profile="$4"
|
|
|
|
|
source "$RUNTIME/fixture.env" || return
|
|
|
|
|
local coordination_image="$REDIS_LOCAL_IMAGE"
|
|
|
|
|
sed \
|
|
|
|
|
-e "s|@@JOB_NAME@@|$job|g" \
|
|
|
|
|
-e "s|@@PROFILE_NAME@@|$profile|g" \
|
|
|
|
|
-e "s|@@TEST_IMAGE@@|$image|g" \
|
|
|
|
|
-e "s|@@POSTGRES_IMAGE@@|$POSTGRES_LOCAL_IMAGE|g" \
|
|
|
|
|
-e "s|@@COORDINATION_IMAGE@@|$coordination_image|g" \
|
|
|
|
|
-e "s|@@CURL_IMAGE@@|$CURL_LOCAL_IMAGE|g" \
|
|
|
|
|
-e "s|@@AWS_CLI_IMAGE@@|$AWS_CLI_LOCAL_IMAGE|g" \
|
|
|
|
|
"$manifest" > "$RUNTIME/$job.yaml"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
run_job_manifest() {
|
|
|
|
|
local job="$1"
|
|
|
|
|
local manifest="$2"
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" delete "job/$job" --ignore-not-found >/dev/null || return
|
|
|
|
|
kube apply --filename "$manifest" >/dev/null || return
|
|
|
|
|
wait_for_job "$job"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup_lane_durable_store() {
|
|
|
|
|
local lane="$1"
|
|
|
|
|
local database="nuthatch_lane_$lane"
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/postgres -- \
|
|
|
|
|
env PGPASSWORD=synthetic-postgres psql \
|
|
|
|
|
--username nuthatch \
|
|
|
|
|
--dbname postgres \
|
|
|
|
|
--set ON_ERROR_STOP=1 \
|
|
|
|
|
--command "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$database'" \
|
|
|
|
|
--command "DROP DATABASE IF EXISTS $database" >/dev/null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup_lane_coordination_store() {
|
|
|
|
|
local provider="$1"
|
|
|
|
|
local lane="$2"
|
|
|
|
|
local database=$((10#$lane))
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$provider" -- \
|
|
|
|
|
redis-cli --raw --no-auth-warning -n "$database" FLUSHDB >/dev/null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup_lane_target_store() {
|
|
|
|
|
local provider="$1"
|
|
|
|
|
local bucket="$2"
|
|
|
|
|
local endpoint
|
|
|
|
|
endpoint="$(target_loopback_endpoint "$provider")" || return
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$provider" --container s3-readiness -- \
|
|
|
|
|
aws --endpoint-url "$endpoint" s3 rb "s3://$bucket" --force >/dev/null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup_lane_state() {
|
|
|
|
|
local lane="$1"
|
|
|
|
|
local coordination="$2"
|
|
|
|
|
local target="$4"
|
|
|
|
|
local status=0
|
|
|
|
|
if fixture_has_family durable; then
|
|
|
|
|
cleanup_lane_durable_store "$lane" || status=1
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family coordination; then
|
|
|
|
|
cleanup_lane_coordination_store "$coordination" "$lane" || status=1
|
|
|
|
|
fi
|
|
|
|
|
if fixture_has_family target; then
|
|
|
|
|
cleanup_lane_target_store "$target" "nuthatch-lane-$lane" || status=1
|
|
|
|
|
fi
|
|
|
|
|
return "$status"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup_lane_resources() {
|
|
|
|
|
local job="$1"
|
|
|
|
|
local profile="$2"
|
|
|
|
|
kube --namespace "$FIXTURE_NAMESPACE" delete \
|
|
|
|
|
"job/$job" \
|
|
|
|
|
"configmap/$profile" \
|
|
|
|
|
"configmap/$profile-file" \
|
|
|
|
|
--ignore-not-found >/dev/null || return
|
|
|
|
|
rm -f "$RUNTIME/$profile.env" "$RUNTIME/$job.yaml"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
run_job_matrix() {
|
|
|
|
|
local base_job="$1"
|
|
|
|
|
local manifest="$2"
|
|
|
|
|
local image="$3"
|
|
|
|
|
shift 3
|
|
|
|
|
local lane
|
|
|
|
|
local coordination
|
|
|
|
|
local oidc
|
|
|
|
|
local target
|
|
|
|
|
local profile
|
|
|
|
|
local job
|
|
|
|
|
select_fixture_families "$@" || return
|
|
|
|
|
while IFS=$'\t' read -r lane coordination oidc target; do
|
|
|
|
|
profile="$(write_lane_profile "$lane" "$coordination" "$oidc" "$target")" || return
|
|
|
|
|
job="$base_job-$lane"
|
|
|
|
|
render_lane_job "$job" "$manifest" "$image" "$profile" || return
|
|
|
|
|
printf 'Running lane %s: coordination=%s oidc=%s target=%s\n' \
|
|
|
|
|
"$lane" "$coordination" "$oidc" "$target"
|
|
|
|
|
run_job_manifest "$job" "$RUNTIME/$job.yaml" || return
|
|
|
|
|
cleanup_lane_state "$lane" "$coordination" "$oidc" "$target" || return
|
|
|
|
|
cleanup_lane_resources "$job" "$profile" || return
|
|
|
|
|
done < <(expand_fixture_matrix "$@")
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 18:30:34 +09:00
|
|
|
destroy_fixture() {
|
2026-07-18 00:54:47 +09:00
|
|
|
local status=0
|
|
|
|
|
configure_provider || return
|
|
|
|
|
delete_cluster || status=1
|
|
|
|
|
remove_runtime_namespace || status=1
|
|
|
|
|
rm -rf "$RUNTIME/image-archives" || status=1
|
|
|
|
|
rm -f \
|
|
|
|
|
"$KUBECONFIG_PATH" \
|
|
|
|
|
"$RUNTIME/dependencies.yaml" \
|
|
|
|
|
"$RUNTIME/probe.yaml" \
|
2026-07-18 16:16:24 +09:00
|
|
|
"$RUNTIME/fixture.env" \
|
|
|
|
|
"$RUNTIME"/nuthatch-fixture-lane-*.env \
|
|
|
|
|
"$RUNTIME"/wp05-app-*.yaml || status=1
|
2026-07-18 00:54:47 +09:00
|
|
|
return "$status"
|
2026-07-17 18:30:34 +09:00
|
|
|
}
|