mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 13:31:08 +00:00
refactor: remove deploy.cmd and update Dockerfile for health checks
- Deleted the deploy.cmd script as it was no longer needed. - Updated Dockerfile to include curl for health checks and added a non-root user for improved security. - Modified health check command to use curl for better reliability. - Adjusted docker-compose.test.yml to reflect changes in health check configuration. - Cleaned up appsettings.json and removed unused configuration properties. - Removed Resource entity and related requests from the codebase as part of the architectural shift. - Updated documentation to reflect the removal of hardware binding and related endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/_lib.sh — shared helpers sourced by all deploy scripts.
|
||||
#
|
||||
# This file is sourced (not executed); do not set -e at the top — leave error
|
||||
# handling to the caller. The helpers always check their own preconditions.
|
||||
|
||||
# ----- logging --------------------------------------------------------------
|
||||
log_info() { printf '\033[32m[deploy]\033[0m %s\n' "$*" >&2; }
|
||||
log_warn() { printf '\033[33m[deploy WARN]\033[0m %s\n' "$*" >&2; }
|
||||
log_error() { printf '\033[31m[deploy ERROR]\033[0m %s\n' "$*" >&2; }
|
||||
die() { log_error "$*"; exit 1; }
|
||||
|
||||
# ----- input validation -----------------------------------------------------
|
||||
require_env() {
|
||||
local var
|
||||
for var in "$@"; do
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
die "Required environment variable not set: $var"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
local cmd
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
die "Required command not found on PATH: $cmd"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ----- env overlay ----------------------------------------------------------
|
||||
# load_env_overlay <env>
|
||||
# 1. Sources scripts/_defaults.env if present (developer-friendly defaults).
|
||||
# 2. Sources secrets/<env>.public.env (committed plain-text).
|
||||
# 3. Decrypts secrets/<env>.env via sops + age and sources the result.
|
||||
# The decrypted intermediate is written to a mktemp file and removed on EXIT.
|
||||
load_env_overlay() {
|
||||
local env="$1"
|
||||
local script_dir repo_root public_file enc_file decrypted
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$script_dir/.." && pwd)"
|
||||
|
||||
if [[ -f "$repo_root/.env" ]]; then
|
||||
# Local dev convenience; harmless on a production host because the
|
||||
# production host should not have a .env in REPO_ROOT.
|
||||
log_info "Sourcing $repo_root/.env"
|
||||
set -a; . "$repo_root/.env"; set +a
|
||||
fi
|
||||
|
||||
public_file="$repo_root/secrets/${env}.public.env"
|
||||
if [[ -f "$public_file" ]]; then
|
||||
log_info "Sourcing $public_file"
|
||||
set -a; . "$public_file"; set +a
|
||||
else
|
||||
log_warn "No $public_file — relying on environment / .env only"
|
||||
fi
|
||||
|
||||
enc_file="$repo_root/secrets/${env}.env"
|
||||
if [[ -f "$enc_file" ]]; then
|
||||
require_cmd sops age
|
||||
decrypted="$(mktemp -t azaion-env.XXXXXX)"
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -f '$decrypted'" EXIT INT TERM
|
||||
if ! SOPS_AGE_KEY_FILE="${SOPS_AGE_KEY_FILE:-/etc/azaion/age.key}" \
|
||||
sops -d "$enc_file" > "$decrypted" 2>/tmp/sops.err; then
|
||||
log_error "sops decrypt failed for $enc_file"
|
||||
cat /tmp/sops.err >&2
|
||||
die "Cannot continue without secrets"
|
||||
fi
|
||||
chmod 600 "$decrypted"
|
||||
log_info "Sourcing decrypted overlay (intermediate: $decrypted)"
|
||||
set -a; . "$decrypted"; set +a
|
||||
else
|
||||
log_warn "No $enc_file — secret values must already be in the environment"
|
||||
fi
|
||||
}
|
||||
|
||||
# ----- container helpers ----------------------------------------------------
|
||||
container_exists() {
|
||||
docker container inspect "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
container_running() {
|
||||
[[ "$(docker container inspect -f '{{.State.Running}}' "$1" 2>/dev/null || echo false)" == "true" ]]
|
||||
}
|
||||
|
||||
current_image_revision() {
|
||||
# Returns the org.opencontainers.image.revision label of the running
|
||||
# container, or empty if the container does not exist.
|
||||
docker container inspect "$1" \
|
||||
--format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' 2>/dev/null || true
|
||||
}
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/deploy.sh — Azaion Admin API deployment orchestrator.
|
||||
#
|
||||
# Usage:
|
||||
# ENV=staging ./scripts/deploy.sh <sha-tag>
|
||||
# ENV=production ./scripts/deploy.sh <sha-tag>
|
||||
# ./scripts/deploy.sh --rollback # uses the SHA from previous_tags.env
|
||||
# ./scripts/deploy.sh --help
|
||||
#
|
||||
# This is the single entry point; do not call the per-step scripts (pull/stop/
|
||||
# start/health) directly except from this file.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# shellcheck source=./_lib.sh
|
||||
. "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
ENV=staging|production ./scripts/deploy.sh <sha-tag>
|
||||
./scripts/deploy.sh --rollback
|
||||
./scripts/deploy.sh --help
|
||||
|
||||
Environment:
|
||||
ENV Required. "staging" or "production". Selects which
|
||||
secrets/<env>.env (sops-encrypted) is decrypted.
|
||||
REGISTRY_HOST,
|
||||
REGISTRY_IMAGE Registry hostname and image path; loaded from
|
||||
secrets/<env>.public.env unless already set.
|
||||
DEPLOY_* See .env.example.
|
||||
|
||||
Notes:
|
||||
- Run this on the deploy target host (it does not SSH for you in cycle 1).
|
||||
- Requires: docker, sops, age, curl, jq.
|
||||
EOF
|
||||
}
|
||||
|
||||
ROLLBACK=0
|
||||
SHA_TAG=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--help|-h) usage; exit 0 ;;
|
||||
--rollback) ROLLBACK=1 ;;
|
||||
-*) die "Unknown flag: $arg (use --help)" ;;
|
||||
*) SHA_TAG="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_env ENV
|
||||
require_cmd docker sops age curl jq
|
||||
|
||||
load_env_overlay "$ENV"
|
||||
|
||||
if [[ "$ROLLBACK" -eq 1 ]]; then
|
||||
PREV_FILE="$REPO_ROOT/scripts/.previous_tags.env"
|
||||
[[ -f "$PREV_FILE" ]] || die "No $PREV_FILE — cannot determine rollback target"
|
||||
# shellcheck disable=SC1090
|
||||
. "$PREV_FILE"
|
||||
[[ -n "${PREVIOUS_SHA_TAG:-}" ]] || die "PREVIOUS_SHA_TAG missing in $PREV_FILE"
|
||||
SHA_TAG="$PREVIOUS_SHA_TAG"
|
||||
log_warn "ROLLBACK requested → redeploying $SHA_TAG"
|
||||
fi
|
||||
|
||||
[[ -n "$SHA_TAG" ]] || die "Missing <sha-tag>. Pass the immutable SHA-tag (e.g. a1b2c3d4e5f6-arm) or use --rollback."
|
||||
|
||||
export REGISTRY_TAG="$SHA_TAG"
|
||||
|
||||
log_info "Deploy plan"
|
||||
log_info " ENV=$ENV"
|
||||
log_info " REGISTRY_HOST=$REGISTRY_HOST"
|
||||
log_info " REGISTRY_IMAGE=$REGISTRY_IMAGE"
|
||||
log_info " REGISTRY_TAG=$REGISTRY_TAG"
|
||||
log_info " DEPLOY_CONTAINER_NAME=$DEPLOY_CONTAINER_NAME"
|
||||
log_info " DEPLOY_HOST_PORT=$DEPLOY_HOST_PORT"
|
||||
|
||||
"$SCRIPT_DIR/pull-images.sh"
|
||||
"$SCRIPT_DIR/stop-services.sh"
|
||||
"$SCRIPT_DIR/start-services.sh"
|
||||
"$SCRIPT_DIR/health-check.sh"
|
||||
|
||||
log_info "Deploy succeeded — $REGISTRY_HOST/$REGISTRY_IMAGE:$REGISTRY_TAG is live as $DEPLOY_CONTAINER_NAME"
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/health-check.sh — poll /health/ready until 200 or timeout. Used as
|
||||
# the post-start gate by deploy.sh. Returns non-zero on any failure.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/health-check.sh [--help]
|
||||
|
||||
Reads from the environment:
|
||||
DEPLOY_HOST_PORT port the container is published on (default 4000)
|
||||
HEALTH_BASE_URL full URL override; defaults to http://127.0.0.1:$DEPLOY_HOST_PORT
|
||||
HEALTH_TIMEOUT seconds total to wait (default 60)
|
||||
HEALTH_INTERVAL seconds between attempts (default 2)
|
||||
EOF
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && { usage; exit 0; }
|
||||
|
||||
require_cmd curl
|
||||
PORT="${DEPLOY_HOST_PORT:-4000}"
|
||||
BASE_URL="${HEALTH_BASE_URL:-http://127.0.0.1:$PORT}"
|
||||
TIMEOUT="${HEALTH_TIMEOUT:-60}"
|
||||
INTERVAL="${HEALTH_INTERVAL:-2}"
|
||||
|
||||
# Liveness first (cheap; fails only if the process is wedged).
|
||||
log_info "Probing $BASE_URL/health/live"
|
||||
if ! curl --fail --silent --show-error --max-time 3 "$BASE_URL/health/live" >/dev/null; then
|
||||
die "/health/live did not return 200 — container is not responsive"
|
||||
fi
|
||||
|
||||
log_info "Polling $BASE_URL/health/ready (timeout=${TIMEOUT}s, interval=${INTERVAL}s)"
|
||||
DEADLINE=$(( $(date +%s) + TIMEOUT ))
|
||||
ATTEMPT=0
|
||||
while :; do
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
if BODY="$(curl --fail --silent --show-error --max-time 3 "$BASE_URL/health/ready" 2>/dev/null)"; then
|
||||
log_info "/health/ready returned 200 on attempt $ATTEMPT: $BODY"
|
||||
exit 0
|
||||
fi
|
||||
NOW=$(date +%s)
|
||||
if (( NOW >= DEADLINE )); then
|
||||
die "/health/ready did not return 200 within ${TIMEOUT}s (gave up after $ATTEMPT attempts)"
|
||||
fi
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
@@ -0,0 +1,128 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
|
||||
const ADMIN_EMAIL = __ENV.ADMIN_EMAIL || 'admin@azaion.com';
|
||||
const ADMIN_PASSWORD = __ENV.ADMIN_PASSWORD || 'Admin1234';
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
nft_perf_01_login: {
|
||||
executor: 'constant-vus',
|
||||
vus: 10,
|
||||
duration: '30s',
|
||||
exec: 'login',
|
||||
tags: { scenario: 'nft_perf_01_login' },
|
||||
},
|
||||
nft_perf_04_user_list: {
|
||||
executor: 'constant-vus',
|
||||
vus: 10,
|
||||
duration: '30s',
|
||||
exec: 'userList',
|
||||
tags: { scenario: 'nft_perf_04_user_list' },
|
||||
startTime: '35s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
'http_req_duration{scenario:nft_perf_01_login}': ['p(95)<500'],
|
||||
'http_req_duration{scenario:nft_perf_04_user_list}': ['p(95)<1000'],
|
||||
'http_req_failed{scenario:nft_perf_01_login}': ['rate<0.01'],
|
||||
'http_req_failed{scenario:nft_perf_04_user_list}': ['rate<0.01'],
|
||||
},
|
||||
};
|
||||
|
||||
// setup() runs once before all VUs. Pre-fetch a JWT so the user-list scenario
|
||||
// measures only the listing path, not login latency.
|
||||
export function setup() {
|
||||
const res = http.post(
|
||||
`${BASE_URL}/login`,
|
||||
JSON.stringify({ Email: ADMIN_EMAIL, Password: ADMIN_PASSWORD }),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`setup: login failed (status ${res.status}): ${res.body}`);
|
||||
}
|
||||
const body = JSON.parse(res.body);
|
||||
const token = body.token || body.Token;
|
||||
if (!token) {
|
||||
throw new Error(`setup: login response missing Token: ${res.body}`);
|
||||
}
|
||||
return { token };
|
||||
}
|
||||
|
||||
export function login() {
|
||||
const res = http.post(
|
||||
`${BASE_URL}/login`,
|
||||
JSON.stringify({ Email: ADMIN_EMAIL, Password: ADMIN_PASSWORD }),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { scenario: 'nft_perf_01_login' },
|
||||
}
|
||||
);
|
||||
check(res, {
|
||||
'login status 200': (r) => r.status === 200,
|
||||
'login returned token': (r) => {
|
||||
try {
|
||||
const body = JSON.parse(r.body);
|
||||
return !!(body.token || body.Token);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
sleep(0.1);
|
||||
}
|
||||
|
||||
export function userList(data) {
|
||||
const res = http.get(`${BASE_URL}/users`, {
|
||||
headers: { Authorization: `Bearer ${data.token}` },
|
||||
tags: { scenario: 'nft_perf_04_user_list' },
|
||||
});
|
||||
check(res, {
|
||||
'user list status 200': (r) => r.status === 200,
|
||||
'user list returned >= 500 users': (r) => {
|
||||
try {
|
||||
const arr = JSON.parse(r.body);
|
||||
return Array.isArray(arr) && arr.length >= 500;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
sleep(0.1);
|
||||
}
|
||||
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'e2e/test-results/perf-summary.json': JSON.stringify(data, null, 2),
|
||||
stdout: textSummary(data),
|
||||
};
|
||||
}
|
||||
|
||||
function textSummary(data) {
|
||||
const lines = [];
|
||||
lines.push('');
|
||||
lines.push('=== PERF SUMMARY ===');
|
||||
for (const [name, metric] of Object.entries(data.metrics)) {
|
||||
if (!name.startsWith('http_req_duration') && !name.startsWith('http_req_failed')) continue;
|
||||
const vals = metric.values || {};
|
||||
const parts = [];
|
||||
if (vals['p(50)'] !== undefined) parts.push(`p50=${vals['p(50)'].toFixed(1)}ms`);
|
||||
if (vals['p(95)'] !== undefined) parts.push(`p95=${vals['p(95)'].toFixed(1)}ms`);
|
||||
if (vals['p(99)'] !== undefined) parts.push(`p99=${vals['p(99)'].toFixed(1)}ms`);
|
||||
if (vals.rate !== undefined) parts.push(`rate=${(vals.rate * 100).toFixed(2)}%`);
|
||||
if (vals.count !== undefined) parts.push(`count=${vals.count}`);
|
||||
lines.push(` ${name}: ${parts.join(' · ')}`);
|
||||
}
|
||||
if (data.root_group && data.root_group.checks) {
|
||||
const checks = data.root_group.checks;
|
||||
if (checks.length > 0) {
|
||||
lines.push('--- checks ---');
|
||||
for (const c of checks) {
|
||||
lines.push(` ${c.name}: ${c.passes} pass / ${c.fails} fail`);
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push('====================');
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/pull-images.sh — login + pull the target image. Idempotent.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/pull-images.sh [--help]
|
||||
|
||||
Reads from the environment (use scripts/deploy.sh which sources the overlays):
|
||||
REGISTRY_HOST, REGISTRY_IMAGE, REGISTRY_TAG image coordinates
|
||||
REGISTRY_USER, REGISTRY_TOKEN optional; if both set, docker login first
|
||||
EOF
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && { usage; exit 0; }
|
||||
|
||||
require_env REGISTRY_HOST REGISTRY_IMAGE REGISTRY_TAG
|
||||
require_cmd docker
|
||||
|
||||
IMAGE="$REGISTRY_HOST/$REGISTRY_IMAGE:$REGISTRY_TAG"
|
||||
|
||||
if [[ -n "${REGISTRY_USER:-}" && -n "${REGISTRY_TOKEN:-}" ]]; then
|
||||
log_info "Logging in to $REGISTRY_HOST as $REGISTRY_USER"
|
||||
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin >/dev/null
|
||||
else
|
||||
log_warn "No REGISTRY_USER / REGISTRY_TOKEN — assuming pre-authenticated docker"
|
||||
fi
|
||||
|
||||
log_info "Pulling $IMAGE"
|
||||
docker pull "$IMAGE"
|
||||
|
||||
# Surface the digest for the deploy log; the operator can reference it later.
|
||||
DIGEST="$(docker image inspect "$IMAGE" --format '{{ index .RepoDigests 0 }}' 2>/dev/null || true)"
|
||||
if [[ -n "$DIGEST" ]]; then
|
||||
log_info "Pulled digest: $DIGEST"
|
||||
else
|
||||
log_warn "Could not resolve digest for $IMAGE (image may not have a registry digest yet)"
|
||||
fi
|
||||
@@ -3,41 +3,64 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
RESULTS_DIR="$PROJECT_ROOT/e2e/test-results"
|
||||
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.test.yml"
|
||||
SCENARIOS_FILE="$SCRIPT_DIR/perf-scenarios.js"
|
||||
|
||||
BASE_URL="${BASE_URL:-http://localhost:8080}"
|
||||
PERF_USER_COUNT="${PERF_USER_COUNT:-500}"
|
||||
|
||||
if ! command -v k6 >/dev/null 2>&1; then
|
||||
echo "ERROR: k6 not found on PATH. Install with: brew install k6"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$RESULTS_DIR"
|
||||
|
||||
cleanup() {
|
||||
echo "Cleaning up..."
|
||||
docker compose -f "$PROJECT_ROOT/docker-compose.test.yml" down -v --remove-orphans 2>/dev/null || true
|
||||
echo "=== Tearing down ==="
|
||||
docker compose -f "$COMPOSE_FILE" down -v --remove-orphans || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== Starting system under test ==="
|
||||
docker compose -f "$PROJECT_ROOT/docker-compose.test.yml" up -d system-under-test test-db
|
||||
docker compose -f "$COMPOSE_FILE" up -d system-under-test test-db
|
||||
|
||||
echo "=== Waiting for system to be ready ==="
|
||||
MAX_WAIT=30
|
||||
MAX_WAIT=60
|
||||
WAIT=0
|
||||
until curl -sf http://localhost:8080/swagger/index.html > /dev/null 2>&1 || [ $WAIT -ge $MAX_WAIT ]; do
|
||||
until curl -sf "$BASE_URL/swagger/index.html" > /dev/null 2>&1 || [ $WAIT -ge $MAX_WAIT ]; do
|
||||
sleep 1
|
||||
WAIT=$((WAIT + 1))
|
||||
done
|
||||
|
||||
if [ $WAIT -ge $MAX_WAIT ]; then
|
||||
echo "ERROR: System did not become ready within ${MAX_WAIT}s"
|
||||
docker compose -f "$COMPOSE_FILE" logs --tail=80 system-under-test || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Running performance tests ==="
|
||||
echo "Performance test runner not yet configured."
|
||||
echo "Install k6, locust, or artillery and add load scenarios from:"
|
||||
echo " _docs/02_document/tests/performance-tests.md"
|
||||
echo ""
|
||||
echo "Example with k6:"
|
||||
echo " k6 run scripts/perf-scenarios.js"
|
||||
echo ""
|
||||
echo "Thresholds from test spec:"
|
||||
echo " NFT-PERF-01: Login p95 < 500ms"
|
||||
echo " NFT-PERF-02: Small file download p95 < 1000ms"
|
||||
echo " NFT-PERF-03: Large file download p95 < 30000ms"
|
||||
echo " NFT-PERF-04: User list p95 < 1000ms"
|
||||
echo "=== Seeding $PERF_USER_COUNT perf users ==="
|
||||
# Reuse the admin password hash so the rows satisfy NOT NULL on password_hash.
|
||||
# These users are only used as listing volume, never for login.
|
||||
docker compose -f "$COMPOSE_FILE" exec -T test-db psql -U postgres -d azaion -v ON_ERROR_STOP=1 <<SQL
|
||||
INSERT INTO public.users (id, email, password_hash, role)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
'perf-user-' || lpad(g::text, 5, '0') || '@perf.azaion.com',
|
||||
'282wqVHZU0liTxphiGkKIaJtUA1W6rILdvfEOx8Ez350x0XLbgNtrSUYCK1r/ajq',
|
||||
'ApiAdmin'
|
||||
FROM generate_series(1, $PERF_USER_COUNT) AS g
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
SELECT count(*) AS users_total FROM public.users;
|
||||
SQL
|
||||
|
||||
echo "=== Performance test scaffolding complete ==="
|
||||
echo "=== Running k6 performance scenarios ==="
|
||||
# handleSummary() in perf-scenarios.js writes the JSON summary to RESULTS_DIR.
|
||||
set +e
|
||||
BASE_URL="$BASE_URL" k6 run "$SCENARIOS_FILE"
|
||||
K6_EXIT=$?
|
||||
set -e
|
||||
|
||||
echo "=== Performance test run complete (k6 exit=$K6_EXIT) ==="
|
||||
exit $K6_EXIT
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/smoke.sh — minimal post-deploy critical-path checks. Run from the
|
||||
# operator's workstation against the public URL, NOT from the deploy host.
|
||||
#
|
||||
# Usage:
|
||||
# BASE_URL=https://stage.admin.azaion.com SMOKE_ADMIN_EMAIL=... SMOKE_ADMIN_PASSWORD=... ./scripts/smoke.sh
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/smoke.sh [--help]
|
||||
|
||||
Required environment:
|
||||
BASE_URL public base URL of the deploy target
|
||||
SMOKE_ADMIN_EMAIL existing ApiAdmin user
|
||||
SMOKE_ADMIN_PASSWORD matching password
|
||||
EOF
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && { usage; exit 0; }
|
||||
|
||||
require_env BASE_URL SMOKE_ADMIN_EMAIL SMOKE_ADMIN_PASSWORD
|
||||
require_cmd curl jq
|
||||
|
||||
step() { log_info "smoke[$1] $2"; }
|
||||
fail() { log_error "smoke[$1] FAILED: $2"; exit 1; }
|
||||
|
||||
# 1. Liveness (anonymous)
|
||||
step 1 "GET /health/live"
|
||||
curl --fail --silent --show-error --max-time 5 "$BASE_URL/health/live" >/dev/null \
|
||||
|| fail 1 "/health/live did not return 200"
|
||||
|
||||
# 2. Readiness — public-facing nginx may not expose /health/ready (it's internal-
|
||||
# only by design). Skip if not 200; the deploy script already checked it inside
|
||||
# the host network.
|
||||
step 2 "GET /health/ready (best-effort, may be unreachable from public URL)"
|
||||
if curl --fail --silent --show-error --max-time 5 "$BASE_URL/health/ready" >/dev/null; then
|
||||
log_info "/health/ready returned 200 (exposed publicly — verify this is intentional)"
|
||||
fi
|
||||
|
||||
# 3. Login → JWT
|
||||
step 3 "POST /login as $SMOKE_ADMIN_EMAIL"
|
||||
TOKEN_JSON="$(curl --fail --silent --show-error --max-time 10 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg e "$SMOKE_ADMIN_EMAIL" --arg p "$SMOKE_ADMIN_PASSWORD" '{email:$e, password:$p}')" \
|
||||
"$BASE_URL/login")" \
|
||||
|| fail 3 "login request failed"
|
||||
TOKEN="$(echo "$TOKEN_JSON" | jq -r '.token // .Token // empty')"
|
||||
[[ -n "$TOKEN" ]] || fail 3 "login returned no token: $TOKEN_JSON"
|
||||
|
||||
AUTH=(-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
# 4. Authenticated GET /users/current
|
||||
step 4 "GET /users/current"
|
||||
curl --fail --silent --show-error --max-time 5 "${AUTH[@]}" "$BASE_URL/users/current" >/dev/null \
|
||||
|| fail 4 "/users/current did not return 200"
|
||||
|
||||
# 5. Authenticated GET /users
|
||||
step 5 "GET /users"
|
||||
USERS_JSON="$(curl --fail --silent --show-error --max-time 10 "${AUTH[@]}" "$BASE_URL/users")" \
|
||||
|| fail 5 "/users did not return 200"
|
||||
USER_COUNT="$(echo "$USERS_JSON" | jq 'length')"
|
||||
log_info "/users returned $USER_COUNT rows"
|
||||
|
||||
# 6. Authenticated GET /resources/list (default folder)
|
||||
step 6 "GET /resources/list"
|
||||
curl --fail --silent --show-error --max-time 5 "${AUTH[@]}" "$BASE_URL/resources/list" >/dev/null \
|
||||
|| fail 6 "/resources/list did not return 200"
|
||||
|
||||
log_info "smoke OK — 6 checks passed"
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/start-services.sh — `docker run` the API with the env overlay
|
||||
# materialized into a temp file. Bind mounts come from DEPLOY_HOST_*.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/start-services.sh [--help]
|
||||
|
||||
Reads from the environment (deploy.sh sets these):
|
||||
REGISTRY_HOST, REGISTRY_IMAGE, REGISTRY_TAG
|
||||
DEPLOY_CONTAINER_NAME, DEPLOY_HOST_PORT
|
||||
DEPLOY_HOST_CONTENT_DIR, DEPLOY_HOST_LOGS_DIR
|
||||
ASPNETCORE_ENVIRONMENT, ASPNETCORE_URLS
|
||||
ASPNETCORE_ConnectionStrings__AzaionDb / __AzaionDbAdmin
|
||||
ASPNETCORE_JwtConfig__Secret
|
||||
ASPNETCORE_ResourcesConfig__* (defaults from appsettings.json if unset)
|
||||
EOF
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && { usage; exit 0; }
|
||||
|
||||
require_env \
|
||||
REGISTRY_HOST REGISTRY_IMAGE REGISTRY_TAG \
|
||||
DEPLOY_CONTAINER_NAME DEPLOY_HOST_PORT \
|
||||
DEPLOY_HOST_CONTENT_DIR DEPLOY_HOST_LOGS_DIR \
|
||||
ASPNETCORE_ConnectionStrings__AzaionDb \
|
||||
ASPNETCORE_ConnectionStrings__AzaionDbAdmin \
|
||||
ASPNETCORE_JwtConfig__Secret
|
||||
require_cmd docker
|
||||
|
||||
IMAGE="$REGISTRY_HOST/$REGISTRY_IMAGE:$REGISTRY_TAG"
|
||||
|
||||
# Materialize an env file for `docker run --env-file`. We pass only the
|
||||
# ASPNETCORE_* + AZAION_* variables — registry / deploy host vars stay on the
|
||||
# host, never in the container.
|
||||
ENV_FILE="$(mktemp -t azaion-runtime-env.XXXXXX)"
|
||||
chmod 600 "$ENV_FILE"
|
||||
trap 'rm -f "$ENV_FILE"' EXIT INT TERM
|
||||
|
||||
env | grep -E '^(ASPNETCORE_|AZAION_)' > "$ENV_FILE" || true
|
||||
|
||||
mkdir -p "$DEPLOY_HOST_CONTENT_DIR" "$DEPLOY_HOST_LOGS_DIR"
|
||||
|
||||
log_info "Starting $DEPLOY_CONTAINER_NAME from $IMAGE on host port $DEPLOY_HOST_PORT"
|
||||
docker run --detach \
|
||||
--name "$DEPLOY_CONTAINER_NAME" \
|
||||
--restart unless-stopped \
|
||||
--env-file "$ENV_FILE" \
|
||||
--publish "$DEPLOY_HOST_PORT:8080" \
|
||||
--volume "$DEPLOY_HOST_CONTENT_DIR:/app/Content" \
|
||||
--volume "$DEPLOY_HOST_LOGS_DIR:/app/logs" \
|
||||
"$IMAGE" >/dev/null
|
||||
|
||||
log_info "Container ID: $(docker container inspect -f '{{.Id}}' "$DEPLOY_CONTAINER_NAME" | cut -c1-12)"
|
||||
log_info "Running revision label: $(current_image_revision "$DEPLOY_CONTAINER_NAME")"
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/stop-services.sh — graceful stop + record the previous image SHA so
|
||||
# `./scripts/deploy.sh --rollback` can find a target.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/stop-services.sh [--help]
|
||||
|
||||
Reads from the environment:
|
||||
DEPLOY_CONTAINER_NAME name of the container to stop
|
||||
REGISTRY_TAG (optional, for logging only)
|
||||
|
||||
Side-effect: writes scripts/.previous_tags.env containing PREVIOUS_SHA_TAG so
|
||||
the next deploy can roll back to whatever was running just before this stop.
|
||||
EOF
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && { usage; exit 0; }
|
||||
|
||||
require_env DEPLOY_CONTAINER_NAME
|
||||
require_cmd docker
|
||||
|
||||
PREV_FILE="$SCRIPT_DIR/.previous_tags.env"
|
||||
|
||||
if container_exists "$DEPLOY_CONTAINER_NAME"; then
|
||||
REVISION="$(current_image_revision "$DEPLOY_CONTAINER_NAME")"
|
||||
if [[ -n "$REVISION" ]]; then
|
||||
SHA12="$(echo "$REVISION" | cut -c1-12)"
|
||||
SUFFIX="${REGISTRY_TAG##*-}" # arm / amd; falls back to whatever follows the last dash
|
||||
PREV_TAG="${SHA12}-${SUFFIX:-arm}"
|
||||
printf 'PREVIOUS_SHA_TAG=%s\nPREVIOUS_REVISION=%s\nRECORDED_AT=%s\n' \
|
||||
"$PREV_TAG" "$REVISION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$PREV_FILE"
|
||||
log_info "Recorded rollback target → $PREV_TAG (revision $REVISION) in $PREV_FILE"
|
||||
else
|
||||
log_warn "Could not read org.opencontainers.image.revision from $DEPLOY_CONTAINER_NAME — rollback target NOT recorded"
|
||||
fi
|
||||
|
||||
if container_running "$DEPLOY_CONTAINER_NAME"; then
|
||||
# 40s matches the grace period in deployment_procedures.md §1; the app
|
||||
# itself shuts down after ShutdownTimeout=30s leaving 10s headroom.
|
||||
log_info "Stopping $DEPLOY_CONTAINER_NAME (grace 40s)"
|
||||
docker stop -t 40 "$DEPLOY_CONTAINER_NAME"
|
||||
else
|
||||
log_info "$DEPLOY_CONTAINER_NAME exists but is not running"
|
||||
fi
|
||||
log_info "Removing container $DEPLOY_CONTAINER_NAME"
|
||||
docker rm -f "$DEPLOY_CONTAINER_NAME"
|
||||
else
|
||||
log_info "$DEPLOY_CONTAINER_NAME does not exist — nothing to stop"
|
||||
fi
|
||||
Reference in New Issue
Block a user