Files

194 lines
5.8 KiB
Bash
Executable File

#!/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'