#!/usr/bin/env bash set -Eeuo pipefail # Secret이 셸 추적에 노출되지 않도록 호출자가 bash -x를 사용해도 끈다. set +x readonly NAMESPACE="object-storage" readonly OBJECTSTORE="minio-aistor" readonly ROOT_SECRET="aistor-root-configuration" readonly S3_ENDPOINT="http://minio.object-storage.svc.cluster.local" readonly MC_IMAGE="quay.io/minio/aistor/mc@sha256:c5ec777c080fd6292b7529309a0f311c58c3a92e268c9c57c0901da86881949b" readonly PASS_LINE="AISTOR_S3_SMOKE_PASS bucket-create object-write object-read object-delete bucket-delete" execute_requested=false pod_name="" fail() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } cleanup() { if [[ -n "$pod_name" ]]; then kubectl --namespace "$NAMESPACE" delete pod "$pod_name" \ --ignore-not-found --wait=true --timeout=30s >/dev/null 2>&1 || true fi } usage() { cat <<'USAGE' Usage: bash scripts/validate/aistor-s3-smoke.sh --execute Creates one restricted, short-lived AIStor Client Pod. It authenticates with object-storage/aistor-root-configuration and performs: bucket create -> object write -> SHA-256 read check -> object delete -> bucket delete The script never prints credentials. It removes the temporary bucket and Pod. Type APPLY only when prompted because this performs temporary S3 writes. USAGE } while (( $# > 0 )); do case "$1" in --execute) execute_requested=true shift ;; -h|--help) usage exit 0 ;; *) usage >&2 fail "unsupported argument: $1" ;; esac done [[ "$execute_requested" == true ]] || { usage fail "--execute is required" } for command_name in kubectl grep jq; do command -v "$command_name" >/dev/null 2>&1 || \ fail "${command_name} is required" done [[ "$( kubectl --namespace "$NAMESPACE" get objectstore "$OBJECTSTORE" \ --output=jsonpath='{.status.currentState}' )" == "Initialized" ]] || fail "ObjectStore is not Initialized" [[ "$( kubectl --namespace "$NAMESPACE" get objectstore "$OBJECTSTORE" \ --output=jsonpath='{.status.healthStatus}' )" == "green" ]] || fail "ObjectStore health is not green" kubectl --namespace "$NAMESPACE" get secret "$ROOT_SECRET" >/dev/null printf '%s\n' \ "Target: ${NAMESPACE}/${OBJECTSTORE}" \ "Endpoint: ${S3_ENDPOINT}" \ 'Temporary resources: one bucket, one object, and one restricted client Pod' \ 'No credential value will be printed.' printf 'Type APPLY to run the authenticated S3 smoke test: ' IFS= read -r confirmation [[ "$confirmation" == "APPLY" ]] || fail "confirmation did not match APPLY" pod_name="aistor-s3-smoke-$(date +%H%M%S)-$$" trap cleanup EXIT INT TERM smoke_command='set -eu . /run/aistor-root/config.env bucket="platform-smoke-$(date +%s)" cleanup_bucket() { mc rb --force "local/${bucket}" >/dev/null 2>&1 || true; } trap cleanup_bucket EXIT mc alias set local "'"$S3_ENDPOINT"'" "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" --api S3v4 >/dev/null payload="aistor-authenticated-smoke-$(date +%s)" expected="$(printf "%s" "$payload" | sha256sum | cut -d " " -f 1)" mc mb "local/${bucket}" >/dev/null printf "%s" "$payload" | mc pipe "local/${bucket}/probe.txt" >/dev/null actual="$(mc cat "local/${bucket}/probe.txt" | sha256sum | cut -d " " -f 1)" [ "$expected" = "$actual" ] mc stat "local/${bucket}/probe.txt" >/dev/null mc rm "local/${bucket}/probe.txt" >/dev/null mc rb "local/${bucket}" >/dev/null trap - EXIT printf "%s\n" "'"$PASS_LINE"'"' overrides="$( jq -nc --arg command "$smoke_command" --arg image "$MC_IMAGE" '{ spec: { automountServiceAccountToken: false, securityContext: { runAsNonRoot: true, runAsUser: 1000, runAsGroup: 1000, fsGroup: 1000, seccompProfile: {type: "RuntimeDefault"} }, containers: [{ name: "aistor-s3-smoke", image: $image, imagePullPolicy: "IfNotPresent", command: ["/bin/sh", "-ec"], args: [$command], securityContext: { allowPrivilegeEscalation: false, capabilities: {drop: ["ALL"]}, readOnlyRootFilesystem: true }, env: [{name: "MC_CONFIG_DIR", value: "/tmp/mc"}], volumeMounts: [ {name: "root-config", mountPath: "/run/aistor-root", readOnly: true}, {name: "tmp", mountPath: "/tmp"} ] }], volumes: [ { name: "root-config", secret: {secretName: "'"$ROOT_SECRET"'", defaultMode: 256} }, {name: "tmp", emptyDir: {}} ] } }' )" kubectl --namespace "$NAMESPACE" run "$pod_name" \ --image="$MC_IMAGE" \ --restart=Never \ --labels='platform.hyeonworks.com/aistor-client=true,app.kubernetes.io/name=aistor-s3-smoke' \ --overrides="$overrides" succeeded=false for _ in $(seq 1 90); do phase="$( kubectl --namespace "$NAMESPACE" get pod "$pod_name" \ --output=jsonpath='{.status.phase}' )" if [[ "$phase" == "Succeeded" ]]; then succeeded=true break fi [[ "$phase" != "Failed" ]] || break waiting_reason="$( kubectl --namespace "$NAMESPACE" get pod "$pod_name" \ --output=jsonpath='{.status.containerStatuses[0].state.waiting.reason}' \ 2>/dev/null || true )" case "$waiting_reason" in CreateContainerConfigError|CrashLoopBackOff|ErrImagePull|ImagePullBackOff) break ;; esac sleep 2 done if [[ "$succeeded" != true ]]; then kubectl --namespace "$NAMESPACE" logs "$pod_name" || true kubectl --namespace "$NAMESPACE" describe pod "$pod_name" | tail -n 60 fail "authenticated S3 smoke test failed" fi log_output="$(kubectl --namespace "$NAMESPACE" logs "$pod_name")" grep --fixed-strings --line-regexp --quiet "$PASS_LINE" <<<"$log_output" || \ fail "smoke Pod did not emit the exact success contract" printf '%s\n' "$PASS_LINE" printf '%s\n' \ "Client image: ${MC_IMAGE}" \ 'Temporary bucket, object, and client Pod cleanup: complete'