diff --git a/internal/enterprise/assets/install-advisor.sh b/internal/enterprise/assets/install-advisor.sh index 1d89fec..871fcfd 100644 --- a/internal/enterprise/assets/install-advisor.sh +++ b/internal/enterprise/assets/install-advisor.sh @@ -3,7 +3,8 @@ # advisor_register only pushes the chart + prereqs to Harbor; this installs # the chart and confirms the advisor serves. Idempotent: safe to re-run. # -# args: [base_url] [console_pool] [console_account] +# args: [base_url] [console_pool] +# [console_account] [provider_key_file] [provider_url] set -uo pipefail FRAMEWORK="${1:?framework name required}" ADVISOR_LB_IP="${2:?advisor lb ip required}" @@ -27,6 +28,10 @@ CONSOLE_POOL="${5:-}" # provisions "advisor" for exactly this, so that is the default; empty leaves # the console disabled, which is what every install did before. CONSOLE_ACCOUNT="${6:-advisor}" +# The inference endpoint the chat surface calls, and a file holding its key. +# Both optional: given, they set it; omitted, whatever the deployment already +# uses is carried through. +PROVIDER_URL="${8:-}" NS=cube-advisor fail() { echo "ERROR: $*" >&2; exit 1; } @@ -242,23 +247,71 @@ if [ "${#POOL_ADDRS[@]}" -ge 2 ]; then WC_ARGS+=(--set webConsole.enabled=true --set webConsole.mode=address) n=0 if [ -n "$INGRESS" ]; then + # /portal, not /: the framework ingress serves the portal there and + # Keycloak under /auth, and claims nothing at the root. WC_ARGS+=(--set "webConsole.origins[$n].address=${POOL_ADDRS[$n]}" \ --set "webConsole.origins[$n].upstream=https://$INGRESS" \ + --set "webConsole.origins[$n].path=/portal" \ --set "webConsole.origins[$n].targets[0]=cube-cmp" \ --set "webConsole.origins[$n].targets[1]=app-fw-idp") n=$((n+1)) else echo "warning: no ingress-lb on framework $FRAMEWORK; the CMP console origin is not configured" >&2 fi - # The node's own dashboard, which every enrolled node serves locally. + # The node's own dashboard, at the control VIP rather than loopback: + # 127.0.0.1:8080 is httpd, which answers 403 to everything. + # + # The three endpoints it links out to are companions rather than origins of + # their own: the dashboard sends the browser straight at them, so they must + # be reachable from one session, and they share its address and its LB. WC_ARGS+=(--set "webConsole.origins[$n].address=${POOL_ADDRS[$n]}" \ - --set "webConsole.origins[$n].upstream=http://127.0.0.1:8080" \ + --set "webConsole.origins[$n].upstream=https://$CTRL" \ --set "webConsole.origins[$n].targets[0]=cube-cos") + c=0 + for pair in "10443:cube-cos-idp" "9999:cube-cos-skyline" "7443:cube-cos-ceph"; do + port="${pair%%:*}"; name="${pair#*:}" + WC_ARGS+=(--set "webConsole.origins[$n].companions[$c].address=${POOL_ADDRS[$n]}:$port" \ + --set "webConsole.origins[$n].companions[$c].upstream=https://$CTRL:$port" \ + --set "webConsole.origins[$n].companions[$c].targets[0]=$name") + c=$((c+1)) + done echo "web console enabled on ${#POOL_ADDRS[@]} origin address(es)." elif [ -n "$CONSOLE_POOL" ]; then echo "warning: the console pool has fewer than 2 addresses; leaving the web console off" >&2 fi +# --- the inference provider --- +# Carried through a re-run, like the TLS and enrollment material above and for +# the same reason: this script renders the whole release, so a value it does +# not pass back is one helm removes. +PROVIDER_ARGS=() +PROVIDER_KEY_FILE="${7:-}" +if [ -n "$PROVIDER_KEY_FILE" ]; then + [ -r "$PROVIDER_KEY_FILE" ] || fail "cannot read the provider key file: $PROVIDER_KEY_FILE" + PROVIDER_ARGS+=(--set-file provider.key="$PROVIDER_KEY_FILE") + [ -n "$PROVIDER_URL" ] && PROVIDER_ARGS+=(--set provider.url="$PROVIDER_URL") + echo "provider set to ${PROVIDER_URL:-the chart default}." +else + # Read back what the deployment is already using. The key lives in the + # Secret; the URL is an argument on the container. + PREV_KEY="$($K -n "$NS" get secret cube-advisor-secrets -o jsonpath='{.data.providerKey}' 2>/dev/null | base64 -d)" + # By position in the argument list: a jsonpath range piped through grep + # silently produced nothing, carrying the key while the URL fell back. + PREV_URL="$($K -n "$NS" get deploy cube-advisor -o json 2>/dev/null | jq -r ' + .spec.template.spec.containers[0].args as $a + | ($a | index("-provider-url")) as $i + | if $i == null then empty else $a[$i + 1] end')" + if [ -n "$PREV_KEY" ]; then + PROVIDER_KEEP="$(mktemp)"; TMPDIRS+=("$PROVIDER_KEEP") + printf '%s' "$PREV_KEY" > "$PROVIDER_KEEP" + PROVIDER_ARGS+=(--set-file provider.key="$PROVIDER_KEEP") + echo "carrying the existing provider key through the upgrade." + fi + if [ -n "$PREV_URL" ]; then + PROVIDER_ARGS+=(--set provider.url="$PREV_URL") + fi +fi + # --- the console's SSH user CA --- # advisor-api generates one at startup when the chart passes no key, which # suits a dev run and breaks a deployment: a node pins this CA in sshd and @@ -293,6 +346,7 @@ helm upgrade --install cube-advisor "oci://$RURL/$RPROJ/cube-advisor" --version "${ENROLL_ARGS[@]}" \ "${WC_ARGS[@]}" \ "${CONSOLE_ARGS[@]}" \ + "${PROVIDER_ARGS[@]}" \ --kube-insecure-skip-tls-verify --insecure-skip-tls-verify --timeout 20m --wait=false \ || fail "helm upgrade failed — the release is unchanged; do not uninstall to retry, that deletes the database" diff --git a/internal/enterprise/assets_test.go b/internal/enterprise/assets_test.go index b27c1e7..0f1ec95 100644 --- a/internal/enterprise/assets_test.go +++ b/internal/enterprise/assets_test.go @@ -228,3 +228,65 @@ func TestAdvisorInstallPrintsTheConsoleCAForTheNodes(t *testing.T) { "the console CA, so the console cannot work end to end") } } + +// The script renders the whole release, so a value it does not pass back is a +// value helm removes. The provider was not carried, so every re-run reset the +// chat surface to the chart's placeholder endpoint and dropped the key — +// leaving a deployment that reports "chat enabled" and fails on the first +// question. Seen twice on the 1cc r630, the second time caused by deploying an +// unrelated fix. +func TestAdvisorInstallCarriesTheProviderThroughAnUpgrade(t *testing.T) { + for _, want := range []string{ + "get secret cube-advisor-secrets -o jsonpath='{.data.providerKey}'", + "--set-file provider.key=", + "carrying the existing provider key through the upgrade", + } { + if !contains(installAdvisorScript, want) { + t.Errorf("the advisor installer does not %q; a re-run would reset the "+ + "provider and the chat surface would fail on its first question", want) + } + } +} + +// 127.0.0.1:8080 is httpd, which answers 403 to everything. The dashboard is +// nginx on the management address, so a cube-cos target pointed at loopback +// looks configured and refuses every request — which is exactly how it +// presented: a proxied tab showing "403 Forbidden" with nothing in any log. +func TestAdvisorConsolePointsCubeCosAtTheDashboard(t *testing.T) { + if contains(installAdvisorScript, "upstream=http://127.0.0.1:8080") { + t.Error("cube-cos still points at 127.0.0.1:8080, which is httpd and 403s; " + + "the dashboard is served on the control address") + } + if !contains(installAdvisorScript, `webConsole.origins[$n].upstream=https://$CTRL`) { + t.Error("cube-cos does not point at the control address the rest of this " + + "script already resolves") + } +} + +// The dashboard sends the browser at Keycloak on :10443 of the same address, +// so that endpoint has to be part of the same openable group — a companion, +// The dashboard links out to Keycloak, Skyline and the Ceph dashboard, each +// on another port of the same address. Every one has to be part of the same +// openable group -- a companion, not a separate origin the session does not +// carry, which would answer "no such origin" the moment the browser followed +// the link. +func TestAdvisorInstallGivesTheDashboardItsCompanions(t *testing.T) { + for _, want := range []string{ + `"10443:cube-cos-idp" "9999:cube-cos-skyline" "7443:cube-cos-ceph"`, + "webConsole.origins[$n].companions[$c].address=${POOL_ADDRS[$n]}:$port", + "webConsole.origins[$n].companions[$c].upstream=https://$CTRL:$port", + "webConsole.origins[$n].companions[$c].targets[0]=$name", + } { + if !contains(installAdvisorScript, want) { + t.Errorf("install-advisor.sh does not set %s", want) + } + } +} + +// The framework ingress serves the portal under /portal and claims nothing at +// the root, so an origin opened at / lands on the ingress's own 404. +func TestAdvisorInstallOpensTheCmpPortalAtItsOwnPath(t *testing.T) { + if !contains(installAdvisorScript, "webConsole.origins[$n].path=/portal") { + t.Error("install-advisor.sh does not give the CMP origin its landing path") + } +}