migrate yaml auth keys.sh
#!/usr/bin/env bash
migrate-yaml-auth-keys.sh
Migrates YAML-managed ingestion auth keys (Kubernetes secret +
ingester ConfigMap) to the UI-managed (Postgres-backed) form by calling
config-mgmt-service's GraphQL API.
Prerequisites:
- kubectl configured for the target cluster.
- jq, base64, curl.
- python3 with PyYAML installed (pip3 install PyYAML if missing).
- config-mgmt-service port-forwarded:
kubectl port-forward -n svc/config-mgmt-service 8080:8080
Usage:
./migrate-yaml-auth-keys.sh -n [options]
Options:
-n, --namespace NAME Kloudfuse namespace (default: kfuse)
--secret NAME Auth secret (default: kfuse-auth-ingest)
--configmap NAME Ingester ConfigMap (default: ingester)
--configmap-key KEY Key inside the ConfigMap (default: config.yaml)
--port PORT Local port for config-mgmt-service (default: 8080)
--admin-email EMAIL Email recorded as updatedBy
(default: yaml-migration@kloudfuse.local)
--token-name NAME Name for the imported token
(default: yaml-imported)
--dry-run Print what would be created and exit
-h, --help Show this help
set -euo pipefail
NAMESPACE="kfuse" SECRET="kfuse-auth-ingest" CONFIGMAP="ingester" CONFIGMAP_KEY="config.yaml" PORT="8080" ADMIN_EMAIL="yaml-migration@kloudfuse.local" TOKEN_NAME="yaml-imported" DRY_RUN=0
usage() { sed -n '/^# Usage:/,/^# *-h,/p' "$0" | sed 's/^# {0,1}//'; exit 0; }
while [[ $# -gt 0 ]]; do case "$1" in -n|--namespace) NAMESPACE="$2"; shift 2 ;; --secret) SECRET="$2"; shift 2 ;; --configmap) CONFIGMAP="$2"; shift 2 ;; --configmap-key) CONFIGMAP_KEY="$2"; shift 2 ;; --port) PORT="$2"; shift 2 ;; --admin-email) ADMIN_EMAIL="$2"; shift 2 ;; --token-name) TOKEN_NAME="$2"; shift 2 ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) usage ;; *) echo "Unknown argument: $1" >&2; exit 2 ;; esac done
GQL_URL="http://localhost:${PORT}/config/graph/api"
---------------------------------------------------------------------------
Read tokens from the secret. Each .data. is a base64-encoded
token. We collapse displayName to lowercase + trimmed, matching the loader.
Output: TSV "displayName\tplaintextToken" (one per line).
---------------------------------------------------------------------------
read_secret_tokens() {
local raw
if ! raw=$(kubectl get secret "$SECRET" -n "$NAMESPACE" -o json 2>/dev/null); then
echo "ERROR: cannot read secret $SECRET in namespace $NAMESPACE" >&2
exit 1
fi
echo "$raw" | python3 -c '
import json, sys, base64\
doc = json.load(sys.stdin)
data = doc.get("data") or {}
for name, b64 in data.items():
name_clean = name.strip().lower()
if not name_clean:
continue
try:
token = base64.b64decode(b64).decode("utf-8").strip()
except Exception as e:
print(f"WARN: skipping {name}: cannot base64-decode ({e})", file=sys.stderr)
continue
if not token:
continue
print(f"{name_clean}\t{token}")
'
}
---------------------------------------------------------------------------
Read authKeyAdditionalLabels from the ConfigMap. We accept either
top-level (matching how config-mgmt-service mounts it) or nested under
ingester.config (matching how helm/custom_values.yaml is typically
structured).
Output: JSON object keyed by lowercased displayName, value is an array
of {name, value} entries. Empty {} if no labels.
---------------------------------------------------------------------------
read_configmap_labels() {
local raw
if ! raw=$(kubectl get cm "$CONFIGMAP" -n "$NAMESPACE" -o json 2>/dev/null); then
echo "WARN: cannot read configmap $CONFIGMAP in namespace $NAMESPACE — proceeding with no labels" >&2
echo "{}"
return
fi
local content
content=$(echo "$raw" | jq -r --arg k "$CONFIGMAP_KEY" '.data[$k] // empty')
if [[ -z "$content" ]]; then
echo "WARN: configmap $CONFIGMAP has no key $CONFIGMAP_KEY — proceeding with no labels" >&2
echo "{}"
return
fi
echo "$content" | python3 -c '
import sys, json, yaml
doc = yaml.safe_load(sys.stdin) or {}
candidates = [
doc.get("authKeyAdditionalLabels"),
(doc.get("ingester") or {}).get("config", {}).get("authKeyAdditionalLabels") if isinstance(doc.get("ingester"), dict) else None,
(doc.get("config") or {}).get("authKeyAdditionalLabels") if isinstance(doc.get("config"), dict) else None,
]
labels = next((c for c in candidates if isinstance(c, dict)), {}) or {}
out = {}
for name, entries in labels.items():
name_clean = (name or "").strip().lower()
if not name_clean or not isinstance(entries, list):
continue
cleaned = []
for e in entries:
if not isinstance(e, dict):
continue
n = str(e.get("name", "")).strip()
if not n:
continue
cleaned.append({"name": n, "value": str(e.get("value", ""))})
out[name_clean] = cleaned
print(json.dumps(out))
'
}
---------------------------------------------------------------------------
GraphQL helpers. RBAC headers spoof an admin caller.
---------------------------------------------------------------------------
gql() {
local query="$1" vars="${2:-{}}"
local payload
payload=$(python3 -c '
import json, sys
print(json.dumps({"query": sys.argv[1], "variables": json.loads(sys.argv[2])}))
' "$query" "$vars")
curl -sS -X POST "$GQL_URL"
-H 'Content-Type: application/json'
-H 'x-auth-request-role: admin'
-H "x-auth-request-email: ${ADMIN_EMAIL}"
-d "$payload"
}
list_existing_display_names() {
local body
body=$(gql 'query{ listAuthKeyLabels(limit: 1000){ items{ id displayName source } total } }' '{}')
echo "$body" | python3 -c '
import sys, json
doc = json.load(sys.stdin)
errs = doc.get("errors") or []
if errs:
sys.stderr.write("GraphQL errors: " + json.dumps(errs) + "\n")
sys.exit(1)
resp = ((doc.get("data") or {}).get("listAuthKeyLabels") or {})
items = resp.get("items") or []
for i in items:\
Skip yaml-sourced rows — those are exactly what the migration is\
supposed to shadow. Only treat UI-managed entries (source != "yaml")\
as "already exists" so the migration is not blocked by its own input.\
if (i.get("source") or "").strip().lower() == "yaml":
continue
print((i.get("displayName") or "").strip().lower())
'
}
create_label() {
local display_name="$1" token="$2" labels_json="$3"
local vars
vars=$(python3 -c '
import json, sys
display_name, token_name, token, labels = sys.argv[1], sys.argv[2], sys.argv[3], json.loads(sys.argv[4])
print(json.dumps({"input": {
"displayName": display_name,
"tokenName": token_name,
"token": token,
"labels": labels,
}}))
' "$display_name" "$TOKEN_NAME" "$token" "$labels_json")
gql 'mutation($input: CreateAuthKeyLabelInput!){
createAuthKeyLabel(input: $input){ id displayName }
}' "$vars"
}
---------------------------------------------------------------------------
Main
---------------------------------------------------------------------------
bold "==> Reading YAML auth keys from cluster" echo " namespace : $NAMESPACE" echo " secret : $SECRET" echo " configmap : $CONFIGMAP (key=$CONFIGMAP_KEY)" echo " target : $GQL_URL" echo " dry-run : $( [[ $DRY_RUN -eq 1 ]] && echo yes || echo no)" echo
TOKENS_TSV=$(read_secret_tokens) LABELS_JSON=$(read_configmap_labels)
if [[ -z "$TOKENS_TSV" ]]; then yellow "No tokens found in secret. Nothing to migrate." exit 0 fi
bold "==> Discovered yaml entries:" echo "$TOKENS_TSV" | awk -F'\t' '{ print " " $1 }' echo
Surface label-only entries (declared in authKeyAdditionalLabels but with
no matching token in the auth secret). The script can't migrate them —
without a token there's nothing to authenticate — but operators should
know these will be dropped when authKeyAdditionalLabels is cleaned up.
ORPHAN_LABELS=$(LABELS_JSON_ENV="$LABELS_JSON" TOKENS_TSV_ENV="$TOKENS_TSV" python3 -c '
import json, os
labels = json.loads(os.environ.get("LABELS_JSON_ENV") or "{}") or {}
tokens = set()
for line in (os.environ.get("TOKENS_TSV_ENV") or "").splitlines():
if not line.strip():
continue
tokens.add(line.split("\t", 1)[0].strip().lower())
for name in labels:
if name.strip().lower() not in tokens:
print(name)
')
if [[ -n "$ORPHAN_LABELS" ]]; then bold "==> Orphan label entries (declared in authKeyAdditionalLabels but no matching token in secret):" yellow " These entries cannot authenticate ingest traffic and will not be migrated." echo "$ORPHAN_LABELS" | sed 's/^/ /' echo fi
Always query existing UI-managed entries — dry-run also needs this so it
can accurately predict which entries would be skipped on a real run.
bold "==> Listing existing UI-managed entries" if ! EXISTING=$(list_existing_display_names 2>&1); then red "Failed to query existing labels — is config-mgmt-service reachable on :$PORT?" echo "$EXISTING" >&2 exit 1 fi if [[ -n "$EXISTING" ]]; then echo "$EXISTING" | sed 's/^/ /' else echo " (none)" fi echo
CREATED=0 SKIPPED=0 FAILED=0
bold "==> Migrating" while IFS=$'\t' read -r display_name token; do [[ -z "$display_name" ]] && continue labels=$(echo "$LABELS_JSON" | jq -c --arg k "$display_name" '.[$k] // []')
if echo "$EXISTING" | grep -Fxq "$display_name"; then if [[ $DRY_RUN -eq 1 ]]; then yellow " [dry-skip] $display_name — already exists in UI" else yellow " [skip] $display_name — already exists in UI" fi SKIPPED=$((SKIPPED + 1)) continue fi
if [[ $DRY_RUN -eq 1 ]]; then label_count=$(echo "$labels" | jq 'length') green " [dry] $display_name (labels=$label_count, token=${token:0:8}…)" continue fi
resp=$(create_label "$display_name" "$token" "$labels" || true)
err=$(echo "$resp" | python3 -c '
import sys, json
try:
d = json.load(sys.stdin)
except Exception as e:
print(f"unparseable response: {e}")
sys.exit(0)
errs = d.get("errors") or []
if errs:
print(errs[0].get("message", "unknown error"))
' 2>/dev/null || echo "parse error")
if [[ -n "$err" ]]; then red " [fail] $display_name — $err" FAILED=$((FAILED + 1)) else green " [create] $display_name" CREATED=$((CREATED + 1)) fi done <<< "$TOKENS_TSV"
echo bold "==> Summary" echo " created : $CREATED" echo " skipped : $SKIPPED" echo " failed : $FAILED"
if [[ $DRY_RUN -eq 0 && $FAILED -eq 0 && $CREATED -gt 0 ]]; then echo bold "Next steps:" echo " 1. Verify in the UI (Admin > Settings > Auth key labels) that all" echo " YAML entries now have a matching UI-managed entry that" echo " 'shadows' the YAML row." echo " 2. Verify ingestion is still healthy." echo " 3. Delete the secret and remove authKeyAdditionalLabels from" echo " custom_values.yaml, then restart config-mgmt-service:" echo " kubectl delete secret $SECRET -n $NAMESPACE" echo " kubectl rollout restart deploy/config-mgmt-service -n $NAMESPACE" fi
[[ $FAILED -eq 0 ]] || exit 1