diff --git a/deploy/stacks/self-managed/environments/base.yaml b/deploy/stacks/self-managed/environments/base.yaml index 060f87def..25ae155fe 100644 --- a/deploy/stacks/self-managed/environments/base.yaml +++ b/deploy/stacks/self-managed/environments/base.yaml @@ -291,8 +291,9 @@ addons: # LLM addon: gateway + request router (stargate) for LLM function invocation llm: enabled: false - # QUIC TLS certificate for the request router (Stargate). Disabled by - # default; opt in per env. + # QUIC TLS certificate for the request router (Stargate). Managed PKI is + # active by default whenever addons.llm.enabled is true. Set enabled=false + # only when an environment deliberately supplies another transport policy. # # mode selects who owns the server identity: # certManager - the chart requests a Certificate from the configured @@ -306,15 +307,22 @@ addons: # issuance, renewal, rotation, and recovery, and the # stack validates neither the SANs nor the expiry. pki: - enabled: false + enabled: true mode: certManager - # REQUIRED for a managed issuer. Comma-separated DNS suffixes the - # OpenBao PKI role accepts. Typically the customer domain plus - # cluster.local for in-cluster service identity. - allowedDomains: "" - # REQUIRED for mode certManager. SANs requested on the issued - # certificate. Must be empty for mode existingSecret. - dnsNames: [] + # Comma-separated DNS suffixes the managed OpenBao role accepts. For a + # split/multi-cluster deployment, extend this to cover the suffix of each + # external router DNS name used by compute workers. + allowedDomains: cluster.local + # SANs for both the stable Service and authority/SNI-selected Stargate + # pod identities. For a split/multi-cluster deployment, also add every + # external router DNS name configured through + # addons.llm.requestRouter.backendRouter.pylonGrpcDialAddress or + # addons.llm.requestRouter.backendRouter.pylonReverseTunnelDialAddress. + # The reverse-tunnel hostname is used as QUIC SNI and must match this + # Certificate. Must be empty for mode existingSecret. + dnsNames: + - llm-request-router.nvcf.svc.cluster.local + - "*.llm-request-router-headless.nvcf.svc.cluster.local" # REQUIRED for mode existingSecret; the kubernetes.io/tls Secret in the # nvcf namespace holding the tls.crt and tls.key entries. Optional for # mode certManager, where it names the Secret cert-manager writes. @@ -332,8 +340,8 @@ addons: # certPath: /etc/stargate/tls/tls.crt # keyPath: /etc/stargate/tls/tls.key # Image defaults to ${global.image.registry}/${global.image.repository}/nvcf-openbao-migrations. - # tag falls back to openbao.migrations.image.tag if unset here, so the - # operator can pin both in one place. + # tag falls back to openbao.migrations.image.tag and then 0.16.2 if + # neither location is set. # image: # registry: "" # repository: "" diff --git a/deploy/stacks/self-managed/global.yaml.gotmpl b/deploy/stacks/self-managed/global.yaml.gotmpl index 531b6b02e..282d50ff5 100644 --- a/deploy/stacks/self-managed/global.yaml.gotmpl +++ b/deploy/stacks/self-managed/global.yaml.gotmpl @@ -997,13 +997,10 @@ llmRequestRouter: quicInsecure: false {{- if $managedIssuer }} {{- $allowedDomains := required "addons.llm.pki.allowedDomains is required when addons.llm.pki.clusterIssuer management is enabled" (dig "addons" "llm" "pki" "allowedDomains" "" .Values) }} - {{- /* Provisioning hook reuses the same image the k8s-openbao chart drives; - fall back to openbao.migrations.image.tag so operators set the version - in one place. */ -}} - {{- $pkiImageTag := dig "addons" "llm" "pki" "image" "tag" (dig "openbao" "migrations" "image" "tag" "" .Values) .Values }} - {{- if not $pkiImageTag }} - {{- fail "addons.llm.pki.image.tag (or openbao.migrations.image.tag) is required when addons.llm.pki.clusterIssuer management is enabled" }} - {{- end }} + {{- /* Provisioning reuses the OpenBao migrations image. Prefer an explicit + PKI tag, then the OpenBao migrations tag, and finally the compatible + stack default so a normal environment need not duplicate chart values. */ -}} + {{- $pkiImageTag := dig "addons" "llm" "pki" "image" "tag" (dig "openbao" "migrations" "image" "tag" "" .Values) .Values | default "0.16.2" }} pki: enabled: true namespace: {{ dig "addons" "llm" "pki" "namespace" "vault-system" .Values | quote }} diff --git a/deploy/stacks/self-managed/tests/check-llm-pki-issuer.sh b/deploy/stacks/self-managed/tests/check-llm-pki-issuer.sh index ffe0f9918..885b89fff 100755 --- a/deploy/stacks/self-managed/tests/check-llm-pki-issuer.sh +++ b/deploy/stacks/self-managed/tests/check-llm-pki-issuer.sh @@ -195,6 +195,29 @@ render_router() { >"$manifests_file" } +render_default_router() { + local case_name="$1" + local values_file="$work_dir/$case_name.router-values.yaml" + local manifests_file="$work_dir/$case_name.router-manifests.yaml" + local router_chart="$stack_dir/../../helm/llm-request-router/llm-request-router" + + HELMFILE_ENV=base HELMFILE_CACHE_HOME="$work_dir/helmfile-cache" helmfile \ + --file "$stack_dir/helmfile.d/02-core.yaml.gotmpl" \ + --environment default \ + --selector name=llm-request-router \ + --chart "$router_chart" \ + --skip-deps \ + "${core_state_values[@]}" \ + --state-values-set addons.llm.enabled=true \ + write-values \ + --output-file-template "$values_file" >/dev/null + + helm template llm-request-router "$router_chart" \ + --namespace nvcf \ + --values "$values_file" \ + >"$manifests_file" +} + expect_external_router() { local case_name="$1" local issuer_kind="$2" @@ -241,12 +264,19 @@ expect_external_router() { } # existingSecret mode cannot reuse render_router: that helper always passes -# dnsNames, which this mode rejects as a mixed-ownership conflict. +# managed issuance values, which this mode rejects as a mixed-ownership +# conflict. Clear the managed defaults before applying each case override. render_existing_secret_router() { local case_name="$1" shift local values_file="$work_dir/$case_name.router-values.yaml" local router_chart="$stack_dir/../../helm/llm-request-router/llm-request-router" + local ownership_override_file + ownership_override_file="$(pki_override "$case_name-existing-secret-ownership" <<'YAML' +allowedDomains: "" +dnsNames: [] +YAML +)" HELMFILE_ENV=base HELMFILE_CACHE_HOME="$work_dir/helmfile-cache" helmfile \ --file "$stack_dir/helmfile.d/02-core.yaml.gotmpl" \ @@ -258,6 +288,7 @@ render_existing_secret_router() { --state-values-set addons.llm.enabled=true \ --state-values-set addons.llm.pki.enabled=true \ --state-values-set-string addons.llm.pki.mode=existingSecret \ + --state-values-file "$ownership_override_file" \ "$@" \ write-values \ --output-file-template "$values_file" \ @@ -361,10 +392,44 @@ expect_enabled llm-disabled false # Case 2: LLM enabled, PKI disabled. render_list pki-disabled \ --state-values-set addons.llm.enabled=true \ + --state-values-set addons.llm.pki.enabled=false \ --state-values-set addons.llm.pki.clusterIssuer.enabled=true expect_enabled pki-disabled false -# Case 3: LLM and PKI enabled with the default managed ClusterIssuer. +# Case 3: enabling LLM with no PKI overrides must select the managed issuer and +# render an identity that covers both the stable and per-pod router names. +render_list secure-defaults \ + --state-values-set addons.llm.enabled=true +expect_enabled secure-defaults true +render_default_router secure-defaults +secure_defaults_manifests="$work_dir/secure-defaults.router-manifests.yaml" +secure_defaults_issuer_kind="$( + yq ea -r 'select(.kind == "Certificate") | .spec.issuerRef.kind' \ + "$secure_defaults_manifests" +)" +secure_defaults_issuer_name="$( + yq ea -r 'select(.kind == "Certificate") | .spec.issuerRef.name' \ + "$secure_defaults_manifests" +)" +test "$secure_defaults_issuer_kind" = "ClusterIssuer" || + fail "secure defaults did not render Certificate issuer kind ClusterIssuer" +test "$secure_defaults_issuer_name" = "nvcf-openbao-pki" || + fail "secure defaults did not render Certificate issuer name nvcf-openbao-pki" +secure_defaults_dns_names="$( + yq ea -r 'select(.kind == "Certificate") | .spec.dnsNames[]' \ + "$secure_defaults_manifests" +)" +test "$secure_defaults_dns_names" = "$(printf '%s\n%s' \ + 'llm-request-router.nvcf.svc.cluster.local' \ + '*.llm-request-router-headless.nvcf.svc.cluster.local')" || + fail "secure defaults did not render the stable and per-pod request-router DNS names" +grep -Fq 'name: addons-llm-migrations' "$secure_defaults_manifests" || + fail "secure defaults did not render the managed OpenBao provisioning hook" +if grep -Fq -- '--quic-insecure' "$secure_defaults_manifests"; then + fail "secure defaults enabled insecure request-router transport" +fi + +# Case 4: LLM and PKI explicitly enabled with the default managed ClusterIssuer. managed_defaults=( --state-values-set addons.llm.enabled=true --state-values-set addons.llm.pki.enabled=true diff --git a/deploy/stacks/self-managed/tests/llm-pki-release.sh b/deploy/stacks/self-managed/tests/llm-pki-release.sh index 26c5f6865..b1b3d3dfd 100755 --- a/deploy/stacks/self-managed/tests/llm-pki-release.sh +++ b/deploy/stacks/self-managed/tests/llm-pki-release.sh @@ -34,6 +34,60 @@ render_debug() { list --skip-charts --output json 2>&1 } +# The managed issuer hook needs the same OpenBao migrations image as the +# control-plane chart. An environment should not have to duplicate its tag. +render_pki_image_case() { + local case_name="$1" + shift + + HELMFILE_ENV=base HELMFILE_CACHE_HOME="$work_dir/helmfile-cache" helmfile \ + --file "$stack_dir/helmfile.d" \ + --environment default \ + --output-file-template "$work_dir/$case_name-{{ .Release.Name }}.yaml" \ + --state-values-set ingress.gatewayApi.controllerNamespace=envoy-gateway-system \ + --state-values-set ingress.gatewayApi.gateways.shared.name=shared-gw \ + --state-values-set ingress.gatewayApi.gateways.shared.namespace=envoy-gateway-system \ + --state-values-set ingress.gatewayApi.gateways.grpc.name=grpc-gw \ + --state-values-set ingress.gatewayApi.gateways.grpc.namespace=envoy-gateway-system \ + --state-values-set addons.llm.enabled=true \ + --state-values-set addons.llm.pki.enabled=true \ + --state-values-set-string addons.llm.pki.allowedDomains=cluster.local \ + --state-values-set-string 'addons.llm.pki.dnsNames[0]=llm-request-router.nvcf.svc.cluster.local' \ + --state-values-set-string 'addons.llm.pki.dnsNames[1]=*.llm-request-router-headless.nvcf.svc.cluster.local' \ + "$@" \ + write-values --selector name=llm-request-router 2>&1 +} + +assert_pki_image_case() { + local case_name="$1" + local expected_tag="$2" + local values_file="$work_dir/$case_name-llm-request-router.yaml" + local manifests_file="$work_dir/$case_name-manifests.yaml" + local router_chart="$stack_dir/../../helm/llm-request-router/llm-request-router" + local expected_image="nvcr.io/YOUR_ORG/YOUR_TEAM/nvcf-openbao-migrations:$expected_tag" + local actual_tag actual_image + + actual_tag="$(yq -r '.llmRequestRouter.pki.image.tag' "$values_file")" + test "$actual_tag" = "$expected_tag" || + fail "$case_name resolved PKI image tag $actual_tag, expected $expected_tag" + + helm template llm-request-router "$router_chart" \ + --namespace nvcf \ + --values "$values_file" \ + >"$manifests_file" + + actual_image="$( + yq ea -r ' + select(.kind == "Job" and .metadata.name == "addons-llm-migrations") | + .spec.template.spec.containers[] | + select(.name == "addons-llm-migrations") | + .image + ' "$manifests_file" + )" + test "$actual_image" = "$expected_image" || + fail "$case_name rendered migrations image $actual_image, expected $expected_image" +} + # The rendered state is the only place that carries both the release # declarations and their needs edges. `list` reports enablement but drops # needs, and `build` cannot run offline because it pulls every chart. Debug @@ -72,6 +126,25 @@ state_table() { awk -F '|' '{ key = ($1 == "" ? $2 : $1 "/" $2); print key "\t" $3 }' } +if ! render_pki_image_case default-image-tag \ + --state-values-set-string openbao.migrations.image.tag= \ + >"$work_dir/default-image-tag.log"; then + cat "$work_dir/default-image-tag.log" >&2 + fail "managed LLM PKI must render when both PKI image tags are omitted" +fi +assert_pki_image_case default-image-tag 0.16.2 + +render_pki_image_case legacy-image-tag \ + --state-values-set-string openbao.migrations.image.tag=legacy-tag \ + >"$work_dir/legacy-image-tag.log" +assert_pki_image_case legacy-image-tag legacy-tag + +render_pki_image_case explicit-image-tag \ + --state-values-set-string openbao.migrations.image.tag=legacy-tag \ + --state-values-set-string addons.llm.pki.image.tag=explicit-tag \ + >"$work_dir/explicit-image-tag.log" +assert_pki_image_case explicit-image-tag explicit-tag + if ! render_debug >"$work_dir/debug.log"; then cat "$work_dir/debug.log" >&2 fail "helmfile could not render helmfile.d"