diff --git a/.cursor/prompts/deploy-dcm.md b/.cursor/prompts/deploy-dcm.md index 01e1961..a331b64 100644 --- a/.cursor/prompts/deploy-dcm.md +++ b/.cursor/prompts/deploy-dcm.md @@ -48,6 +48,19 @@ Deploy the full DCM stack for E2E testing using `scripts/deploy-dcm.sh`. ./scripts/deploy-dcm.sh --cleanup-on-failure ``` +### Deploy with Authentication Enabled +```bash +# Starts Keycloak (compose profile auth) and enables JWT validation +./scripts/deploy-dcm.sh --auth-enabled + +# Equivalent via environment (Jenkins uses this today) +AUTH_DISABLED=false AUTH_ISSUER_URL=http://keycloak:8080/realms/dcm AUTH_JWT_AUDIENCE=dcm-api \ + ./scripts/deploy-dcm.sh + +# Tear down auth-enabled stack with the same flag +./scripts/deploy-dcm.sh --auth-enabled --tear-down +``` + ### Deploy with k8s Container Service Provider ```bash # Auto-detects cluster from existing oc/kubectl session @@ -107,17 +120,23 @@ When any service provider is enabled, the script resolves cluster access in this | `OPENSHIFT_API` | `--cluster-api` | | `OPENSHIFT_USERNAME` | `--cluster-username` | | `OPENSHIFT_PASSWORD` | `--cluster-password` | +| `AUTH_DISABLED` | Set to `false` to enable auth (same as `--auth-enabled`) | Flags take precedence over environment variables. +## Compose Credentials + +After cloning control-plane, the script creates `deploy/.env` from `deploy/.env.example` if missing. Database and optional auth credentials are written there for compose `env_file: .env` services. Lab defaults match control-plane's `.env.example`; override via shell env vars before running the script. + ## What Happens 1. Clones control-plane (`deploy/compose.yaml`) -2. Runs `podman-compose up -d` -3. Verifies all containers are running -4. Polls `/api/v1alpha1/health` (90s timeout) -5. Resolves container images to git commit SHAs via Quay.io API -6. Writes `dcm-versions.json` +2. Bootstraps `deploy/.env` with DB credentials (and auth credentials when `--auth-enabled`) +3. Runs `podman-compose up -d` +4. Verifies all containers are running +5. Polls `/api/v1alpha1/health` (90s timeout) +6. Resolves container images to git commit SHAs via Quay.io API +7. Writes `dcm-versions.json` ## Output diff --git a/.cursor/prompts/tear-down.md b/.cursor/prompts/tear-down.md index ad1d8a7..d106945 100644 --- a/.cursor/prompts/tear-down.md +++ b/.cursor/prompts/tear-down.md @@ -14,6 +14,13 @@ Stop and clean up a running DCM deployment. ./scripts/deploy-dcm.sh --control-plane-dir /path/to/deploy --tear-down ``` +### Authenticated stack +```bash +./scripts/deploy-dcm.sh --auth-enabled --tear-down +``` + +Use the same authentication setting used during deployment so the auth compose profile, including Keycloak, is included in teardown. + ## What Happens 1. Stops all containers via `podman-compose down -v` diff --git a/.github/workflows/validate-scripts.yaml b/.github/workflows/validate-scripts.yaml index f643718..46b82f8 100644 --- a/.github/workflows/validate-scripts.yaml +++ b/.github/workflows/validate-scripts.yaml @@ -98,6 +98,7 @@ jobs: --expect-contains "--control-plane-branch" "control-plane-branch flag" \ --expect-contains "--kubeconfig" "kubeconfig flag" \ --expect-contains "--compose-file" "compose-file flag" \ + --expect-contains "--auth-enabled" "auth-enabled flag" \ --expect-contains "--cleanup-on-failure" "cleanup-on-failure flag" \ --expect-contains "--running-versions" "running-versions flag" \ --expect-contains "--tear-down" "tear-down flag" \ diff --git a/CLAUDE.md b/CLAUDE.md index 897314f..2aa55e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,9 @@ CI runs ShellCheck on changed `*.sh` files via `.github/workflows/lint.yaml` (on Deploys the full DCM stack for E2E testing by cloning control-plane (`deploy/compose.yaml`), running `podman-compose up`, and polling health endpoints until all services respond 2xx. -**Flow:** clone control-plane → `podman-compose up -d` → verify containers running → poll `/api/v1alpha1/health` → collect container versions from Quay.io API → write `dcm-versions.json`. +**Flow:** clone control-plane → bootstrap `deploy/.env` → `podman-compose up -d` → verify containers running → poll `/api/v1alpha1/health` → collect container versions from Quay.io API → write `dcm-versions.json`. + +**Compose credentials:** After clone, the script copies `deploy/.env.example` to `deploy/.env` when missing and upserts DB/auth keys (lab defaults unless overridden by shell env). Control-plane compose reads these via `env_file: .env`. Pass `--auth-enabled` or set `AUTH_DISABLED=false` to add the compose `auth` profile (Keycloak) and write auth credentials into `.env`. **Modes:** The script has three mutually exclusive modes: - **Deploy** (default): full clone + bring-up + health check. Pass `--cleanup-on-failure` to auto-teardown on error (default leaves partial state for debugging). @@ -48,6 +50,8 @@ When a non-main version is specified, `--control-plane-branch` is auto-derived t **Cluster authentication:** When any provider is enabled, the script resolves cluster access in priority order: explicit `--kubeconfig`, existing `oc`/`kubectl` session, or `oc login` via `--cluster-api` + `--cluster-password`. +**Control-plane authentication:** Pass `--auth-enabled` (or set `AUTH_DISABLED=false`) to start Keycloak and enable JWT validation. Use the same flag on `--tear-down` when tearing down an auth-enabled stack. The E2E suite currently supports unauthenticated test runs only. + Run `./scripts/deploy-dcm.sh --help` for all flags and environment variable overrides. ## Local dev scripts @@ -107,6 +111,8 @@ The script is organized into sections separated by comment banners. Key function | `validate_k8s_container_provider` | Validates k8s container SP prerequisites | | `validate_k8s_storage_provider` | Validates k8s storage SP prerequisites | | `validate_acm_cluster_provider` | Validates ACM cluster SP prerequisites | +| `ensure_deploy_env` | Bootstraps `deploy/.env` from `.env.example` and upserts credentials | +| `upsert_deploy_env_var` | Idempotently sets a key in `deploy/.env` | | `resolve_provider_cli` | Resolves `oc`/`kubectl` per provider's `CLI_REQUIREMENT` | | `collect_provider_compose` | Collects compose profiles/overrides for an enabled provider | | `verify_health` | Confirms all compose services are running, then polls health endpoints with timeout | diff --git a/README.md b/README.md index d906598..c1bafc9 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,10 @@ Both deploy mode and `--running-versions` produce a `dcm-versions.json` mapping `scripts/deploy-dcm.sh` automates the full DCM stack lifecycle for E2E testing: 1. Clones the [control-plane](https://github.com/dcm-project/control-plane) repo (`deploy/compose.yaml`) -2. Starts all services with `podman-compose up` -3. Polls health endpoints until every service responds 2xx -4. Resolves running container images to git commit SHAs via the Quay.io API +2. Bootstraps `deploy/.env` from `deploy/.env.example` (compose credentials; see control-plane `deploy/RUN.md`) +3. Starts all services with `podman-compose up` +4. Polls health endpoints until every service responds 2xx +5. Resolves running container images to git commit SHAs via the Quay.io API ### Prerequisites @@ -78,8 +79,11 @@ Both deploy mode and `--running-versions` produce a `dcm-versions.json` mapping # 8. Deploy ACM cluster provider (install ACM first if needed) ./scripts/deploy-dcm.sh --acm-cluster-service-provider --deploy-acm --kubeconfig ~/.kube/config -# 9. Tear down when done -./scripts/deploy-dcm.sh --tear-down +# 9. Deploy with authentication enabled (Keycloak + JWT validation) +./scripts/deploy-dcm.sh --auth-enabled + +# 10. Tear down when done +./scripts/deploy-dcm.sh --auth-enabled --tear-down ``` Run `./scripts/deploy-dcm.sh --help` for all flags and environment variable overrides. diff --git a/scripts/deploy-dcm.sh b/scripts/deploy-dcm.sh index fc943c1..ea6160c 100755 --- a/scripts/deploy-dcm.sh +++ b/scripts/deploy-dcm.sh @@ -145,6 +145,7 @@ EOF --cluster-username USER Username for oc login (default: kubeadmin) --cluster-password PASS Password for oc login --compose-file PATH Additional compose file to merge (repeatable, e.g. port overrides) + --auth-enabled Enable authentication (Keycloak + JWT validation; adds compose profile auth) --cleanup-on-failure Tear down the stack automatically if deployment fails (default: leave for debugging) --running-versions Print versions of all running containers and write dcm-versions.json --tear-down Stop the stack, remove volumes, and clean the deploy directory @@ -165,6 +166,7 @@ Environment variables (flags take precedence): OPENSHIFT_API Same as --cluster-api OPENSHIFT_USERNAME Same as --cluster-username (default: kubeadmin) OPENSHIFT_PASSWORD Same as --cluster-password + AUTH_DISABLED Set to 'false' to enable auth (same effect as --auth-enabled) EOF # Provider namespace env vars (generated from registry) @@ -189,6 +191,7 @@ Examples: $(basename "$0") --k8s-storage-service-provider --kubeconfig ~/.kube/config $(basename "$0") --all-service-providers --cluster-api https://api.cluster.example.com --cluster-password secret $(basename "$0") --acm-cluster-service-provider --deploy-acm --kubeconfig ~/.kube/config + $(basename "$0") --auth-enabled $(basename "$0") --tear-down $(basename "$0") --running-versions EOF @@ -605,6 +608,115 @@ resolve_provider_cli() { esac } +# --- Compose credential bootstrap ------------------------------------------ # +# +# control-plane deploy/compose.yaml reads credentials from deploy/.env +# (see control-plane deploy/.env.example). Bootstrap that file after clone. + +env_or_default() { + local var_name="$1" + local default_value="$2" + if [[ -n "${!var_name:-}" ]]; then + echo "${!var_name}" + else + echo "${default_value}" + fi +} + +resolve_db_password() { + local password_var + local resolved_password="" + local explicit_password="" + + for password_var in POSTGRES_PASSWORD DB_PASS DB_PASSWORD; do + if [[ -n "${!password_var:-}" ]]; then + if [[ -z "${explicit_password}" ]]; then + explicit_password="${!password_var}" + resolved_password="${password_var}=${!password_var}" + elif [[ "${!password_var}" != "${explicit_password}" ]]; then + err "Conflicting database passwords supplied via ${resolved_password%%=*} and ${password_var}" + return 1 + fi + fi + done + + if [[ -z "${explicit_password}" ]]; then + explicit_password="adminpass" + fi + + printf '%s\n' "${explicit_password}" +} + +upsert_deploy_env_var() { + local deploy_dir="$1" + local key="$2" + local value="$3" + local env_file="${deploy_dir}/deploy/.env" + local tmp + + [[ -n "${value}" ]] || return 0 + + tmp="$(mktemp)" + if [[ -f "${env_file}" ]]; then + grep -v "^${key}=" "${env_file}" > "${tmp}" || true + fi + printf '%s=%s\n' "${key}" "${value}" >> "${tmp}" + mv "${tmp}" "${env_file}" +} + +ensure_deploy_env() { + local deploy_dir="$1" + local env_file="${deploy_dir}/deploy/.env" + local env_example="${deploy_dir}/deploy/.env.example" + local var + local db_password + + if [[ ! -f "${env_file}" ]]; then + if [[ ! -f "${env_example}" ]]; then + err "Missing ${env_example} — cannot bootstrap deploy credentials" + return 1 + fi + cp "${env_example}" "${env_file}" + info "Created ${env_file} from .env.example" + fi + + db_password="$(resolve_db_password)" || return 1 + upsert_deploy_env_var "${deploy_dir}" "POSTGRES_USER" "$(env_or_default POSTGRES_USER admin)" + upsert_deploy_env_var "${deploy_dir}" "POSTGRES_PASSWORD" "${db_password}" + upsert_deploy_env_var "${deploy_dir}" "DB_USER" "$(env_or_default DB_USER admin)" + upsert_deploy_env_var "${deploy_dir}" "DB_PASS" "${db_password}" + upsert_deploy_env_var "${deploy_dir}" "DB_PASSWORD" "${db_password}" + + if [[ "${AUTH_ENABLED}" == true ]]; then + upsert_deploy_env_var "${deploy_dir}" "KEYCLOAK_ADMIN" "$(env_or_default KEYCLOAK_ADMIN admin)" + upsert_deploy_env_var "${deploy_dir}" "KEYCLOAK_ADMIN_PASSWORD" "$(env_or_default KEYCLOAK_ADMIN_PASSWORD admin)" + upsert_deploy_env_var "${deploy_dir}" "DCM_DEV_USER_PASSWORD" "$(env_or_default DCM_DEV_USER_PASSWORD admin)" + upsert_deploy_env_var "${deploy_dir}" "AUTH_PROXY_SECRET" "$(env_or_default AUTH_PROXY_SECRET dcm-dev-proxy-secret)" + upsert_deploy_env_var "${deploy_dir}" "AUTH_DISABLED" "false" + upsert_deploy_env_var "${deploy_dir}" "AUTH_ISSUER_URL" "$(env_or_default AUTH_ISSUER_URL http://keycloak:8080/realms/dcm)" + upsert_deploy_env_var "${deploy_dir}" "AUTH_JWT_AUDIENCE" "$(env_or_default AUTH_JWT_AUDIENCE dcm-api)" + upsert_deploy_env_var "${deploy_dir}" "DCM_ADMIN_SUBJECT" "$(env_or_default DCM_ADMIN_SUBJECT 56deb662-4820-5d83-b828-f4beb11a5fa7)" + else + upsert_deploy_env_var "${deploy_dir}" "AUTH_DISABLED" "true" + fi + + if [[ -n "${DCM_VERSION:-}" ]]; then + for var in "${VERSION_ENV_VARS[@]}"; do + upsert_deploy_env_var "${deploy_dir}" "${var}" "${DCM_VERSION}" + done + else + for var in "${VERSION_ENV_VARS[@]}"; do + if [[ -n "${!var:-}" ]]; then + upsert_deploy_env_var "${deploy_dir}" "${var}" "${!var}" + fi + done + fi + + if [[ -n "${ACM_CLUSTER_SP_PULL_SECRET:-}" ]]; then + upsert_deploy_env_var "${deploy_dir}" "ACM_CLUSTER_SP_PULL_SECRET" "${ACM_CLUSTER_SP_PULL_SECRET}" + fi +} + # Collect compose args (profiles and overrides) for an enabled provider. collect_provider_compose() { local i="$1" @@ -645,6 +757,7 @@ DCM_KUBECONFIG="${KUBECONFIG:-}" OPENSHIFT_API="${OPENSHIFT_API:-}" OPENSHIFT_USERNAME="${OPENSHIFT_USERNAME:-kubeadmin}" OPENSHIFT_PASSWORD="${OPENSHIFT_PASSWORD:-}" +AUTH_ENABLED_EXPLICIT=false COMPOSE_EXTRA_FILE_ARGS=() require_arg() { @@ -727,6 +840,8 @@ while [[ $# -gt 0 ]]; do require_arg "$1" "${2:-}" COMPOSE_EXTRA_FILE_ARGS+=("-f" "$(cd "$(dirname "${2:-}")" && pwd)/$(basename "${2:-}")") shift 2 ;; + --auth-enabled) + AUTH_ENABLED_EXPLICIT=true; shift ;; --cleanup-on-failure) CLEANUP_ON_FAILURE=true; shift ;; --running-versions) @@ -773,6 +888,23 @@ for i in $(seq 0 $((PROV_COUNT - 1))); do collect_provider_compose "${i}" done +AUTH_ENABLED=false +if [[ "${AUTH_ENABLED_EXPLICIT}" == true ]] || [[ "${AUTH_DISABLED:-}" == "false" ]]; then + AUTH_ENABLED=true +fi + +# Read the existing deployment configuration for standalone inspection and teardown. +# Do not source it: deploy/.env contains values that should not be executed as shell code. +if [[ "${RUNNING_VERSIONS}" == true || "${TEAR_DOWN}" == true ]] && + [[ -f "${CONTROL_PLANE_TMP_DIR}/deploy/.env" ]] && + grep -Eq "^AUTH_DISABLED[[:space:]]*=[[:space:]]*(false|\"false\"|'false')[[:space:]]*$" "${CONTROL_PLANE_TMP_DIR}/deploy/.env"; then + AUTH_ENABLED=true +fi + +if [[ "${AUTH_ENABLED}" == true ]]; then + COMPOSE_PROFILES+=("--profile" "auth") +fi + # --- Running versions (standalone) ----------------------------------------- # if [[ "${RUNNING_VERSIONS}" == true ]]; then @@ -1076,6 +1208,8 @@ fi log "Cloning control-plane (repo=${CONTROL_PLANE_REPO}, branch=${CONTROL_PLANE_BRANCH})" git clone --branch "${CONTROL_PLANE_BRANCH}" --single-branch --depth 1 "${CONTROL_PLANE_REPO}" "${CONTROL_PLANE_TMP_DIR}" +ensure_deploy_env "${CONTROL_PLANE_TMP_DIR}" || exit 1 + # --- Deploy ---------------------------------------------------------------- # if [[ "${CLEANUP_ON_FAILURE}" == true ]]; then @@ -1090,6 +1224,9 @@ done if [[ ${#ENABLED_LABELS[@]} -gt 0 ]]; then info "Enabled providers: ${ENABLED_LABELS[*]}" fi +if [[ "${AUTH_ENABLED}" == true ]]; then + info "Authentication enabled (compose profile: auth)" +fi podman-compose -f "${CONTROL_PLANE_TMP_DIR}/deploy/compose.yaml" ${COMPOSE_EXTRA_FILE_ARGS[@]+"${COMPOSE_EXTRA_FILE_ARGS[@]}"} ${COMPOSE_PROFILES[@]+"${COMPOSE_PROFILES[@]}"} up -d echo diff --git a/tests/run-e2e.sh b/tests/run-e2e.sh index 2bc4500..57f7c06 100755 --- a/tests/run-e2e.sh +++ b/tests/run-e2e.sh @@ -313,9 +313,9 @@ fi # Teardown the stack. if [[ "${SKIP_TEARDOWN}" == "false" ]]; then log "Tearing down DCM stack" - if ! "${DEPLOY_SCRIPT}" --tear-down; then + if ! "${DEPLOY_SCRIPT}" --tear-down "${DEPLOY_ARGS[@]+"${DEPLOY_ARGS[@]}"}"; then err "Teardown failed (non-fatal) — containers may still be running" - err "Manual cleanup: ${DEPLOY_SCRIPT} --tear-down" + err "Manual cleanup: ${DEPLOY_SCRIPT} --tear-down ${DEPLOY_ARGS[*]}" fi else log "Skipping teardown (--skip-teardown)"