Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/deploy-environment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,20 @@ jobs:
id: db
run: echo "url=$(terraform output -raw dashboard_url 2>/dev/null || true)" >> $GITHUB_OUTPUT

- name: Get smoke test virtual key
id: smoke_key
shell: bash
run: |
set -euo pipefail
KV_NAME=$(printf 'pvc-%s-%s-kv' "${TF_VAR_env}" "${TF_VAR_projname}" | tr '[:upper:]_' '[:lower:]-' | cut -c1-24)
KEY=$(az keyvault secret show --vault-name "${KV_NAME}" --name vkey-dashboard-playground --query value -o tsv)
Comment on lines +309 to +311
if [[ -z "${KEY}" || "${KEY}" != sk-* ]]; then
echo "::error::Key Vault secret vkey-dashboard-playground in ${KV_NAME} must contain a LiteLLM virtual key beginning with sk-."
exit 1
fi
echo "::add-mask::${KEY}"
echo "key=${KEY}" >> "$GITHUB_OUTPUT"

- name: Runtime diagnostics (Container App config)
shell: bash
run: |
Expand Down Expand Up @@ -342,7 +356,7 @@ jobs:
uses: ./.github/actions/smoke-test-gateway
with:
gateway_url: ${{ steps.gw.outputs.url }}
gateway_key: ${{ secrets.AIGATEWAY_KEY }}
gateway_key: ${{ steps.smoke_key.outputs.key }}
embedding_model: ${{ env.TF_VAR_embedding_deployment }}
codex_model: ${{ env.TF_VAR_codex_model }}
aoai_endpoint: ${{ env.TF_VAR_azure_openai_endpoint }}
Expand Down
5 changes: 5 additions & 0 deletions docs/CI_CD.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ The composite action `.github/actions/smoke-test-gateway` performs:
- Candidate probing for embeddings if the requested model fails.
- Azure OpenAI deployment discovery fallback using configured endpoint/key when needed.

Deploy jobs authenticate those gateway smoke calls with the
`vkey-dashboard-playground` virtual key from the environment Key Vault. The
`AIGATEWAY_KEY` secret remains the LiteLLM master key used by Terraform/runtime
configuration, not the user-facing API key for smoke traffic.

Additionally, when `STATE_SERVICE_CONTAINER_IMAGE` is configured, `deploy.yaml` runs state-service smoke checks via dashboard proxy endpoints:

- `GET /api/state/catalog`
Expand Down
11 changes: 6 additions & 5 deletions infra/env/prod/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,12 @@ module "dashboard" {
tpm_limit = var.tpm_limit

# Entra OIDC plumbing (Item 11, Path 1). Only consumed when auth_mode = "entra".
auth_mode = var.dashboard_auth_mode
entra_tenant_id = var.entra_tenant_id
entra_client_id = var.entra_client_id
key_vault_id = module.sluice.key_vault_id
gateway_key_versionless_id = module.sluice.gateway_key_versionless_id
auth_mode = var.dashboard_auth_mode
entra_tenant_id = var.entra_tenant_id
entra_client_id = var.entra_client_id
key_vault_id = module.sluice.key_vault_id
gateway_key_versionless_id = module.sluice.gateway_key_versionless_id
gateway_proxy_key_secret_name = "vkey-dashboard-playground"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve admin credentials for the dashboard proxy

When prod runs in the default Entra mode, this wires the BFF's LITELLM_GATEWAY_KEY to the dashboard-playground virtual key instead of the master key. The dashboard admin page is restricted to dashboard.admin but its key explorer calls /key/list through dashboard/hooks/use-keys.ts; LiteLLM documents the master key as the Proxy Admin key (https://docs.litellm.ai/docs/proxy/virtual_keys), while the generated dashboard-playground key in scripts/keys.yaml only has model/budget limits. As a result, signed-in admins will no longer be able to list/manage gateway keys through the Entra dashboard and will fall back to seeing only that virtual key or a permission error. Keep the admin BFF path on the master/admin credential, or split playground/smoke traffic onto the virtual key separately.

Useful? React with 👍 / 👎.

}

output "gateway_url" {
Expand Down
16 changes: 12 additions & 4 deletions infra/modules/dashboard_aca/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ locals {
use_entra = var.auth_mode == "entra"

gateway_url = trimspace(var.gateway_public_url) != "" ? trimspace(var.gateway_public_url) : var.gateway_url
gateway_proxy_key_secret_name = trimspace(var.gateway_proxy_key_secret_name)
gateway_proxy_key_secret_id = local.gateway_proxy_key_secret_name != "" ? data.azurerm_key_vault_secret.gateway_proxy_key[0].versionless_id : var.gateway_key_versionless_id
Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix "Index out of bounds" evaluation error.

When a secret name is provided but auth_mode is not "entra", the ternary condition local.gateway_proxy_key_secret_name != "" will evaluate to true. This causes Terraform to evaluate data.azurerm_key_vault_secret.gateway_proxy_key[0]. Since local.use_entra is false, the data source count is 0, causing an immediate "Index out of bounds" failure during plan/apply.

Check the length of the conditionally created data source array instead.

🐛 Proposed fix
   gateway_proxy_key_secret_name = trimspace(var.gateway_proxy_key_secret_name)
-  gateway_proxy_key_secret_id = local.gateway_proxy_key_secret_name != "" ? data.azurerm_key_vault_secret.gateway_proxy_key[0].versionless_id : var.gateway_key_versionless_id
+  gateway_proxy_key_secret_id = length(data.azurerm_key_vault_secret.gateway_proxy_key) > 0 ? data.azurerm_key_vault_secret.gateway_proxy_key[0].versionless_id : var.gateway_key_versionless_id
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gateway_proxy_key_secret_name = trimspace(var.gateway_proxy_key_secret_name)
gateway_proxy_key_secret_id = local.gateway_proxy_key_secret_name != "" ? data.azurerm_key_vault_secret.gateway_proxy_key[0].versionless_id : var.gateway_key_versionless_id
gateway_proxy_key_secret_name = trimspace(var.gateway_proxy_key_secret_name)
gateway_proxy_key_secret_id = length(data.azurerm_key_vault_secret.gateway_proxy_key) > 0 ? data.azurerm_key_vault_secret.gateway_proxy_key[0].versionless_id : var.gateway_key_versionless_id
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@infra/modules/dashboard_aca/main.tf` around lines 19 - 20, Update the
gateway_proxy_key_secret_id expression to determine whether the conditional data
source exists by checking the length of
data.azurerm_key_vault_secret.gateway_proxy_key, rather than
gateway_proxy_key_secret_name. Preserve the existing fallback to
var.gateway_key_versionless_id when the data source array is empty.


# Derive the dashboard's public URL from the gateway URL by swapping the
# subdomain. Gateway: https://pvc-{env}-{proj}-ca.<suffix> → Dashboard:
Expand All @@ -40,7 +42,7 @@ data "azurerm_resource_group" "shared" {
# When auth_mode = "entra" the dashboard reads three KV secrets at runtime:
# - entra-client-secret (set out-of-band during Entra app provisioning)
# - dashboard-session-secret (next-auth JWE encryption key)
# - gateway-key (LiteLLM master key, owned by the sluice module)
# - vkey-dashboard-playground or configured gateway proxy key
data "azurerm_key_vault_secret" "entra_client_secret" {
count = local.use_entra ? 1 : 0
name = "entra-client-secret"
Expand All @@ -53,6 +55,12 @@ data "azurerm_key_vault_secret" "dashboard_session_secret" {
key_vault_id = var.key_vault_id
}

data "azurerm_key_vault_secret" "gateway_proxy_key" {
count = local.use_entra && local.gateway_proxy_key_secret_name != "" ? 1 : 0
name = local.gateway_proxy_key_secret_name
key_vault_id = var.key_vault_id
}

resource "azurerm_user_assigned_identity" "dashboard" {
count = local.use_entra ? 1 : 0
name = "${local.ca_name}-id"
Expand All @@ -77,8 +85,8 @@ resource "azurerm_container_app" "dashboard" {
error_message = "container_image must not be empty."
}
precondition {
condition = var.auth_mode != "entra" || (var.entra_tenant_id != "" && var.entra_client_id != "" && var.key_vault_id != "" && var.gateway_key_versionless_id != "")
error_message = "auth_mode = 'entra' requires entra_tenant_id, entra_client_id, key_vault_id, and gateway_key_versionless_id."
condition = var.auth_mode != "entra" || (var.entra_tenant_id != "" && var.entra_client_id != "" && var.key_vault_id != "" && local.gateway_proxy_key_secret_id != "")
error_message = "auth_mode = 'entra' requires entra_tenant_id, entra_client_id, key_vault_id, and either gateway_proxy_key_secret_name or gateway_key_versionless_id."
}
}

Expand Down Expand Up @@ -128,7 +136,7 @@ resource "azurerm_container_app" "dashboard" {
for_each = local.use_entra ? [1] : []
content {
name = "litellm-gateway-key"
key_vault_secret_id = var.gateway_key_versionless_id
key_vault_secret_id = local.gateway_proxy_key_secret_id
identity = azurerm_user_assigned_identity.dashboard[0].id
}
}
Expand Down
10 changes: 8 additions & 2 deletions infra/modules/dashboard_aca/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,18 @@ variable "entra_client_id" {

variable "key_vault_id" {
type = string
description = "Resource ID of the shared Key Vault. Required when auth_mode = 'entra' so the dashboard's UAMI can read entra-client-secret, dashboard-session-secret, and gateway-key. Empty otherwise."
description = "Resource ID of the shared Key Vault. Required when auth_mode = 'entra' so the dashboard's UAMI can read entra-client-secret, dashboard-session-secret, and the gateway proxy key. Empty otherwise."
default = ""
}

variable "gateway_key_versionless_id" {
type = string
description = "Versionless KV secret ID for the LiteLLM master key the BFF forwards to the gateway. Only consumed when auth_mode = 'entra'."
description = "Versionless KV secret ID for the fallback LiteLLM gateway key the BFF forwards to the gateway. Only consumed when auth_mode = 'entra' and gateway_proxy_key_secret_name is empty."
default = ""
}

variable "gateway_proxy_key_secret_name" {
type = string
description = "Key Vault secret name for the LiteLLM virtual key the Entra BFF forwards to the gateway. Leave empty to use gateway_key_versionless_id."
default = ""
}