1213 lines
44 KiB
Shell
Executable file
1213 lines
44 KiB
Shell
Executable file
#!/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/remora-$(id -u)}"
|
|
FIXTURE_LOCK_PATH="$FIXTURE_LOCK_DIR/remora-testbed-kind.lock"
|
|
CONTEXT=kind-remora-testbed
|
|
CLUSTER=remora-testbed
|
|
NERDCTL_NAMESPACE=remora-wp02
|
|
FIXTURE_NAMESPACE=remora-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"
|
|
}
|
|
|
|
prepare_provider() {
|
|
configure_provider
|
|
delete_cluster
|
|
remove_runtime_namespace
|
|
"$REMORA_REAL_NERDCTL" namespace create "$NERDCTL_NAMESPACE" >/dev/null
|
|
}
|
|
|
|
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 "${REMORA_REAL_NERDCTL:-}" ]]; then
|
|
REMORA_REAL_NERDCTL="$(command -v nerdctl)"
|
|
fi
|
|
export REMORA_REAL_NERDCTL
|
|
export CONTAINERD_NAMESPACE="$NERDCTL_NAMESPACE"
|
|
export KIND_EXPERIMENTAL_PROVIDER=nerdctl
|
|
export PATH="$TESTBED/provider:$PATH"
|
|
}
|
|
|
|
remove_runtime_namespace() {
|
|
local status=0
|
|
local digests
|
|
local digest
|
|
local leases
|
|
local lease
|
|
if ! runtime_namespace_exists; then
|
|
return
|
|
fi
|
|
"$REMORA_REAL_NERDCTL" --namespace "$NERDCTL_NAMESPACE" image prune --all --force >/dev/null || status=1
|
|
leases="$(runtime_ctr --namespace "$NERDCTL_NAMESPACE" leases list --quiet)" || status=1
|
|
if [[ -n "$leases" ]]; then
|
|
while read -r lease; do
|
|
runtime_ctr --namespace "$NERDCTL_NAMESPACE" leases delete "$lease" >/dev/null || status=1
|
|
done <<< "$leases"
|
|
fi
|
|
digests="$(runtime_ctr --namespace "$NERDCTL_NAMESPACE" content list --quiet)" || status=1
|
|
if [[ -n "$digests" ]]; then
|
|
while read -r digest; do
|
|
runtime_ctr --namespace "$NERDCTL_NAMESPACE" content remove "$digest" >/dev/null || status=1
|
|
done <<< "$digests"
|
|
fi
|
|
"$REMORA_REAL_NERDCTL" namespace remove "$NERDCTL_NAMESPACE" >/dev/null || status=1
|
|
if runtime_namespace_exists; then
|
|
printf 'containerd namespace %s still exists after teardown\n' "$NERDCTL_NAMESPACE" >&2
|
|
status=1
|
|
fi
|
|
return "$status"
|
|
}
|
|
|
|
runtime_ctr() {
|
|
ctr --address "${CONTAINERD_ADDRESS:-${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/containerd/containerd.sock}" "$@"
|
|
}
|
|
|
|
runtime_namespace_exists() {
|
|
"$REMORA_REAL_NERDCTL" namespace list --quiet | grep -Fx "$NERDCTL_NAMESPACE" >/dev/null
|
|
}
|
|
|
|
cluster_exists() {
|
|
if ! runtime_namespace_exists; then
|
|
return 1
|
|
fi
|
|
"$BIN/kind" get clusters | grep -Fx "$CLUSTER" >/dev/null
|
|
}
|
|
|
|
delete_cluster() {
|
|
if cluster_exists; then
|
|
"$BIN/kind" delete cluster --name "$CLUSTER"
|
|
fi
|
|
if cluster_exists; then
|
|
printf 'kind cluster %s still exists after teardown\n' "$CLUSTER" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
load_external_image() {
|
|
local name="$1"
|
|
local source="$2"
|
|
local digest="${source##*@}"
|
|
local image="remora-testbed/$name:sha256-${digest#sha256:}"
|
|
local archive="$RUNTIME/image-archives/$digest.tar"
|
|
"$REMORA_REAL_NERDCTL" image pull --platform linux/amd64 --unpack=false --quiet "$source" >&2 || return 1
|
|
"$REMORA_REAL_NERDCTL" image tag "$source" "$image" || return 1
|
|
"$REMORA_REAL_NERDCTL" image save --platform linux/amd64 --output "$archive" "$image" || return 1
|
|
printf 'Loading %s from %s\n' "$image" "$source" >&2
|
|
"$BIN/kind" load image-archive "$archive" --name "$CLUSTER" >&2 || return 1
|
|
printf '%s\n' "$image"
|
|
}
|
|
|
|
load_local_image() {
|
|
local image="$1"
|
|
local namespace="${2:-default}"
|
|
local archive="$RUNTIME/image-archives/package-test.tar"
|
|
rm -f "$archive"
|
|
"$REMORA_REAL_NERDCTL" --namespace "$namespace" image save \
|
|
--platform linux/amd64 \
|
|
--output "$archive" \
|
|
"$image"
|
|
"$BIN/kind" load image-archive "$archive" --name "$CLUSTER"
|
|
}
|
|
|
|
render_manifests() {
|
|
local postgres_image="$1"
|
|
local redis_image="$2"
|
|
local dragonfly_image="$3"
|
|
local vault_image="$4"
|
|
local keycloak_image="$5"
|
|
local zitadel_image="$6"
|
|
local garage_image="$7"
|
|
local versitygw_image="$8"
|
|
local coredns_image="$9"
|
|
local curl_image="${10}"
|
|
local aws_cli_image="${11}"
|
|
local postgres_replicas="${12}"
|
|
local redis_replicas="${13}"
|
|
local dragonfly_replicas="${14}"
|
|
local vault_replicas="${15}"
|
|
local keycloak_replicas="${16}"
|
|
local zitadel_replicas="${17}"
|
|
local garage_replicas="${18}"
|
|
local versitygw_replicas="${19}"
|
|
local coredns_replicas="${20}"
|
|
sed \
|
|
-e "s|@@POSTGRES_IMAGE@@|$postgres_image|g" \
|
|
-e "s|@@REDIS_IMAGE@@|$redis_image|g" \
|
|
-e "s|@@DRAGONFLY_IMAGE@@|$dragonfly_image|g" \
|
|
-e "s|@@VAULT_IMAGE@@|$vault_image|g" \
|
|
-e "s|@@KEYCLOAK_IMAGE@@|$keycloak_image|g" \
|
|
-e "s|@@ZITADEL_IMAGE@@|$zitadel_image|g" \
|
|
-e "s|@@GARAGE_IMAGE@@|$garage_image|g" \
|
|
-e "s|@@VERSITYGW_IMAGE@@|$versitygw_image|g" \
|
|
-e "s|@@COREDNS_IMAGE@@|$coredns_image|g" \
|
|
-e "s|@@CURL_IMAGE@@|$curl_image|g" \
|
|
-e "s|@@AWS_CLI_IMAGE@@|$aws_cli_image|g" \
|
|
-e "s|@@POSTGRES_REPLICAS@@|$postgres_replicas|g" \
|
|
-e "s|@@REDIS_REPLICAS@@|$redis_replicas|g" \
|
|
-e "s|@@DRAGONFLY_REPLICAS@@|$dragonfly_replicas|g" \
|
|
-e "s|@@VAULT_REPLICAS@@|$vault_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" \
|
|
"$TESTBED/dependencies.yaml.in" > "$RUNTIME/dependencies.yaml"
|
|
sed \
|
|
-e "s|@@POSTGRES_IMAGE@@|$postgres_image|g" \
|
|
-e "s|@@REDIS_IMAGE@@|$redis_image|g" \
|
|
-e "s|@@VAULT_IMAGE@@|$vault_image|g" \
|
|
-e "s|@@CURL_IMAGE@@|$curl_image|g" \
|
|
-e "s|@@AWS_CLI_IMAGE@@|$aws_cli_image|g" \
|
|
"$TESTBED/probe.yaml.in" > "$RUNTIME/probe.yaml"
|
|
{
|
|
printf 'POSTGRES_LOCAL_IMAGE=%q\n' "$postgres_image"
|
|
printf 'REDIS_LOCAL_IMAGE=%q\n' "$redis_image"
|
|
printf 'DRAGONFLY_LOCAL_IMAGE=%q\n' "$dragonfly_image"
|
|
printf 'VAULT_LOCAL_IMAGE=%q\n' "$vault_image"
|
|
printf 'KEYCLOAK_LOCAL_IMAGE=%q\n' "$keycloak_image"
|
|
printf 'ZITADEL_LOCAL_IMAGE=%q\n' "$zitadel_image"
|
|
printf 'GARAGE_LOCAL_IMAGE=%q\n' "$garage_image"
|
|
printf 'VERSITYGW_LOCAL_IMAGE=%q\n' "$versitygw_image"
|
|
printf 'COREDNS_LOCAL_IMAGE=%q\n' "$coredns_image"
|
|
printf 'CURL_LOCAL_IMAGE=%q\n' "$curl_image"
|
|
printf 'AWS_CLI_LOCAL_IMAGE=%q\n' "$aws_cli_image"
|
|
} > "$RUNTIME/fixture.env"
|
|
}
|
|
|
|
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 | secret) ;;
|
|
*)
|
|
printf 'unknown fixture family: %s\n' "$family" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
done
|
|
if [[ "$use_all" = true ]]; then
|
|
requested=(durable coordination oidc target secret)
|
|
fi
|
|
for candidate in durable coordination oidc target secret; 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
|
|
}
|
|
|
|
load_selected_image() {
|
|
local family="$1"
|
|
local variable="$2"
|
|
local name="$3"
|
|
local source="$4"
|
|
local image
|
|
if fixture_has_family "$family"; then
|
|
image="$(load_external_image "$name" "$source")" || return
|
|
printf -v "$variable" '%s' "$image"
|
|
fi
|
|
}
|
|
|
|
setup_fixture() {
|
|
select_fixture_families "$@" || return
|
|
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
|
|
|
|
POSTGRES_LOCAL_IMAGE=remora-testbed/unused:postgres
|
|
REDIS_LOCAL_IMAGE=remora-testbed/unused:redis
|
|
DRAGONFLY_LOCAL_IMAGE=remora-testbed/unused:dragonfly
|
|
VAULT_LOCAL_IMAGE=remora-testbed/unused:vault
|
|
KEYCLOAK_LOCAL_IMAGE=remora-testbed/unused:keycloak
|
|
ZITADEL_LOCAL_IMAGE=remora-testbed/unused:zitadel
|
|
GARAGE_LOCAL_IMAGE=remora-testbed/unused:garage
|
|
VERSITYGW_LOCAL_IMAGE=remora-testbed/unused:versitygw
|
|
COREDNS_LOCAL_IMAGE=remora-testbed/unused:coredns
|
|
CURL_LOCAL_IMAGE=remora-testbed/unused:curl
|
|
AWS_CLI_LOCAL_IMAGE=remora-testbed/unused:aws-cli
|
|
if fixture_has_family durable || fixture_has_family oidc; then
|
|
POSTGRES_LOCAL_IMAGE="$(load_external_image postgres "$POSTGRES_IMAGE")"
|
|
fi
|
|
load_selected_image coordination REDIS_LOCAL_IMAGE redis "$REDIS_IMAGE"
|
|
load_selected_image coordination DRAGONFLY_LOCAL_IMAGE dragonfly "$DRAGONFLY_IMAGE"
|
|
load_selected_image secret VAULT_LOCAL_IMAGE vault "$VAULT_IMAGE"
|
|
load_selected_image oidc KEYCLOAK_LOCAL_IMAGE keycloak "$KEYCLOAK_IMAGE"
|
|
load_selected_image oidc ZITADEL_LOCAL_IMAGE zitadel "$ZITADEL_IMAGE"
|
|
load_selected_image target GARAGE_LOCAL_IMAGE garage "$GARAGE_IMAGE"
|
|
load_selected_image target VERSITYGW_LOCAL_IMAGE versitygw "$VERSITYGW_IMAGE"
|
|
load_selected_image oidc COREDNS_LOCAL_IMAGE coredns "$COREDNS_IMAGE"
|
|
if fixture_has_family oidc || fixture_has_family target; then
|
|
CURL_LOCAL_IMAGE="$(load_external_image curl "$CURL_IMAGE")"
|
|
fi
|
|
load_selected_image target AWS_CLI_LOCAL_IMAGE aws-cli "$AWS_CLI_IMAGE"
|
|
|
|
POSTGRES_REPLICAS=0
|
|
REDIS_REPLICAS=0
|
|
DRAGONFLY_REPLICAS=0
|
|
VAULT_REPLICAS=0
|
|
KEYCLOAK_REPLICAS=0
|
|
ZITADEL_REPLICAS=0
|
|
GARAGE_REPLICAS=0
|
|
VERSITYGW_REPLICAS=0
|
|
COREDNS_REPLICAS=0
|
|
if fixture_has_family durable || fixture_has_family oidc; then POSTGRES_REPLICAS=1; fi
|
|
if fixture_has_family coordination; then REDIS_REPLICAS=1; DRAGONFLY_REPLICAS=1; fi
|
|
if fixture_has_family secret; then VAULT_REPLICAS=1; fi
|
|
if fixture_has_family oidc; then KEYCLOAK_REPLICAS=1; ZITADEL_REPLICAS=1; COREDNS_REPLICAS=1; fi
|
|
if fixture_has_family target; then GARAGE_REPLICAS=1; VERSITYGW_REPLICAS=1; fi
|
|
render_manifests \
|
|
"$POSTGRES_LOCAL_IMAGE" \
|
|
"$REDIS_LOCAL_IMAGE" \
|
|
"$DRAGONFLY_LOCAL_IMAGE" \
|
|
"$VAULT_LOCAL_IMAGE" \
|
|
"$KEYCLOAK_LOCAL_IMAGE" \
|
|
"$ZITADEL_LOCAL_IMAGE" \
|
|
"$GARAGE_LOCAL_IMAGE" \
|
|
"$VERSITYGW_LOCAL_IMAGE" \
|
|
"$COREDNS_LOCAL_IMAGE" \
|
|
"$CURL_LOCAL_IMAGE" \
|
|
"$AWS_CLI_LOCAL_IMAGE" \
|
|
"$POSTGRES_REPLICAS" \
|
|
"$REDIS_REPLICAS" \
|
|
"$DRAGONFLY_REPLICAS" \
|
|
"$VAULT_REPLICAS" \
|
|
"$KEYCLOAK_REPLICAS" \
|
|
"$ZITADEL_REPLICAS" \
|
|
"$GARAGE_REPLICAS" \
|
|
"$VERSITYGW_REPLICAS" \
|
|
"$COREDNS_REPLICAS"
|
|
|
|
apply_fixture_component common
|
|
if fixture_has_family durable || fixture_has_family oidc; then apply_fixture_component postgres; fi
|
|
if fixture_has_family coordination; then apply_fixture_component redis; apply_fixture_component dragonfly; fi
|
|
if fixture_has_family secret; then apply_fixture_component vault; fi
|
|
if fixture_has_family oidc; then apply_fixture_component keycloak; apply_fixture_component zitadel; apply_fixture_component controlled-dns; fi
|
|
if fixture_has_family target; then apply_fixture_component garage; apply_fixture_component versitygw; fi
|
|
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 secret; then wait_for_deployment vault; 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
|
|
}
|
|
|
|
apply_fixture_component() {
|
|
kube apply \
|
|
--filename "$RUNTIME/dependencies.yaml" \
|
|
--selector "fixture.remora.dev/component=$1"
|
|
}
|
|
|
|
wait_for_deployment() {
|
|
kube --namespace "$FIXTURE_NAMESPACE" rollout status "deployment/$1" --timeout 240s
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
reset_lane_durable_store() {
|
|
local lane="$1"
|
|
local database="remora_lane_$lane"
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/postgres -- \
|
|
env PGPASSWORD=synthetic-postgres psql \
|
|
--username remora \
|
|
--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_target_store() {
|
|
local provider="$1"
|
|
local bucket="$2"
|
|
local deployment
|
|
local endpoint
|
|
if [[ "$provider" = garage ]]; then
|
|
deployment=garage
|
|
endpoint=http://127.0.0.1:3900
|
|
else
|
|
deployment=versitygw
|
|
endpoint=http://127.0.0.1:10000
|
|
fi
|
|
if kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$deployment" --container s3-readiness -- \
|
|
aws --endpoint-url "$endpoint" s3api head-bucket --bucket "$bucket" >/dev/null 2>&1; then
|
|
cleanup_lane_target_store "$provider" "$bucket" || return
|
|
fi
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$deployment" --container s3-readiness -- \
|
|
aws --endpoint-url "$endpoint" s3api create-bucket --bucket "$bucket" >&2 || return
|
|
}
|
|
|
|
# Remora storage role for fixture lanes: target-main / target-bucket / models.
|
|
remora_fixture_storage_role() {
|
|
local b64url
|
|
b64url() {
|
|
printf '%s' "$1" | openssl base64 -A | tr '+/' '-_' | tr -d '='
|
|
}
|
|
printf 'remora:v1:storage|%s|%s|%s\n' \
|
|
"$(b64url target-main)" \
|
|
"$(b64url target-bucket)" \
|
|
"$(b64url models)"
|
|
}
|
|
|
|
reset_keycloak_lane() {
|
|
local lane="$1"
|
|
local realm="remora-lane-$lane"
|
|
local username="remora-user-$lane"
|
|
local client_id="remora-client-$lane"
|
|
local storage_role
|
|
local client_uuid
|
|
storage_role="$(remora_fixture_storage_role)"
|
|
if kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh get "realms/$realm" \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak >/dev/null 2>&1; then
|
|
cleanup_keycloak_lane "$lane" || return
|
|
fi
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh create realms \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
--set "realm=$realm" \
|
|
--set enabled=true \
|
|
--set registrationEmailAsUsername=false \
|
|
--set verifyEmail=false \
|
|
--set resetPasswordAllowed=false \
|
|
--set loginWithEmailAllowed=true \
|
|
--set duplicateEmailsAllowed=false \
|
|
--set sslRequired=none >&2 || return
|
|
# Keycloak 26 enables VERIFY_PROFILE by default; that blocks ROPC with
|
|
# "Account is not fully set up".
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh update authentication/required-actions/VERIFY_PROFILE \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--set enabled=false \
|
|
--set defaultAction=false >&2 || true
|
|
# Public client with direct access grants so package Jobs can mint ROPC tokens.
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh create clients \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--set "clientId=$client_id" \
|
|
--set publicClient=true \
|
|
--set standardFlowEnabled=true \
|
|
--set directAccessGrantsEnabled=true \
|
|
--set 'redirectUris=["http://localhost/callback"]' \
|
|
--set 'attributes={"access.token.lifespan":"3600"}' >&2 || return
|
|
client_uuid="$(kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh get clients \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--query "clientId=$client_id" \
|
|
--fields id \
|
|
--format csv \
|
|
--noquotes)" || return
|
|
client_uuid="$(printf '%s' "$client_uuid" | tr -d '\r' | head -n1)"
|
|
[[ -n "$client_uuid" ]] || return
|
|
# Include this client in access-token `aud` so mandatory audience checks pass.
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh create "clients/$client_uuid/protocol-mappers/models" \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--set name=remora-audience \
|
|
--set protocol=openid-connect \
|
|
--set protocolMapper=oidc-audience-mapper \
|
|
--set 'config."included.client.audience"='"$client_id" \
|
|
--set 'config."id.token.claim"=false' \
|
|
--set 'config."access.token.claim"=true' >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh create roles \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--set name=test-user >&2 || return
|
|
# Client roles are the only Keycloak claim source Remora trusts (resource_access).
|
|
# Use -b JSON so `|` in storage roles is not percent-encoded by --set.
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh create "clients/$client_uuid/roles" \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
-b "{\"name\":\"remora:v1:admin\"}" >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh create "clients/$client_uuid/roles" \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
-b "{\"name\":\"$storage_role\"}" >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh create users \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--set "username=$username" \
|
|
--set enabled=true \
|
|
--set emailVerified=true \
|
|
--set "email=$username@remora.test" \
|
|
--set 'requiredActions=[]' >&2 || return
|
|
local user_id
|
|
user_id="$(kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh get users \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--query "username=$username" \
|
|
--fields id \
|
|
--format csv \
|
|
--noquotes)" || return
|
|
user_id="$(printf '%s' "$user_id" | tr -d '\r' | head -n1)"
|
|
[[ -n "$user_id" ]] || return
|
|
# Clear any required actions and mark email verified for ROPC.
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh update "users/$user_id" \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--set emailVerified=true \
|
|
--set 'requiredActions=[]' \
|
|
--set enabled=true >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh set-password \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--userid "$user_id" \
|
|
--new-password Synthetic-OIDC-1! \
|
|
--temporary=false >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh add-roles \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--uusername "$username" \
|
|
--rolename test-user >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh add-roles \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak \
|
|
-r "$realm" \
|
|
--uusername "$username" \
|
|
--cclientid "$client_id" \
|
|
--rolename 'remora:v1:admin' \
|
|
--rolename "$storage_role" >&2 || return
|
|
# issuer \t tenant \t client_id \t username \t storage_role \t project_id \t user_id \t mgmt_token
|
|
# issuer \t tenant \t client_id \t username \t storage_role \t project_id \t user_id \t mgmt_token \t user_key_b64
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
"http://keycloak.remora.test:8080/realms/$realm" \
|
|
"$realm" \
|
|
"$client_id" \
|
|
"$username" \
|
|
"$storage_role" \
|
|
"" \
|
|
"" \
|
|
"" \
|
|
""
|
|
}
|
|
|
|
zitadel_request() {
|
|
local endpoint="$1"
|
|
local body="$2"
|
|
local organization="${3:-}"
|
|
local method="${4:-POST}"
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/zitadel --container provisioner -- \
|
|
/bin/sh -ec '
|
|
endpoint="$1"
|
|
body="$2"
|
|
organization="$3"
|
|
method="$4"
|
|
token="$(cat /bootstrap/admin.pat)"
|
|
set -- \
|
|
--fail \
|
|
--silent \
|
|
--show-error \
|
|
--request "$method" \
|
|
--header "Host: zitadel.remora.test:8080" \
|
|
--header "Authorization: Bearer $token" \
|
|
--header "Content-Type: application/json"
|
|
if test -n "$organization"; then
|
|
set -- "$@" --header "x-zitadel-orgid: $organization"
|
|
fi
|
|
curl "$@" --data "$body" "http://127.0.0.1:8080$endpoint"
|
|
' remora-zitadel-request "$endpoint" "$body" "$organization" "$method"
|
|
}
|
|
|
|
json_string_field() {
|
|
local document="$1"
|
|
local field="$2"
|
|
local value
|
|
value="$(sed -nE 's/.*"'"$field"'"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' <<< "$document")"
|
|
if [[ -z "$value" ]]; then
|
|
printf 'ZITADEL response omitted %s\n' "$field" >&2
|
|
return 1
|
|
fi
|
|
printf '%s\n' "$value"
|
|
}
|
|
|
|
reset_zitadel_lane() {
|
|
local lane="$1"
|
|
local tenant="remora-lane-$lane"
|
|
local username="remora-user-$lane"
|
|
local storage_role
|
|
local response
|
|
local organization_id
|
|
local project_id
|
|
local user_id
|
|
local client_id
|
|
local mgmt_token
|
|
local grant_id
|
|
storage_role="$(remora_fixture_storage_role)"
|
|
cleanup_zitadel_lane "$lane" || return
|
|
response="$(zitadel_request /management/v1/orgs "{\"name\":\"$tenant\"}")" || return
|
|
organization_id="$(json_string_field "$response" id)" || return
|
|
response="$(zitadel_request /management/v1/projects "{\"name\":\"$tenant\",\"projectRoleAssertion\":true,\"projectRoleCheck\":true}" "$organization_id")" || return
|
|
project_id="$(json_string_field "$response" id)" || return
|
|
zitadel_request \
|
|
"/management/v1/projects/$project_id/roles" \
|
|
'{"roleKey":"test-user","displayName":"Test User","group":"fixture"}' \
|
|
"$organization_id" >/dev/null || return
|
|
zitadel_request \
|
|
"/management/v1/projects/$project_id/roles" \
|
|
'{"roleKey":"remora:v1:admin","displayName":"Remora Admin","group":"remora"}' \
|
|
"$organization_id" >/dev/null || return
|
|
zitadel_request \
|
|
"/management/v1/projects/$project_id/roles" \
|
|
"{\"roleKey\":\"$storage_role\",\"displayName\":\"Remora Storage\",\"group\":\"remora\"}" \
|
|
"$organization_id" >/dev/null || return
|
|
# Native app: PKCE public client with JWT access tokens and project role assertion.
|
|
response="$(zitadel_request \
|
|
"/management/v1/projects/$project_id/apps/oidc" \
|
|
"{\"name\":\"$tenant\",\"redirectUris\":[\"http://localhost/callback\"],\"postLogoutRedirectUris\":[\"http://localhost/logout\"],\"responseTypes\":[\"OIDC_RESPONSE_TYPE_CODE\"],\"grantTypes\":[\"OIDC_GRANT_TYPE_AUTHORIZATION_CODE\",\"OIDC_GRANT_TYPE_REFRESH_TOKEN\"],\"appType\":\"OIDC_APP_TYPE_NATIVE\",\"authMethodType\":\"OIDC_AUTH_METHOD_TYPE_NONE\",\"version\":\"OIDC_VERSION_1_0\",\"devMode\":true,\"accessTokenType\":\"OIDC_TOKEN_TYPE_JWT\",\"accessTokenRoleAssertion\":true,\"idTokenRoleAssertion\":true,\"idTokenUserinfoAssertion\":true}" \
|
|
"$organization_id")" || return
|
|
client_id="$(json_string_field "$response" clientId)" || return
|
|
response="$(zitadel_request \
|
|
/management/v1/users/human \
|
|
"{\"userName\":\"$username\",\"profile\":{\"firstName\":\"Remora\",\"lastName\":\"Lane $lane\",\"displayName\":\"Remora Lane $lane\",\"preferredLanguage\":\"en\",\"gender\":\"GENDER_UNSPECIFIED\"},\"email\":{\"email\":\"$username@remora.test\",\"isEmailVerified\":true},\"initialPassword\":\"Synthetic-OIDC-1!\"}" \
|
|
"$organization_id")" || return
|
|
user_id="$(json_string_field "$response" userId)" || return
|
|
# Set a permanent password for Session API checks (initialPassword alone is not enough on all versions).
|
|
if ! zitadel_request \
|
|
"/management/v1/users/$user_id/password" \
|
|
'{"password":"Synthetic-OIDC-1!","noChangeRequired":true}' \
|
|
"$organization_id" >/dev/null; then
|
|
# Fallback field name used by older management API payloads.
|
|
zitadel_request \
|
|
"/management/v1/users/$user_id/password" \
|
|
'{"password":"Synthetic-OIDC-1!","changeRequired":false}' \
|
|
"$organization_id" >/dev/null || return
|
|
fi
|
|
response="$(zitadel_request \
|
|
"/management/v1/users/$user_id/grants" \
|
|
"{\"projectId\":\"$project_id\",\"roleKeys\":[\"test-user\",\"remora:v1:admin\",\"$storage_role\"]}" \
|
|
"$organization_id")" || return
|
|
# Machine user + application key for JWT-bearer access tokens with project roles.
|
|
local machine_name="remora-machine-$lane"
|
|
# Bearer defaults to opaque tokens; Remora validates only JWKS-signed JWTs.
|
|
response="$(zitadel_request \
|
|
/management/v1/users/machine \
|
|
"{\"userName\":\"$machine_name\",\"name\":\"Remora Machine $lane\",\"description\":\"JWT bearer minting\",\"accessTokenType\":\"ACCESS_TOKEN_TYPE_JWT\"}" \
|
|
"$organization_id")" || return
|
|
local machine_id
|
|
machine_id="$(json_string_field "$response" userId)" || return
|
|
zitadel_request \
|
|
"/management/v1/users/$machine_id/grants" \
|
|
"{\"projectId\":\"$project_id\",\"roleKeys\":[\"remora:v1:admin\",\"$storage_role\"]}" \
|
|
"$organization_id" >/dev/null || return
|
|
response="$(zitadel_request \
|
|
"/management/v1/users/$machine_id/keys" \
|
|
'{"type":"KEY_TYPE_JSON","expirationDate":"2099-01-01T00:00:00Z"}' \
|
|
"$organization_id")" || return
|
|
local key_details_b64
|
|
key_details_b64="$(json_string_field "$response" keyDetails)" || return
|
|
mgmt_token="$(kube --namespace "$FIXTURE_NAMESPACE" exec deployment/zitadel --container provisioner -- \
|
|
cat /bootstrap/admin.pat)" || return
|
|
mgmt_token="$(printf '%s' "$mgmt_token" | tr -d '\r\n')"
|
|
# issuer \t tenant \t client_id \t username \t storage_role \t project_id \t user_id \t mgmt_token \t key_details_b64
|
|
# user_id is the machine user used for JWT-bearer minting (and grant loss).
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
http://zitadel.remora.test:8080 \
|
|
"$organization_id" \
|
|
"$client_id" \
|
|
"$username" \
|
|
"$storage_role" \
|
|
"$project_id" \
|
|
"$machine_id" \
|
|
"$mgmt_token" \
|
|
"$key_details_b64"
|
|
}
|
|
|
|
reset_lane_oidc_provider() {
|
|
local provider="$1"
|
|
local lane="$2"
|
|
if [[ "$provider" = keycloak ]]; then
|
|
reset_keycloak_lane "$lane"
|
|
else
|
|
reset_zitadel_lane "$lane"
|
|
fi
|
|
}
|
|
|
|
cleanup_lane_durable_store() {
|
|
local database="remora_lane_$1"
|
|
local remaining
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/postgres -- \
|
|
env PGPASSWORD=synthetic-postgres psql \
|
|
--username remora \
|
|
--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" >&2 || return
|
|
remaining="$(kube --namespace "$FIXTURE_NAMESPACE" exec deployment/postgres -- \
|
|
env PGPASSWORD=synthetic-postgres psql \
|
|
--username remora \
|
|
--dbname postgres \
|
|
--tuples-only \
|
|
--no-align \
|
|
--command "SELECT count(*) FROM pg_database WHERE datname = '$database'")" || return
|
|
[[ "$remaining" = 0 ]]
|
|
}
|
|
|
|
cleanup_lane_coordination_store() {
|
|
local provider="$1"
|
|
local namespace="$2"
|
|
local keys
|
|
local key
|
|
while true; do
|
|
keys="$(kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$provider" -- \
|
|
redis-cli --scan --pattern "$namespace*")" || return
|
|
[[ -z "$keys" ]] && return
|
|
while IFS= read -r key; do
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$provider" -- \
|
|
redis-cli DEL "$key" >/dev/null || return
|
|
done <<< "$keys"
|
|
done
|
|
}
|
|
|
|
cleanup_lane_target_store() {
|
|
local provider="$1"
|
|
local bucket="$2"
|
|
local deployment
|
|
local endpoint
|
|
if [[ "$provider" = garage ]]; then
|
|
deployment=garage
|
|
endpoint=http://127.0.0.1:3900
|
|
else
|
|
deployment=versitygw
|
|
endpoint=http://127.0.0.1:10000
|
|
fi
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$deployment" --container s3-readiness -- \
|
|
aws --endpoint-url "$endpoint" s3 rm "s3://$bucket" --recursive >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$deployment" --container s3-readiness -- \
|
|
aws --endpoint-url "$endpoint" s3api delete-bucket --bucket "$bucket" >&2 || return
|
|
if kube --namespace "$FIXTURE_NAMESPACE" exec "deployment/$deployment" --container s3-readiness -- \
|
|
aws --endpoint-url "$endpoint" s3api head-bucket --bucket "$bucket" >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
cleanup_keycloak_lane() {
|
|
local realm="remora-lane-$1"
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
|
|
/opt/keycloak/bin/kcadm.sh delete "realms/$realm" \
|
|
--no-config \
|
|
--server http://127.0.0.1:8080 \
|
|
--realm master \
|
|
--user remora-admin \
|
|
--password synthetic-keycloak >&2 || return
|
|
}
|
|
|
|
find_zitadel_lane_organizations() {
|
|
local lane="$1"
|
|
local tenant="remora-lane-$lane"
|
|
local response
|
|
response="$(zitadel_request /v2/organizations/_search '{}')" || return
|
|
jq --raw-output --arg tenant "$tenant" \
|
|
'.result[]? | select(.name == $tenant) | .id' <<< "$response"
|
|
}
|
|
|
|
cleanup_zitadel_lane() {
|
|
local lane="$1"
|
|
local organization_ids
|
|
local organization_id
|
|
local status=0
|
|
organization_ids="$(find_zitadel_lane_organizations "$lane")" || return
|
|
while IFS= read -r organization_id; do
|
|
[[ -z "$organization_id" ]] && continue
|
|
zitadel_request "/v2/organizations/$organization_id" '' '' DELETE >/dev/null || status=1
|
|
done <<< "$organization_ids"
|
|
return "$status"
|
|
}
|
|
|
|
cleanup_lane_secret_store() {
|
|
local path="secret/remora/lane-$1"
|
|
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/vault -- \
|
|
env VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=synthetic-vault-token \
|
|
vault kv metadata delete "$path" >/dev/null || return
|
|
}
|
|
|
|
cleanup_lane_state() {
|
|
local lane="$1"
|
|
local coordination="$2"
|
|
local oidc="$3"
|
|
local target="$4"
|
|
local status=0
|
|
if fixture_has_family durable && ! cleanup_lane_durable_store "$lane"; then
|
|
printf 'durable store cleanup failed for lane %s\n' "$lane" >&2
|
|
status=1
|
|
fi
|
|
if [[ "$coordination" != - ]] && ! cleanup_lane_coordination_store "$coordination" "remora:lane:$lane:"; then
|
|
printf 'coordination store cleanup failed for lane %s\n' "$lane" >&2
|
|
status=1
|
|
fi
|
|
if [[ "$oidc" = keycloak ]] && ! cleanup_keycloak_lane "$lane"; then
|
|
printf 'Keycloak cleanup failed for lane %s\n' "$lane" >&2
|
|
status=1
|
|
fi
|
|
if [[ "$oidc" = zitadel ]] && ! cleanup_zitadel_lane "$lane"; then
|
|
printf 'ZITADEL cleanup failed for lane %s\n' "$lane" >&2
|
|
status=1
|
|
fi
|
|
if [[ "$target" != - ]] && ! cleanup_lane_target_store "$target" "remora-lane-$lane"; then
|
|
printf 'target store cleanup failed for lane %s\n' "$lane" >&2
|
|
status=1
|
|
fi
|
|
if fixture_has_family secret && ! cleanup_lane_secret_store "$lane"; then
|
|
printf 'secret store cleanup failed for lane %s\n' "$lane" >&2
|
|
status=1
|
|
fi
|
|
return "$status"
|
|
}
|
|
|
|
write_lane_profile() {
|
|
local lane="$1"
|
|
local coordination="$2"
|
|
local oidc="$3"
|
|
local target="$4"
|
|
local profile_name="remora-fixture-lane-$lane"
|
|
local database=
|
|
local database_url=
|
|
local coordination_url=
|
|
local coordination_namespace=
|
|
local oidc_issuer=
|
|
local oidc_tenant=
|
|
local oidc_client_id=
|
|
local oidc_username=
|
|
local oidc_storage_role=
|
|
local oidc_project_id=
|
|
local oidc_user_id=
|
|
local oidc_mgmt_token=
|
|
local oidc_user_key_b64=
|
|
local oidc_profile
|
|
local target_endpoint=
|
|
local target_bucket=
|
|
local target_prefix=
|
|
local target_access_key=
|
|
local target_secret_key=
|
|
local target_region=
|
|
local vault_path=
|
|
local profile
|
|
if fixture_has_family durable; then
|
|
database="$(reset_lane_durable_store "$lane")" || return
|
|
database_url="postgres://remora:synthetic-postgres@postgres:5432/$database?sslmode=disable"
|
|
fi
|
|
if [[ "$coordination" != - ]]; then
|
|
coordination_url="redis://$coordination:6379"
|
|
coordination_namespace="remora:lane:$lane:"
|
|
cleanup_lane_coordination_store "$coordination" "$coordination_namespace" || return
|
|
fi
|
|
if [[ "$oidc" != - ]]; then
|
|
oidc_profile="$(reset_lane_oidc_provider "$oidc" "$lane")" || return
|
|
IFS=$'\t' read -r oidc_issuer oidc_tenant oidc_client_id oidc_username \
|
|
oidc_storage_role oidc_project_id oidc_user_id oidc_mgmt_token oidc_user_key_b64 \
|
|
<<< "$oidc_profile"
|
|
fi
|
|
if [[ "$target" != - ]]; then
|
|
target_bucket="remora-lane-$lane"
|
|
target_prefix="lane-$lane/"
|
|
reset_lane_target_store "$target" "$target_bucket" || return
|
|
if [[ "$target" = garage ]]; then
|
|
target_endpoint=http://garage:3900
|
|
target_access_key=GK00000000000000000000000000000000
|
|
target_secret_key=0000000000000000000000000000000000000000000000000000000000000000
|
|
target_region=garage
|
|
else
|
|
target_endpoint=http://versitygw:10000
|
|
target_access_key=synthetic-s3-access
|
|
target_secret_key=synthetic-s3-secret
|
|
target_region=us-east-1
|
|
fi
|
|
fi
|
|
if fixture_has_family secret; then
|
|
vault_path="secret/remora/lane-$lane"
|
|
cleanup_lane_secret_store "$lane" || return
|
|
fi
|
|
printf -v profile '%s\n' \
|
|
"REMORA_FIXTURE_LANE=$lane" \
|
|
"REMORA_COORDINATION_PROVIDER=$coordination" \
|
|
"REMORA_COORDINATION_URL=$coordination_url" \
|
|
"REMORA_COORDINATION_NAMESPACE=$coordination_namespace" \
|
|
"REMORA_OIDC_PROVIDER=$oidc" \
|
|
"REMORA_OIDC_ISSUER=$oidc_issuer" \
|
|
"REMORA_OIDC_TENANT=$oidc_tenant" \
|
|
"REMORA_OIDC_CLIENT_ID=$oidc_client_id" \
|
|
"REMORA_OIDC_USERNAME=$oidc_username" \
|
|
"REMORA_OIDC_PASSWORD=Synthetic-OIDC-1!" \
|
|
"REMORA_OIDC_STORAGE_ROLE=$oidc_storage_role" \
|
|
"REMORA_OIDC_PROJECT_ID=$oidc_project_id" \
|
|
"REMORA_OIDC_USER_ID=$oidc_user_id" \
|
|
"REMORA_OIDC_MGMT_TOKEN=$oidc_mgmt_token" \
|
|
"REMORA_OIDC_USER_KEY_B64=$oidc_user_key_b64" \
|
|
"REMORA_TARGET_PROVIDER=$target" \
|
|
"REMORA_TARGET_ENDPOINT=$target_endpoint" \
|
|
"REMORA_TARGET_BUCKET=$target_bucket" \
|
|
"REMORA_TARGET_PREFIX=$target_prefix" \
|
|
"REMORA_TARGET_ACCESS_KEY=$target_access_key" \
|
|
"REMORA_TARGET_SECRET_KEY=$target_secret_key" \
|
|
"REMORA_TARGET_REGION=$target_region" \
|
|
"REMORA_TARGET_EXCLUSIVE_WRITER=true" \
|
|
"REMORA_VAULT_ADDR=http://vault:8200" \
|
|
"REMORA_VAULT_TOKEN=synthetic-vault-token" \
|
|
"REMORA_VAULT_PATH=$vault_path" \
|
|
"DATABASE_URL=$database_url"
|
|
kube --namespace "$FIXTURE_NAMESPACE" delete \
|
|
"configmap/$profile_name" \
|
|
"configmap/$profile_name-file" \
|
|
--ignore-not-found \
|
|
--wait=true >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" create configmap "$profile_name" \
|
|
--from-literal="REMORA_FIXTURE_LANE=$lane" \
|
|
--from-literal="REMORA_COORDINATION_PROVIDER=$coordination" \
|
|
--from-literal="REMORA_COORDINATION_URL=$coordination_url" \
|
|
--from-literal="REMORA_COORDINATION_NAMESPACE=$coordination_namespace" \
|
|
--from-literal="REMORA_OIDC_PROVIDER=$oidc" \
|
|
--from-literal="REMORA_OIDC_ISSUER=$oidc_issuer" \
|
|
--from-literal="REMORA_OIDC_TENANT=$oidc_tenant" \
|
|
--from-literal="REMORA_OIDC_CLIENT_ID=$oidc_client_id" \
|
|
--from-literal="REMORA_OIDC_USERNAME=$oidc_username" \
|
|
--from-literal=REMORA_OIDC_PASSWORD=Synthetic-OIDC-1! \
|
|
--from-literal="REMORA_OIDC_STORAGE_ROLE=$oidc_storage_role" \
|
|
--from-literal="REMORA_OIDC_PROJECT_ID=$oidc_project_id" \
|
|
--from-literal="REMORA_OIDC_USER_ID=$oidc_user_id" \
|
|
--from-literal="REMORA_OIDC_MGMT_TOKEN=$oidc_mgmt_token" \
|
|
--from-literal="REMORA_OIDC_USER_KEY_B64=$oidc_user_key_b64" \
|
|
--from-literal="REMORA_TARGET_PROVIDER=$target" \
|
|
--from-literal="REMORA_TARGET_ENDPOINT=$target_endpoint" \
|
|
--from-literal="REMORA_TARGET_BUCKET=$target_bucket" \
|
|
--from-literal="REMORA_TARGET_PREFIX=$target_prefix" \
|
|
--from-literal="REMORA_TARGET_ACCESS_KEY=$target_access_key" \
|
|
--from-literal="REMORA_TARGET_SECRET_KEY=$target_secret_key" \
|
|
--from-literal="REMORA_TARGET_REGION=$target_region" \
|
|
--from-literal=REMORA_TARGET_EXCLUSIVE_WRITER=true \
|
|
--from-literal=REMORA_VAULT_ADDR=http://vault:8200 \
|
|
--from-literal=REMORA_VAULT_TOKEN=synthetic-vault-token \
|
|
--from-literal="REMORA_VAULT_PATH=$vault_path" \
|
|
--from-literal="DATABASE_URL=$database_url" >&2 || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" create configmap "$profile_name-file" \
|
|
--from-literal="profile.env=$profile" >&2 || return
|
|
printf '%s\n' "$profile_name"
|
|
}
|
|
|
|
render_lane_job() {
|
|
local template="$1"
|
|
local output="$2"
|
|
local job="$3"
|
|
local profile="$4"
|
|
local image="$5"
|
|
local postgres_image="${POSTGRES_LOCAL_IMAGE:-remora-testbed/unused:postgres}"
|
|
local redis_image="${REDIS_LOCAL_IMAGE:-remora-testbed/unused:redis}"
|
|
local vault_image="${VAULT_LOCAL_IMAGE:-remora-testbed/unused:vault}"
|
|
local curl_image="${CURL_LOCAL_IMAGE:-remora-testbed/unused:curl}"
|
|
local aws_cli_image="${AWS_CLI_LOCAL_IMAGE:-remora-testbed/unused:aws-cli}"
|
|
sed \
|
|
-e "s|@@JOB_NAME@@|$job|g" \
|
|
-e "s|@@PROFILE_NAME@@|$profile|g" \
|
|
-e "s|@@TEST_IMAGE@@|$image|g" \
|
|
-e "s|@@POSTGRES_IMAGE@@|$postgres_image|g" \
|
|
-e "s|@@REDIS_IMAGE@@|$redis_image|g" \
|
|
-e "s|@@VAULT_IMAGE@@|$vault_image|g" \
|
|
-e "s|@@CURL_IMAGE@@|$curl_image|g" \
|
|
-e "s|@@AWS_CLI_IMAGE@@|$aws_cli_image|g" \
|
|
"$template" > "$output"
|
|
}
|
|
|
|
run_job_matrix() {
|
|
local job_base="$1"
|
|
local template="$2"
|
|
local image="$3"
|
|
shift 3
|
|
local families=("$@")
|
|
local lane
|
|
local coordination
|
|
local oidc
|
|
local target
|
|
local job
|
|
local profile
|
|
local manifest
|
|
select_fixture_families "${families[@]}" || return
|
|
while IFS=$'\t' read -r lane coordination oidc target; do
|
|
job="$job_base-$lane"
|
|
profile="$(write_lane_profile "$lane" "$coordination" "$oidc" "$target")" || return
|
|
manifest="$RUNTIME/$job.yaml"
|
|
render_lane_job "$template" "$manifest" "$job" "$profile" "$image" || return
|
|
printf 'Running fixture lane %s: coordination=%s oidc=%s target=%s\n' "$lane" "$coordination" "$oidc" "$target"
|
|
run_job_manifest "$FIXTURE_NAMESPACE" "$job" "$manifest" || return
|
|
cleanup_lane_state "$lane" "$coordination" "$oidc" "$target" || return
|
|
kube --namespace "$FIXTURE_NAMESPACE" delete \
|
|
"job/$job" \
|
|
"configmap/$profile" \
|
|
"configmap/$profile-file" \
|
|
--wait=true || return
|
|
rm -f "$manifest" || return
|
|
done < <(expand_fixture_matrix "${families[@]}")
|
|
}
|
|
|
|
verify_dependencies() {
|
|
local family
|
|
for family in durable coordination oidc target secret; do
|
|
if ! fixture_has_family "$family"; then
|
|
printf 'released-image dependency probes require the all fixture profile\n' >&2
|
|
return 1
|
|
fi
|
|
done
|
|
kube apply --filename "$RUNTIME/probe.yaml"
|
|
for job in state-probe oidc-probe garage-probe versitygw-probe; do
|
|
wait_for_job "$FIXTURE_NAMESPACE" "$job"
|
|
done
|
|
}
|
|
|
|
destroy_fixture() {
|
|
configure_provider
|
|
delete_cluster
|
|
remove_runtime_namespace
|
|
rm -rf "$RUNTIME/image-archives"
|
|
rm -f \
|
|
"$KUBECONFIG_PATH" \
|
|
"$RUNTIME/dependencies.yaml" \
|
|
"$RUNTIME/fixture.env" \
|
|
"$RUNTIME/probe.yaml" \
|
|
"$RUNTIME/sample-job.yaml" \
|
|
"$RUNTIME/durable-job.yaml"
|
|
find "$RUNTIME" -maxdepth 1 -type f -name '*-[0-9][0-9][0-9].yaml' -delete
|
|
}
|
|
|
|
kube() {
|
|
"$BIN/kubectl" \
|
|
--kubeconfig "$KUBECONFIG_PATH" \
|
|
--context "$CONTEXT" \
|
|
"$@"
|
|
}
|
|
|
|
wait_for_job() {
|
|
local namespace="$1"
|
|
local job="$2"
|
|
local deadline=$((SECONDS + 300))
|
|
local complete
|
|
local failed
|
|
while ((SECONDS < deadline)); do
|
|
complete="$(kube --namespace "$namespace" get "job/$job" --output jsonpath='{.status.conditions[?(@.type=="Complete")].status}')"
|
|
if [[ "$complete" = True ]]; then
|
|
print_job_logs "$namespace" "$job"
|
|
return
|
|
fi
|
|
failed="$(kube --namespace "$namespace" get "job/$job" --output jsonpath='{.status.conditions[?(@.type=="Failed")].status}')"
|
|
if [[ "$failed" = True ]]; then
|
|
print_job_logs "$namespace" "$job" >&2 || true
|
|
return 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
print_job_logs "$namespace" "$job" >&2 || true
|
|
printf 'job/%s did not complete within 300 seconds\n' "$job" >&2
|
|
return 1
|
|
}
|
|
|
|
print_job_logs() {
|
|
local namespace="$1"
|
|
local job="$2"
|
|
local pod
|
|
local containers
|
|
local container
|
|
local status=0
|
|
pod="$(kube --namespace "$namespace" get pods \
|
|
--selector "job-name=$job" \
|
|
--output jsonpath='{.items[0].metadata.name}')" || return
|
|
containers="$(kube --namespace "$namespace" get "pod/$pod" \
|
|
--output jsonpath='{range .spec.initContainers[*]}{.name}{"\n"}{end}{range .spec.containers[*]}{.name}{"\n"}{end}')" || return
|
|
while IFS= read -r container; do
|
|
[[ -z "$container" ]] && continue
|
|
kube --namespace "$namespace" logs "pod/$pod" --container "$container" || status=1
|
|
done <<< "$containers"
|
|
return "$status"
|
|
}
|
|
|
|
run_job_manifest() {
|
|
local namespace="$1"
|
|
local job="$2"
|
|
local manifest="$3"
|
|
kube --namespace "$namespace" delete "job/$job" --ignore-not-found --wait=true || return
|
|
kube --namespace "$namespace" apply --filename "$manifest" || return
|
|
wait_for_job "$namespace" "$job" || return
|
|
}
|