reef-storage/tools/create-platform-audit-bucket.sh

139 lines
5.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# Create the planned Scaleway backup bucket. Reads bootstrap creds from
# OpenBao. Never prints secret values. Writes only non-secret attributes
# into substrate/object-stores/platform-audit-storage.yaml.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ATTR="$ROOT/substrate/object-stores/platform-audit-storage.yaml"
BAO_ADDR="${BAO_ADDR:-${VAULT_ADDR:-https://bao.coulomb.social}}"
BOOTSTRAP_PATH="platform/workloads/railiance/scaleway/bootstrap"
SCOPED_PATH="platform/workloads/railiance/backup/object-storage"
REGION="nl-ams"
BUCKET="${BUCKET:-railiance-platform-pg-backup}"
PREFIX="${PREFIX:-platform-pg/}"
ENDPOINT="https://s3.nl-ams.scw.cloud"
need() { command -v "$1" >/dev/null || { echo "missing $1" >&2; exit 2; }; }
export PATH="${HOME}/.local/bin:${PATH}"
need python3
need curl
need scw
TOKEN="${OPENBAO_TOKEN:-${VAULT_TOKEN:-}}"
if [[ -z "$TOKEN" && -f "$HOME/.vault-token" ]]; then
TOKEN="$(cat "$HOME/.vault-token")"
fi
[[ -n "$TOKEN" ]] || { echo "no OpenBao token" >&2; exit 2; }
read_kv() {
local path="$1"
curl -fsS -H "X-Vault-Token: $TOKEN" \
"$BAO_ADDR/v1/platform/data/${path#platform/}" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(json.dumps(d["data"]["data"]))'
}
if ! BOOTSTRAP_JSON="$(read_kv workloads/railiance/scaleway/bootstrap 2>/dev/null)"; then
echo "Bootstrap secret missing at $BOOTSTRAP_PATH" >&2
echo "Founder: put ACCESS_KEY SECRET_KEY DEFAULT_ORGANIZATION_ID DEFAULT_PROJECT_ID locally (not in chat)." >&2
echo "See docs/put-scaleway-bootstrap.md" >&2
exit 3
fi
eval "$(BOOTSTRAP_JSON="$BOOTSTRAP_JSON" python3 - <<'PY'
import json, os, sys
data = json.loads(os.environ["BOOTSTRAP_JSON"])
needed = ("ACCESS_KEY", "SECRET_KEY", "DEFAULT_ORGANIZATION_ID", "DEFAULT_PROJECT_ID")
missing = [k for k in needed if not (data.get(k) or data.get(k.lower()))]
if missing:
sys.stderr.write("bootstrap fields missing: " + ",".join(missing) + "\n")
sys.exit(4)
placeholders = [k for k in needed if (data.get(k) or data.get(k.lower()) or "") in {"xxx", "redacted", "changeme"}]
if placeholders:
sys.stderr.write("bootstrap still has example placeholders; replace them in the OpenBao UI first\n")
sys.exit(5)
def g(k):
return data.get(k) or data.get(k.lower())
print("export SCW_ACCESS_KEY=" + json.dumps(g("ACCESS_KEY")))
print("export SCW_SECRET_KEY=" + json.dumps(g("SECRET_KEY")))
print("export SCW_DEFAULT_ORGANIZATION_ID=" + json.dumps(g("DEFAULT_ORGANIZATION_ID")))
print("export SCW_DEFAULT_PROJECT_ID=" + json.dumps(g("DEFAULT_PROJECT_ID")))
PY
)"
export SCW_DEFAULT_REGION="$REGION"
if scw object bucket get "$BUCKET" region="$REGION" -o json >/dev/null 2>&1; then
echo "bucket $BUCKET already exists in $REGION; skipping create"
else
echo "creating private bucket $BUCKET in $REGION (versioning on)"
scw object bucket create "$BUCKET" region="$REGION" acl=private enable-versioning=true -o json
fi
echo "applying 30-day lifecycle (current + noncurrent versions)"
VENV="${TMPDIR:-/tmp}/reef-storage-boto3"
if [[ ! -x "$VENV/bin/python" ]]; then
python3 -m venv "$VENV"
"$VENV/bin/pip" -q install boto3
fi
BUCKET="$BUCKET" AWS_ACCESS_KEY_ID="$SCW_ACCESS_KEY" AWS_SECRET_ACCESS_KEY="$SCW_SECRET_KEY" \
"$VENV/bin/python" - <<'PY'
import os
import boto3
client = boto3.client(
"s3",
region_name="nl-ams",
endpoint_url="https://s3.nl-ams.scw.cloud",
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
)
client.put_bucket_lifecycle_configuration(
Bucket=os.environ["BUCKET"],
LifecycleConfiguration={
"Rules": [
{
"ID": "retain-30-days",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"Expiration": {"Days": 30},
"NoncurrentVersionExpiration": {"NoncurrentDays": 30},
}
]
},
)
print("lifecycle applied")
PY
echo "creating a 20 EUR monthly budget alert if none exists"
scw billing budget create consumption-limit=20 enabled=true -o json | python3 -c '
import json,sys
b=json.load(sys.stdin)
print("budget_id", b.get("id") or b.get("budget",{}).get("id"))
print("limit", b.get("consumption_limit") or b.get("budget",{}).get("consumption_limit"))
' || echo "budget create skipped (permissions or API shape)"
echo "writing non-secret attributes (no keys)"
python3 - <<PY
from pathlib import Path
p = Path("$ATTR")
text = p.read_text()
repl = {
"status: planned": "status: active",
"endpoint: null": "endpoint: $ENDPOINT",
"region: nl-ams": "region: $REGION",
"bucket: null": "bucket: $BUCKET",
"prefix: null": "prefix: $PREFIX",
"versioning: null": "versioning: true",
"lifecycle: null": "lifecycle: 30-day current and noncurrent expiration",
"provider_project_ref: null": "provider_project_ref: ${SCW_DEFAULT_PROJECT_ID}",
}
for a,b in repl.items():
text = text.replace(a, b, 1)
p.write_text(text)
print(f"updated {p}")
print(f"endpoint={ENDPOINT} bucket={BUCKET} region={REGION}")
PY
echo "Cost alert: set a project budget in the Scaleway console if scw billing is unavailable."
echo "Scoped backup key is T04: $SCOPED_PATH (not written here)."
echo "done. Commit the YAML; do not commit any key."