diff --git a/.github/workflows/ai-workspace-helm-release.yml b/.github/workflows/ai-workspace-helm-release.yml new file mode 100644 index 0000000000..ec0a83707e --- /dev/null +++ b/.github/workflows/ai-workspace-helm-release.yml @@ -0,0 +1,154 @@ +name: Release AI Workspace Umbrella Helm Chart + +# Publishes the AI Workspace product umbrella (Platform API + AI Workspace UI) to +# GHCR as an OCI artifact. Its component dependencies reference the OCI registry +# and are pinned in Chart.yaml, so the component charts must be published FIRST. +# This workflow logs in, pulls the components (helm dependency update), then +# packages + pushes — otherwise identical to the component release workflows. + +on: + workflow_dispatch: + inputs: + version: + description: 'Chart version (e.g., 0.1.0)' + required: true + type: string + appVersion: + description: 'App version (e.g., 1.0.0-beta)' + required: true + type: string + +env: + CHART_NAME: ai-workspace + CHART_PATH: kubernetes/helm/ai-workspace-helm-chart + REGISTRY: ghcr.io + REGISTRY_USERNAME: api-platform-bot + ORGANIZATION: wso2 + REGISTRY_PATH: api-platform/helm-charts + CHART_VERSION: ${{ inputs.version }} + APP_VERSION: ${{ inputs.appVersion }} + +jobs: + release-helm-chart: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate input formats + run: | + if ! echo "${CHART_VERSION}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$'; then + echo "::error::Invalid version '${CHART_VERSION}'. Expected semver (e.g. 0.1.0 or 0.1.0-rc.1)." + exit 1 + fi + if ! echo "${APP_VERSION}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$'; then + echo "::error::Invalid appVersion '${APP_VERSION}'. Expected semver (e.g. 1.0.0-beta)." + exit 1 + fi + + - name: Check tag does not already exist + run: | + TAG="helm-${CHART_NAME}-${CHART_VERSION}" + if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then + echo "::error::Tag '$TAG' already exists. Bump the version or delete the tag first." + exit 1 + fi + echo "Tag '$TAG' is free. Proceeding." + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: 'latest' + + - name: Update Chart version + working-directory: ${{ env.CHART_PATH }} + run: | + sed -i "s/^version:.*/version: ${CHART_VERSION}/" Chart.yaml + sed -i "s/^appVersion:.*/appVersion: \"${APP_VERSION}\"/" Chart.yaml + echo "Updated Chart.yaml:" + cat Chart.yaml + + - name: Login to GitHub Container Registry + run: | + echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login ${{ env.REGISTRY }} --username ${{ env.REGISTRY_USERNAME }} --password-stdin + + - name: Resolve dependencies + working-directory: ${{ env.CHART_PATH }} + # Pulls the component charts pinned in Chart.yaml from GHCR — they must + # already be published at those versions (release the components first). + run: | + helm dependency update . + ls -la charts/*.tgz + + - name: Package Helm chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm package . + echo "Packaged chart:" + ls -la *.tgz + + - name: Push Helm chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}" + + - name: Commit and push version changes + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add ${{ env.CHART_PATH }}/Chart.yaml + if ! git diff --cached --quiet; then + git commit -m "chore: bump helm-${CHART_NAME} chart version to ${CHART_VERSION}" + git push + else + echo "No changes to commit" + fi + + - name: Create and push git tag + run: | + TAG="helm-${CHART_NAME}-${CHART_VERSION}" + git tag "$TAG" + git push origin "refs/tags/$TAG" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: helm-${{ env.CHART_NAME }}-${{ inputs.version }} + name: AI Workspace Helm Chart ${{ inputs.version }} + body: | + ## AI Workspace Helm Chart Release ${{ inputs.version }} + + **App Version:** `${{ inputs.appVersion }}` + + AI Workspace product package — shared Platform API + the AI Workspace UI. + + ### Installation + ```bash + helm install ${{ env.CHART_NAME }} oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}/${{ env.CHART_NAME }} --version ${{ inputs.version }} + ``` + + ### OCI Registry + `oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}/${{ env.CHART_NAME }}:${{ inputs.version }}` + draft: true + prerelease: ${{ contains(inputs.version, '-') }} + token: ${{ secrets.API_PLATFORM_BOT_TOKEN }} + + + - name: Chart Summary + run: | + echo "### AI Workspace Helm Chart Released :rocket:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Chart Name:** \`${CHART_NAME}\`" >> $GITHUB_STEP_SUMMARY + echo "**Chart Version:** \`${CHART_VERSION}\`" >> $GITHUB_STEP_SUMMARY + echo "**App Version:** \`${APP_VERSION}\`" >> $GITHUB_STEP_SUMMARY + echo "**Git Tag:** \`helm-${CHART_NAME}-${CHART_VERSION}\`" >> $GITHUB_STEP_SUMMARY + echo "**Registry:** \`oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### Install Command" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "helm install ${CHART_NAME} oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME} --version ${CHART_VERSION}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/developer-portal-helm-release.yml b/.github/workflows/developer-portal-helm-release.yml new file mode 100644 index 0000000000..034ba49d37 --- /dev/null +++ b/.github/workflows/developer-portal-helm-release.yml @@ -0,0 +1,154 @@ +name: Release Developer Portal Umbrella Helm Chart + +# Publishes the Developer Portal product umbrella (Platform API + Developer Portal +# UI) to GHCR as an OCI artifact. Its component dependencies reference the OCI +# registry and are pinned in Chart.yaml, so the component charts must be published +# FIRST. This workflow logs in, pulls the components (helm dependency update), +# then packages + pushes — otherwise identical to the component release workflows. + +on: + workflow_dispatch: + inputs: + version: + description: 'Chart version (e.g., 0.1.0)' + required: true + type: string + appVersion: + description: 'App version (e.g., 1.0.0-beta)' + required: true + type: string + +env: + CHART_NAME: developer-portal + CHART_PATH: kubernetes/helm/developer-portal-helm-chart + REGISTRY: ghcr.io + REGISTRY_USERNAME: api-platform-bot + ORGANIZATION: wso2 + REGISTRY_PATH: api-platform/helm-charts + CHART_VERSION: ${{ inputs.version }} + APP_VERSION: ${{ inputs.appVersion }} + +jobs: + release-helm-chart: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate input formats + run: | + if ! echo "${CHART_VERSION}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$'; then + echo "::error::Invalid version '${CHART_VERSION}'. Expected semver (e.g. 0.1.0 or 0.1.0-rc.1)." + exit 1 + fi + if ! echo "${APP_VERSION}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$'; then + echo "::error::Invalid appVersion '${APP_VERSION}'. Expected semver (e.g. 1.0.0-beta)." + exit 1 + fi + + - name: Check tag does not already exist + run: | + TAG="helm-${CHART_NAME}-${CHART_VERSION}" + if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then + echo "::error::Tag '$TAG' already exists. Bump the version or delete the tag first." + exit 1 + fi + echo "Tag '$TAG' is free. Proceeding." + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: 'latest' + + - name: Update Chart version + working-directory: ${{ env.CHART_PATH }} + run: | + sed -i "s/^version:.*/version: ${CHART_VERSION}/" Chart.yaml + sed -i "s/^appVersion:.*/appVersion: \"${APP_VERSION}\"/" Chart.yaml + echo "Updated Chart.yaml:" + cat Chart.yaml + + - name: Login to GitHub Container Registry + run: | + echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login ${{ env.REGISTRY }} --username ${{ env.REGISTRY_USERNAME }} --password-stdin + + - name: Resolve dependencies + working-directory: ${{ env.CHART_PATH }} + # Pulls the component charts pinned in Chart.yaml from GHCR — they must + # already be published at those versions (release the components first). + run: | + helm dependency update . + ls -la charts/*.tgz + + - name: Package Helm chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm package . + echo "Packaged chart:" + ls -la *.tgz + + - name: Push Helm chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}" + + - name: Commit and push version changes + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add ${{ env.CHART_PATH }}/Chart.yaml + if ! git diff --cached --quiet; then + git commit -m "chore: bump helm-${CHART_NAME} chart version to ${CHART_VERSION}" + git push + else + echo "No changes to commit" + fi + + - name: Create and push git tag + run: | + TAG="helm-${CHART_NAME}-${CHART_VERSION}" + git tag "$TAG" + git push origin "refs/tags/$TAG" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: helm-${{ env.CHART_NAME }}-${{ inputs.version }} + name: Developer Portal Helm Chart ${{ inputs.version }} + body: | + ## Developer Portal Helm Chart Release ${{ inputs.version }} + + **App Version:** `${{ inputs.appVersion }}` + + Developer Portal product package — shared Platform API + the Developer Portal UI. + + ### Installation + ```bash + helm install ${{ env.CHART_NAME }} oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}/${{ env.CHART_NAME }} --version ${{ inputs.version }} + ``` + + ### OCI Registry + `oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}/${{ env.CHART_NAME }}:${{ inputs.version }}` + draft: true + prerelease: ${{ contains(inputs.version, '-') }} + token: ${{ secrets.API_PLATFORM_BOT_TOKEN }} + + + - name: Chart Summary + run: | + echo "### Developer Portal Helm Chart Released :rocket:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Chart Name:** \`${CHART_NAME}\`" >> $GITHUB_STEP_SUMMARY + echo "**Chart Version:** \`${CHART_VERSION}\`" >> $GITHUB_STEP_SUMMARY + echo "**App Version:** \`${APP_VERSION}\`" >> $GITHUB_STEP_SUMMARY + echo "**Git Tag:** \`helm-${CHART_NAME}-${CHART_VERSION}\`" >> $GITHUB_STEP_SUMMARY + echo "**Registry:** \`oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### Install Command" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "helm install ${CHART_NAME} oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME} --version ${CHART_VERSION}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/kubernetes/helm/ai-workspace-helm-chart/.helmignore b/kubernetes/helm/ai-workspace-helm-chart/.helmignore new file mode 100644 index 0000000000..74200fc5ee --- /dev/null +++ b/kubernetes/helm/ai-workspace-helm-chart/.helmignore @@ -0,0 +1,19 @@ +# Common Helm ignores +.DS_Store +.git/ +.gitignore +.idea/ +.vscode/ +Thumbs.db +*.swp +*.tmp +*.bak + +# Examples and documentation +examples/ + +# Local development overrides (not shipped) +values-local.yaml + +# Generated / local — never package +values-secrets.yaml diff --git a/kubernetes/helm/ai-workspace-helm-chart/Chart.yaml b/kubernetes/helm/ai-workspace-helm-chart/Chart.yaml new file mode 100644 index 0000000000..f513bd6f57 --- /dev/null +++ b/kubernetes/helm/ai-workspace-helm-chart/Chart.yaml @@ -0,0 +1,39 @@ +apiVersion: v2 +name: ai-workspace +kubeVersion: ">=1.24.0-0" +description: >- + WSO2 API Platform — AI Workspace product package. Chart whose default + profile is the shared Platform API control plane plus the AI Workspace UI. Each + component is a separately-released chart pulled as an OCI dependency; add other + components (e.g. the Developer Portal) later by appending a dependency. +version: 1.0.0-alpha2 +appVersion: "1.0.0-alpha2" +type: application +home: https://github.com/wso2/api-platform +sources: + - https://github.com/wso2/api-platform +keywords: + - api-platform + - ai-workspace + - platform-api + - wso2 +maintainers: + - name: ap-portals-maintainers + email: contact@wso2.com +dependencies: + # NOTE: repositories below reference the OCI registry (the release default), so + # `helm dependency update` pulls the components from ghcr (requires them to be + # published + `helm registry login ghcr.io`). For local development WITHOUT + # publishing, temporarily switch each to repository: "file://../-helm-chart" + # and re-run `helm dependency update`. + # + # Shared control plane. On by default; the UI reaches it in-cluster. + - name: platform-api + version: "0.13.0" + repository: "oci://ghcr.io/wso2/api-platform/helm-charts" + condition: platform-api.enabled + # AI Workspace UI — this package's headline component. + - name: ai-workspace-ui + version: "1.0.0-alpha" + repository: "oci://ghcr.io/wso2/api-platform/helm-charts" + condition: ai-workspace-ui.enabled diff --git a/kubernetes/helm/ai-workspace-helm-chart/generate-secrets.sh b/kubernetes/helm/ai-workspace-helm-chart/generate-secrets.sh new file mode 100755 index 0000000000..bf0a7682e4 --- /dev/null +++ b/kubernetes/helm/ai-workspace-helm-chart/generate-secrets.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# -------------------------------------------------------------------- +# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). +# +# WSO2 LLC. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# -------------------------------------------------------------------- +# +# generate-secrets.sh — secret provisioning for the ai-workspace umbrella. +# +# Model: "setup generates, startup only checks." Creates the Kubernetes Secrets +# the subcharts reference and writes values-secrets.yaml with the secret +# REFERENCES only (never secret values). Component enablement is controlled +# separately by your -f my_values.yaml — this script never toggles components. +# +# CUMULATIVE + IDEMPOTENT: a re-run never rotates or drops anything. Existing +# Secrets are left untouched (the RS256 public key is re-read from the existing +# Platform API Secret so the portal keeps verifying its tokens), and +# values-secrets.yaml is rebuilt to reference every component Secret that exists. +# +# The Platform API signs its RS256 JWTs with a private key; the Developer Portal +# verifies them with the matching PUBLIC key — the same keypair, no shared HMAC +# secret. The file-mode admin credential is GENERATED here (random password, +# bcrypt hash stored in the Secret) — there is no admin/admin default. +# +# Usage: +# [flags] [inputs] ./generate-secrets.sh [release-name] +# Namespace to create the Secrets in (required). +# [release-name] Helm release name; names the Secrets (default: ai-workspace). +# +# Component flags: +# DEVELOPER_PORTAL=true Also provision the Developer Portal Secret. +# (The shared Platform API Secret is always ensured. The AI Workspace UI Secret +# is provisioned only when AIW_OIDC_CLIENT_SECRET is set — basic mode needs none.) +# +# Optional inputs (only wired if set): +# ADMIN_USERNAME File-mode admin username (default: admin). +# DATABASE_PASSWORD Postgres password for the Platform API. +# WEBHOOK_SECRET HMAC secret for the Platform API webhook receiver. +# AIW_OIDC_CLIENT_SECRET AI Workspace BFF OIDC client secret (OIDC mode). +# DP_DATABASE_PASSWORD Postgres password for the Developer Portal. +# DP_OIDC_CLIENT_SECRET Developer Portal OIDC client secret. +# DP_SERVICE_API_KEY Developer Portal service API key value. +# -------------------------------------------------------------------- +set -euo pipefail + +NAMESPACE="${1:-}" +RELEASE="${2:-ai-workspace}" +DEVELOPER_PORTAL="${DEVELOPER_PORTAL:-false}" +ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" + +if [[ -z "$NAMESPACE" ]]; then + echo "Usage: [DEVELOPER_PORTAL=true] $0 [release-name]" >&2 + exit 1 +fi +command -v kubectl >/dev/null 2>&1 || { echo "error: kubectl not found in PATH" >&2; exit 1; } +command -v openssl >/dev/null 2>&1 || { echo "error: openssl not found in PATH" >&2; exit 1; } + +PA_SECRET="${RELEASE}-platform-api-secrets" +UI_SECRET="${RELEASE}-ai-workspace-ui-secrets" +DP_SECRET="${RELEASE}-developer-portal-ui-secrets" +OUT_FILE="values-secrets.yaml" + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT + +kubectl get namespace "$NAMESPACE" >/dev/null 2>&1 || { + echo "==> Creating namespace $NAMESPACE"; kubectl create namespace "$NAMESPACE"; +} + +gen_key() { openssl rand -hex 32; } +secret_exists() { kubectl -n "$NAMESPACE" get secret "$1" >/dev/null 2>&1; } +# NB: a missing map key renders as the literal "" under go-template, +# which a bare `grep -q .` would wrongly treat as present — filter it out. +secret_has_key(){ local v; v="$(kubectl -n "$NAMESPACE" get secret "$1" -o "go-template={{ index .data \"$2\" }}" 2>/dev/null)"; [[ -n "$v" && "$v" != "" ]]; } +# bcrypt via htpasswd, else the httpd docker image (matches the distribution setup.sh). +bcrypt_hash() { + local pw="$1" + if command -v htpasswd >/dev/null 2>&1; then + printf '%s' "$pw" | htpasswd -niB -C 10 "" | cut -d: -f2 | tr -d '\r\n' + elif command -v docker >/dev/null 2>&1; then + printf '%s' "$pw" | docker run --rm -i httpd:2.4-alpine htpasswd -niB -C 10 "" | cut -d: -f2 | tr -d '\r\n' + else + echo "error: need htpasswd (apache2-utils/httpd-tools) or docker to bcrypt-hash the admin password" >&2 + exit 1 + fi +} + +# --- Platform API (shared): encryption key, RS256 keypair, generated admin creds --- +if secret_exists "$PA_SECRET"; then + echo "==> $PA_SECRET already exists — leaving it untouched" + kubectl -n "$NAMESPACE" get secret "$PA_SECRET" -o "go-template={{ index .data \"jwt_public.pem\" }}" | base64 --decode > "$TMP/jwt_public.pem" +else + echo "==> Creating $PA_SECRET" + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$TMP/jwt_private.pem" 2>/dev/null + openssl rsa -in "$TMP/jwt_private.pem" -pubout -out "$TMP/jwt_public.pem" 2>/dev/null + ADMIN_PASSWORD="$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | cut -c1-20)" + ADMIN_PASSWORD_HASH="$(bcrypt_hash "$ADMIN_PASSWORD")" + args=( + --from-literal=ENCRYPTION_KEY="$(gen_key)" + --from-literal=ADMIN_USERNAME="$ADMIN_USERNAME" + --from-literal=ADMIN_PASSWORD_HASH="$ADMIN_PASSWORD_HASH" + --from-file=jwt_public.pem="$TMP/jwt_public.pem" + --from-file=jwt_private.pem="$TMP/jwt_private.pem" + ) + [[ -n "${DATABASE_PASSWORD:-}" ]] && args+=( --from-literal=DATABASE_PASSWORD="$DATABASE_PASSWORD" ) + [[ -n "${WEBHOOK_SECRET:-}" ]] && args+=( --from-literal=WEBHOOK_SECRET="$WEBHOOK_SECRET" ) + kubectl -n "$NAMESPACE" create secret generic "$PA_SECRET" "${args[@]}" + echo " Generated file-mode admin credential (shown once — store it now):" + echo " username: ${ADMIN_USERNAME}" + echo " password: ${ADMIN_PASSWORD}" +fi + +# --- AI Workspace UI: OIDC client secret (OIDC mode only) --- +if [[ -n "${AIW_OIDC_CLIENT_SECRET:-}" ]]; then + if secret_exists "$UI_SECRET"; then + echo "==> $UI_SECRET already exists — leaving it untouched" + else + echo "==> Creating $UI_SECRET" + kubectl -n "$NAMESPACE" create secret generic "$UI_SECRET" \ + --from-literal=OIDC_CLIENT_SECRET="$AIW_OIDC_CLIENT_SECRET" + fi +else + echo "==> AIW_OIDC_CLIENT_SECRET not set — skipping UI Secret (only needed for OIDC mode)" +fi + +# --- Developer Portal: encryption key + session secret + PA public key (+optional) --- +if [[ "$DEVELOPER_PORTAL" == "true" ]]; then + if secret_exists "$DP_SECRET"; then + echo "==> $DP_SECRET already exists — leaving it untouched" + else + echo "==> Creating $DP_SECRET" + args=( + --from-literal=ENCRYPTION_KEY="$(gen_key)" + --from-literal=SESSION_SECRET="$(gen_key)" + --from-file=jwt_public.pem="$TMP/jwt_public.pem" + ) + [[ -n "${DP_OIDC_CLIENT_SECRET:-}" ]] && args+=( --from-literal=IDP_CLIENT_SECRET="$DP_OIDC_CLIENT_SECRET" ) + [[ -n "${DP_SERVICE_API_KEY:-}" ]] && args+=( --from-literal=SERVICE_API_KEY_VALUE="$DP_SERVICE_API_KEY" ) + [[ -n "${DP_DATABASE_PASSWORD:-}" ]] && args+=( --from-literal=DATABASE_PASSWORD="$DP_DATABASE_PASSWORD" ) + kubectl -n "$NAMESPACE" create secret generic "$DP_SECRET" "${args[@]}" + fi +fi + +# --- Write values-secrets.yaml CUMULATIVELY: reference every Secret that exists now --- +echo "==> Writing $OUT_FILE (references for all provisioned components)" +{ + echo "# Generated by generate-secrets.sh — secret REFERENCES only (no secret values)." + echo "# Cumulative: re-runs keep every previously-provisioned reference. Safe to commit." + echo "# Install/upgrade with:" + echo "# helm upgrade --install $RELEASE ./ai-workspace-helm-chart -n $NAMESPACE \\" + echo "# -f $OUT_FILE -f my_values.yaml" + echo "platform-api:" + echo " secrets:" + echo " existingSecret: $PA_SECRET" + if secret_exists "$UI_SECRET"; then + echo "ai-workspace-ui:" + echo " secrets:" + echo " existingSecret: $UI_SECRET" + fi + if secret_exists "$DP_SECRET"; then + echo "developer-portal-ui:" + echo " secrets:" + echo " existingSecret: $DP_SECRET" + echo " hasPublicKey: true" + secret_has_key "$DP_SECRET" IDP_CLIENT_SECRET && echo " hasIdpClientSecret: true" + secret_has_key "$DP_SECRET" SERVICE_API_KEY_VALUE && echo " hasServiceApiKeyValue: true" + fi +} > "$OUT_FILE" + +echo +echo "Done. Next:" +echo " helm dependency update ./ai-workspace-helm-chart # first time / after editing deps" +echo " helm upgrade --install $RELEASE ./ai-workspace-helm-chart -n $NAMESPACE \\" +echo " -f $OUT_FILE -f my_values.yaml" diff --git a/kubernetes/helm/ai-workspace-helm-chart/templates/NOTES.txt b/kubernetes/helm/ai-workspace-helm-chart/templates/NOTES.txt new file mode 100644 index 0000000000..0ef2c38248 --- /dev/null +++ b/kubernetes/helm/ai-workspace-helm-chart/templates/NOTES.txt @@ -0,0 +1,85 @@ +{{- $pa := index .Values "platform-api" -}} +{{- $aiw := index .Values "ai-workspace-ui" -}} +{{- $dp := index .Values "developer-portal-ui" -}} +{{- $dpScheme := ternary "http" "https" (eq (dig "tls" "certificateProvider" "cert-manager" (default (dict) $dp)) "none") -}} +Thank you for installing {{ .Chart.Name }} ({{ .Chart.Version }})! + +Your release is named {{ .Release.Name }}. + +Enabled components: +{{- if $pa.enabled }} + • Platform API (shared control plane) +{{- end }} +{{- if $aiw.enabled }} + • AI Workspace UI +{{- end }} +{{- if $dp.enabled }} + • Developer Portal +{{- end }} + +To learn more about the release, try: + + $ helm status {{ .Release.Name }} --namespace {{ .Release.Namespace }} + $ helm get all {{ .Release.Name }} --namespace {{ .Release.Namespace }} + +{{- if $pa.enabled }} + +Platform API: + - Service: {{ include "apip.platformApi.fullname" . }} (port {{ dig "service" "port" 9243 $pa }}) + - Database: {{ dig "config" "database" "driver" "sqlite3" $pa }} + + $ kubectl port-forward svc/{{ include "apip.platformApi.fullname" . }} {{ dig "service" "port" 9243 $pa }}:{{ dig "service" "port" 9243 $pa }} -n {{ .Release.Namespace }} + Then: {{ ternary "https" "http" (dig "platformApi" "tlsEnabled" true .Values.global) }}://localhost:{{ dig "service" "port" 9243 $pa }}/health +{{- end }} + +{{- if $aiw.enabled }} + +AI Workspace UI: + - Service: {{ include "apip.aiWorkspace.fullname" . }} (port {{ dig "service" "port" 9643 $aiw }}, type {{ dig "service" "type" "LoadBalancer" $aiw }}) + - Auth mode: {{ dig "config" "auth" "mode" "basic" $aiw }} + - Platform API URL: {{ default (include "apip.platformApi.internalURL" .) (dig "config" "controlPlane" "url" "" $aiw) }} + +To access it: + {{- if eq (dig "service" "type" "LoadBalancer" $aiw) "NodePort" }} + $ export NODE_PORT=$(kubectl get svc {{ include "apip.aiWorkspace.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}') + $ export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}') + $ echo https://$NODE_IP:$NODE_PORT + {{- else if eq (dig "service" "type" "LoadBalancer" $aiw) "LoadBalancer" }} + $ kubectl get svc {{ include "apip.aiWorkspace.fullname" . }} -n {{ .Release.Namespace }} -w # wait for EXTERNAL-IP + $ echo https://:{{ dig "service" "port" 9643 $aiw }} + {{- else }} + $ kubectl port-forward svc/{{ include "apip.aiWorkspace.fullname" . }} {{ dig "service" "port" 9643 $aiw }}:{{ dig "service" "port" 9643 $aiw }} -n {{ .Release.Namespace }} + Then visit: https://localhost:{{ dig "service" "port" 9643 $aiw }} + {{- end }} +{{- if eq (dig "config" "auth" "mode" "basic" $aiw) "basic" }} + + Log in with the file-mode admin credential generated by ./generate-secrets.sh + (username + password shown once at creation; the bcrypt hash is stored in the + Platform API Secret — there is no default admin/admin). +{{- end }} +{{- end }} + +{{- if $dp.enabled }} + +Developer Portal: + - Service: {{ include "apip.developerPortal.fullname" . }} (port {{ dig "service" "port" 3000 $dp }}, type {{ dig "service" "type" "LoadBalancer" $dp }}) + - Platform API URL: {{ default (include "apip.platformApi.internalURL" .) (dig "config" "platformApi" "baseUrl" "" $dp) }} + +To access it: + {{- if eq (dig "service" "type" "LoadBalancer" $dp) "NodePort" }} + $ export NODE_PORT=$(kubectl get svc {{ include "apip.developerPortal.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}') + $ export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}') + $ echo {{ $dpScheme }}://$NODE_IP:$NODE_PORT/default/views/default + {{- else if eq (dig "service" "type" "LoadBalancer" $dp) "LoadBalancer" }} + $ kubectl get svc {{ include "apip.developerPortal.fullname" . }} -n {{ .Release.Namespace }} -w # wait for EXTERNAL-IP + $ echo {{ $dpScheme }}://:{{ dig "service" "port" 3000 $dp }}/default/views/default + {{- else }} + $ kubectl port-forward svc/{{ include "apip.developerPortal.fullname" . }} {{ dig "service" "port" 3000 $dp }}:{{ dig "service" "port" 3000 $dp }} -n {{ .Release.Namespace }} + Then visit: {{ $dpScheme }}://localhost:{{ dig "service" "port" 3000 $dp }}/default/views/default + {{- end }} + + Log in with the file-mode admin credential generated by ./generate-secrets.sh + (shown once at creation; no default admin/admin), unless you configured OIDC. +{{- end }} + +Documentation: {{ .Chart.Home }} diff --git a/kubernetes/helm/ai-workspace-helm-chart/templates/serviceaccount.yaml b/kubernetes/helm/ai-workspace-helm-chart/templates/serviceaccount.yaml new file mode 100644 index 0000000000..bb003ec3b6 --- /dev/null +++ b/kubernetes/helm/ai-workspace-helm-chart/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- $g := default (dict) .Values.global -}} +{{- $sa := default (dict) $g.serviceAccount -}} +{{- if $sa.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "apip.serviceAccountName" . }} + labels: + {{- include "apip.labels" . | nindent 4 }} + {{- with (include "apip.annotations" (list . $sa.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ $sa.automountServiceAccountToken }} +{{- end }} diff --git a/kubernetes/helm/ai-workspace-helm-chart/templates/validation.yaml b/kubernetes/helm/ai-workspace-helm-chart/templates/validation.yaml new file mode 100644 index 0000000000..f1e432d898 --- /dev/null +++ b/kubernetes/helm/ai-workspace-helm-chart/templates/validation.yaml @@ -0,0 +1,18 @@ +{{/* +Render-time guardrails for the umbrella. Renders no Kubernetes objects; only +fails the install early with an actionable message on misconfiguration. +*/}} +{{- $pa := index .Values "platform-api" -}} +{{- $aiw := index .Values "ai-workspace-ui" -}} +{{- $dp := index .Values "developer-portal-ui" -}} +{{- if not (or $aiw.enabled $dp.enabled) }} +{{- fail "No portal component is enabled. Set ai-workspace-ui.enabled=true and/or developer-portal-ui.enabled=true (the shared platform-api alone serves no UI)." }} +{{- end }} +{{- if not $pa.enabled }} +{{- if and $aiw.enabled (not (dig "config" "controlPlane" "url" "" (default (dict) $aiw))) }} +{{- fail "ai-workspace-ui.enabled=true but platform-api.enabled=false and no external Platform API URL is set. Set platform-api.enabled=true, or set ai-workspace-ui.config.controlPlane.url to an external control plane." }} +{{- end }} +{{- if and $dp.enabled (not (dig "config" "platformApi" "baseUrl" "" (default (dict) $dp))) }} +{{- fail "developer-portal-ui.enabled=true but platform-api.enabled=false and no external Platform API URL is set. Set platform-api.enabled=true, or set developer-portal-ui.config.platformApi.baseUrl to an external control plane." }} +{{- end }} +{{- end }} diff --git a/kubernetes/helm/ai-workspace-helm-chart/values-local.yaml b/kubernetes/helm/ai-workspace-helm-chart/values-local.yaml new file mode 100644 index 0000000000..e25023e93b --- /dev/null +++ b/kubernetes/helm/ai-workspace-helm-chart/values-local.yaml @@ -0,0 +1,28 @@ +# Local development values for the ai-workspace umbrella chart. +# Usage: +# helm install ai-workspace ./ai-workspace-helm-chart -f values-secrets.yaml -f values-local.yaml +# +# Overrides the defaults for local development, per component: +# - locally-built images (latest tag, IfNotPresent pull policy) +# - debug logging +# - the UI skips TLS verification to the Platform API's self-signed cert +# +# NOTE: the components have no self-signed TLS fallback. For a local install, +# provide a TLS Secret (tls.certificateProvider=secret) or install cert-manager. +platform-api: + image: + tag: "latest" + pullPolicy: IfNotPresent + config: + logging: + level: debug + +ai-workspace-ui: + image: + tag: "latest" + pullPolicy: IfNotPresent + config: + logging: + level: debug + controlPlane: + tlsSkipVerify: true diff --git a/kubernetes/helm/ai-workspace-helm-chart/values.yaml b/kubernetes/helm/ai-workspace-helm-chart/values.yaml new file mode 100644 index 0000000000..09eb566115 --- /dev/null +++ b/kubernetes/helm/ai-workspace-helm-chart/values.yaml @@ -0,0 +1,79 @@ +# ============================================================================ +# ai-workspace — product values +# ============================================================================ +# This is the AI Workspace product package: the shared Platform API plus the +# AI Workspace UI. You normally keep this file as-is and put your changes in a +# separate `-f my_values.yaml`; both are layered at install time (later wins). +# +# How settings are organised: +# global.* Settings shared by EVERY component (image pull secrets, +# service account, the Platform API address, …). +# .* Settings for one component, keyed by its chart name in +# kebab-case: `platform-api`, `ai-workspace-ui`. Each component +# documents its full option list in charts//values.yaml — +# override any of those keys under the same top-level key here. +# +# Secrets are never written in this file. Run ./generate-secrets.sh first: it +# creates the Kubernetes Secrets and writes values-secrets.yaml holding only the +# Secret *names*. Install with `-f values-secrets.yaml`. See README.md. + +# ---------------------------------------------------------------------------- +# global — merged into every component as `.Values.global` +# ---------------------------------------------------------------------------- +global: + # Resource naming. By default every resource is prefixed with the Helm release + # name (e.g. `myrelease-platform-api`). Set fullnameOverride to force a fixed + # prefix regardless of release name; nameOverride tweaks the chart-name part. + nameOverride: "" + fullnameOverride: "" + + # Extra labels / annotations stamped onto every rendered Kubernetes object. + commonLabels: {} + commonAnnotations: {} + + # Names of image-pull Secrets to attach to every pod (for private registries). + # Merged with any per-component imagePullSecrets. + imagePullSecrets: [] + + # A single ServiceAccount is created once per release and shared by all pods. + serviceAccount: + create: true # false → use an existing SA (set name) + name: "" # "" → defaults to the release fullname + annotations: {} # e.g. an IRSA / workload-identity role ARN + automountServiceAccountToken: true + + # WSO2 Subscription (https://wso2.com/subscription/). Set imagePullSecret to the + # name of a docker-registry Secret and two things happen automatically: it's + # added to every pod, and the default ghcr.io/wso2/api-platform/* image repos are + # rewritten to registry.wso2.com/wso2-api-platform/*. Leave "" for public images. + wso2: + subscription: + imagePullSecret: "" + + # How portals reach the shared Platform API in-cluster. These build the URL the + # UI calls, so they MUST match the Platform API's actual service port and TLS + # setting below (platform-api.service.port / platform-api.config.tls.enabled). + platformApi: + port: 9243 + tlsEnabled: true # true → https://, false → http:// + +# ---------------------------------------------------------------------------- +# Components — turn each on/off with .enabled +# ---------------------------------------------------------------------------- +# `enabled` maps to the dependency condition in Chart.yaml, so a disabled +# component is not installed at all. For anything deeper (image tag, database, +# auth, resources, …) add keys under the same top-level key — see each +# component's own values.yaml, e.g.: +# +# ai-workspace-ui: +# config: +# authMode: oidc # switch the UI from file-based login to OIDC + +# Shared control plane. Required by the UI; leave enabled unless you point the UI +# at an external Platform API. +platform-api: + enabled: true + +# The AI Workspace UI — this package's headline component. +ai-workspace-ui: + enabled: true diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml index 5a30a88932..da77646ccf 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml @@ -6,6 +6,9 @@ {{- $selfSigned := eq $tls.certificateProvider "selfSigned" -}} {{- $tlsMounted := or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret") -}} {{- $imageTag := default .Chart.AppVersion $ui.image.tag -}} +{{- if and $deployment.enabled (not $tlsMounted) }} +{{- fail "ai-workspace-ui: tls.certificateProvider must be \"cert-manager\" or \"secret\" — the AI Workspace UI has no self-signed fallback (the BFF does not generate a certificate at startup, it only reads an existing cert_file/key_file). Provide a TLS Secret (certificateProvider=secret) or use cert-manager." }} +{{- end }} {{- if $deployment.enabled }} apiVersion: apps/v1 kind: Deployment diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml index 403007eb9a..0e692c4c84 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml @@ -164,10 +164,13 @@ containerPort: 9643 # tls — certificate for the BFF's HTTPS listener # ---------------------------------------------------------------------------- tls: - # selfSigned → BFF generates a self-signed pair at startup (demo). - # secret → mount an existing Secret's tls.crt / tls.key. - # cert-manager → provision a Certificate via cert-manager and mount it. - certificateProvider: selfSigned + # How the BFF's HTTPS listener gets its certificate. The BFF does NOT + # self-generate one, so a working certificate must be supplied: + # secret → mount an existing Secret's tls.crt / tls.key. + # cert-manager → provision a Certificate via cert-manager and mount it. + # NOTE: "selfSigned" is NOT supported — the image has no in-container cert + # generation, so the chart rejects it at render time (fail-fast, similar to platform-api). + certificateProvider: cert-manager certManager: create: true createIssuer: true diff --git a/kubernetes/helm/developer-portal-helm-chart/.helmignore b/kubernetes/helm/developer-portal-helm-chart/.helmignore new file mode 100644 index 0000000000..74200fc5ee --- /dev/null +++ b/kubernetes/helm/developer-portal-helm-chart/.helmignore @@ -0,0 +1,19 @@ +# Common Helm ignores +.DS_Store +.git/ +.gitignore +.idea/ +.vscode/ +Thumbs.db +*.swp +*.tmp +*.bak + +# Examples and documentation +examples/ + +# Local development overrides (not shipped) +values-local.yaml + +# Generated / local — never package +values-secrets.yaml diff --git a/kubernetes/helm/developer-portal-helm-chart/Chart.yaml b/kubernetes/helm/developer-portal-helm-chart/Chart.yaml new file mode 100644 index 0000000000..89b2ded7ec --- /dev/null +++ b/kubernetes/helm/developer-portal-helm-chart/Chart.yaml @@ -0,0 +1,39 @@ +apiVersion: v2 +name: developer-portal +kubeVersion: ">=1.24.0-0" +description: >- + WSO2 API Platform — Developer Portal product package. Chart whose + default profile is the shared Platform API control plane plus the Developer + Portal. Each component is a separately-released chart pulled as an OCI + dependency; add other components (e.g. AI Workspace) later by appending a dependency. +version: 1.0.0-alpha2 +appVersion: "1.0.0-alpha2" +type: application +home: https://github.com/wso2/api-platform +sources: + - https://github.com/wso2/api-platform +keywords: + - api-platform + - developer-portal + - platform-api + - wso2 +maintainers: + - name: ap-portals-maintainers + email: contact@wso2.com +dependencies: + # NOTE: repositories below reference the OCI registry (the release default), so + # `helm dependency update` pulls the components from ghcr (requires them to be + # published + `helm registry login ghcr.io`). For local development WITHOUT + # publishing, temporarily switch each to repository: "file://../-helm-chart" + # and re-run `helm dependency update`. + # + # Shared control plane. On by default; the portal reaches it in-cluster. + - name: platform-api + version: "0.13.0" + repository: "oci://ghcr.io/wso2/api-platform/helm-charts" + condition: platform-api.enabled + # Developer Portal — this package's headline component. + - name: developer-portal-ui + version: "1.0.0-alpha" + repository: "oci://ghcr.io/wso2/api-platform/helm-charts" + condition: developer-portal-ui.enabled diff --git a/kubernetes/helm/developer-portal-helm-chart/generate-secrets.sh b/kubernetes/helm/developer-portal-helm-chart/generate-secrets.sh new file mode 100755 index 0000000000..2bf659ff4c --- /dev/null +++ b/kubernetes/helm/developer-portal-helm-chart/generate-secrets.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# -------------------------------------------------------------------- +# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). +# +# WSO2 LLC. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# -------------------------------------------------------------------- +# +# generate-secrets.sh — secret provisioning for the developer-portal umbrella. +# +# Model: "setup generates, startup only checks." Creates the Kubernetes Secrets +# the subcharts reference and writes values-secrets.yaml with the secret +# REFERENCES only (never secret values). Component enablement is controlled +# separately by your -f my_values.yaml — this script never toggles components. +# +# CUMULATIVE + IDEMPOTENT: a re-run never rotates or drops anything. Existing +# Secrets are left untouched (the RS256 public key is re-read from the existing +# Platform API Secret so the portal keeps verifying its tokens), and +# values-secrets.yaml is rebuilt to reference every component Secret that exists. +# +# The Platform API signs its RS256 JWTs with a private key; the Developer Portal +# verifies them with the matching PUBLIC key — the same keypair, no shared HMAC +# secret. The file-mode admin credential is GENERATED here (random password, +# bcrypt hash stored in the Secret) — there is no admin/admin default. +# +# Usage: +# [flags] [inputs] ./generate-secrets.sh [release-name] +# Namespace to create the Secrets in (required). +# [release-name] Helm release name; names the Secrets (default: developer-portal). +# +# Component flags: +# DEVELOPER_PORTAL=false Skip the Developer Portal Secret (default: true). +# (The shared Platform API Secret is always ensured. An AI Workspace UI Secret +# is provisioned only when AIW_OIDC_CLIENT_SECRET is set — for the extensibility +# case where the AI Workspace UI is added to this umbrella.) +# +# Optional inputs (only wired if set): +# ADMIN_USERNAME File-mode admin username (default: admin). +# DATABASE_PASSWORD Postgres password for the Platform API. +# WEBHOOK_SECRET HMAC secret for the Platform API webhook receiver. +# DP_DATABASE_PASSWORD Postgres password for the Developer Portal. +# DP_OIDC_CLIENT_SECRET Developer Portal OIDC client secret. +# DP_SERVICE_API_KEY Developer Portal service API key value. +# AIW_OIDC_CLIENT_SECRET AI Workspace BFF OIDC client secret (extensibility). +# -------------------------------------------------------------------- +set -euo pipefail + +NAMESPACE="${1:-}" +RELEASE="${2:-developer-portal}" +DEVELOPER_PORTAL="${DEVELOPER_PORTAL:-true}" +ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" + +if [[ -z "$NAMESPACE" ]]; then + echo "Usage: [DEVELOPER_PORTAL=false] $0 [release-name]" >&2 + exit 1 +fi +command -v kubectl >/dev/null 2>&1 || { echo "error: kubectl not found in PATH" >&2; exit 1; } +command -v openssl >/dev/null 2>&1 || { echo "error: openssl not found in PATH" >&2; exit 1; } + +PA_SECRET="${RELEASE}-platform-api-secrets" +UI_SECRET="${RELEASE}-ai-workspace-ui-secrets" +DP_SECRET="${RELEASE}-developer-portal-ui-secrets" +OUT_FILE="values-secrets.yaml" + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT + +kubectl get namespace "$NAMESPACE" >/dev/null 2>&1 || { + echo "==> Creating namespace $NAMESPACE"; kubectl create namespace "$NAMESPACE"; +} + +gen_key() { openssl rand -hex 32; } +secret_exists() { kubectl -n "$NAMESPACE" get secret "$1" >/dev/null 2>&1; } +# NB: a missing map key renders as the literal "" under go-template, +# which a bare `grep -q .` would wrongly treat as present — filter it out. +secret_has_key(){ local v; v="$(kubectl -n "$NAMESPACE" get secret "$1" -o "go-template={{ index .data \"$2\" }}" 2>/dev/null)"; [[ -n "$v" && "$v" != "" ]]; } +# bcrypt via htpasswd, else the httpd docker image (matches the distribution setup.sh). +bcrypt_hash() { + local pw="$1" + if command -v htpasswd >/dev/null 2>&1; then + printf '%s' "$pw" | htpasswd -niB -C 10 "" | cut -d: -f2 | tr -d '\r\n' + elif command -v docker >/dev/null 2>&1; then + printf '%s' "$pw" | docker run --rm -i httpd:2.4-alpine htpasswd -niB -C 10 "" | cut -d: -f2 | tr -d '\r\n' + else + echo "error: need htpasswd (apache2-utils/httpd-tools) or docker to bcrypt-hash the admin password" >&2 + exit 1 + fi +} + +# --- Platform API (shared): encryption key, RS256 keypair, generated admin creds --- +if secret_exists "$PA_SECRET"; then + echo "==> $PA_SECRET already exists — leaving it untouched" + kubectl -n "$NAMESPACE" get secret "$PA_SECRET" -o "go-template={{ index .data \"jwt_public.pem\" }}" | base64 --decode > "$TMP/jwt_public.pem" +else + echo "==> Creating $PA_SECRET" + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$TMP/jwt_private.pem" 2>/dev/null + openssl rsa -in "$TMP/jwt_private.pem" -pubout -out "$TMP/jwt_public.pem" 2>/dev/null + ADMIN_PASSWORD="$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | cut -c1-20)" + ADMIN_PASSWORD_HASH="$(bcrypt_hash "$ADMIN_PASSWORD")" + args=( + --from-literal=ENCRYPTION_KEY="$(gen_key)" + --from-literal=ADMIN_USERNAME="$ADMIN_USERNAME" + --from-literal=ADMIN_PASSWORD_HASH="$ADMIN_PASSWORD_HASH" + --from-file=jwt_public.pem="$TMP/jwt_public.pem" + --from-file=jwt_private.pem="$TMP/jwt_private.pem" + ) + [[ -n "${DATABASE_PASSWORD:-}" ]] && args+=( --from-literal=DATABASE_PASSWORD="$DATABASE_PASSWORD" ) + [[ -n "${WEBHOOK_SECRET:-}" ]] && args+=( --from-literal=WEBHOOK_SECRET="$WEBHOOK_SECRET" ) + kubectl -n "$NAMESPACE" create secret generic "$PA_SECRET" "${args[@]}" + echo " Generated file-mode admin credential (shown once — store it now):" + echo " username: ${ADMIN_USERNAME}" + echo " password: ${ADMIN_PASSWORD}" +fi + +# --- Developer Portal: encryption key + session secret + PA public key (+optional) --- +if [[ "$DEVELOPER_PORTAL" == "true" ]]; then + if secret_exists "$DP_SECRET"; then + echo "==> $DP_SECRET already exists — leaving it untouched" + else + echo "==> Creating $DP_SECRET" + args=( + --from-literal=ENCRYPTION_KEY="$(gen_key)" + --from-literal=SESSION_SECRET="$(gen_key)" + --from-file=jwt_public.pem="$TMP/jwt_public.pem" + ) + [[ -n "${DP_OIDC_CLIENT_SECRET:-}" ]] && args+=( --from-literal=IDP_CLIENT_SECRET="$DP_OIDC_CLIENT_SECRET" ) + [[ -n "${DP_SERVICE_API_KEY:-}" ]] && args+=( --from-literal=SERVICE_API_KEY_VALUE="$DP_SERVICE_API_KEY" ) + [[ -n "${DP_DATABASE_PASSWORD:-}" ]] && args+=( --from-literal=DATABASE_PASSWORD="$DP_DATABASE_PASSWORD" ) + kubectl -n "$NAMESPACE" create secret generic "$DP_SECRET" "${args[@]}" + fi +fi + +# --- AI Workspace UI: OIDC client secret (extensibility — only when provided) --- +if [[ -n "${AIW_OIDC_CLIENT_SECRET:-}" ]]; then + if secret_exists "$UI_SECRET"; then + echo "==> $UI_SECRET already exists — leaving it untouched" + else + echo "==> Creating $UI_SECRET" + kubectl -n "$NAMESPACE" create secret generic "$UI_SECRET" \ + --from-literal=OIDC_CLIENT_SECRET="$AIW_OIDC_CLIENT_SECRET" + fi +fi + +# --- Write values-secrets.yaml CUMULATIVELY: reference every Secret that exists now --- +echo "==> Writing $OUT_FILE (references for all provisioned components)" +{ + echo "# Generated by generate-secrets.sh — secret REFERENCES only (no secret values)." + echo "# Cumulative: re-runs keep every previously-provisioned reference. Safe to commit." + echo "# Install/upgrade with:" + echo "# helm upgrade --install $RELEASE ./developer-portal-helm-chart -n $NAMESPACE \\" + echo "# -f $OUT_FILE -f my_values.yaml" + echo "platform-api:" + echo " secrets:" + echo " existingSecret: $PA_SECRET" + if secret_exists "$DP_SECRET"; then + echo "developer-portal-ui:" + echo " secrets:" + echo " existingSecret: $DP_SECRET" + echo " hasPublicKey: true" + secret_has_key "$DP_SECRET" IDP_CLIENT_SECRET && echo " hasIdpClientSecret: true" + secret_has_key "$DP_SECRET" SERVICE_API_KEY_VALUE && echo " hasServiceApiKeyValue: true" + fi + if secret_exists "$UI_SECRET"; then + echo "ai-workspace-ui:" + echo " secrets:" + echo " existingSecret: $UI_SECRET" + fi +} > "$OUT_FILE" + +echo +echo "Done. Next:" +echo " helm dependency update ./developer-portal-helm-chart # first time / after editing deps" +echo " helm upgrade --install $RELEASE ./developer-portal-helm-chart -n $NAMESPACE \\" +echo " -f $OUT_FILE -f my_values.yaml" diff --git a/kubernetes/helm/developer-portal-helm-chart/templates/NOTES.txt b/kubernetes/helm/developer-portal-helm-chart/templates/NOTES.txt new file mode 100644 index 0000000000..0ef2c38248 --- /dev/null +++ b/kubernetes/helm/developer-portal-helm-chart/templates/NOTES.txt @@ -0,0 +1,85 @@ +{{- $pa := index .Values "platform-api" -}} +{{- $aiw := index .Values "ai-workspace-ui" -}} +{{- $dp := index .Values "developer-portal-ui" -}} +{{- $dpScheme := ternary "http" "https" (eq (dig "tls" "certificateProvider" "cert-manager" (default (dict) $dp)) "none") -}} +Thank you for installing {{ .Chart.Name }} ({{ .Chart.Version }})! + +Your release is named {{ .Release.Name }}. + +Enabled components: +{{- if $pa.enabled }} + • Platform API (shared control plane) +{{- end }} +{{- if $aiw.enabled }} + • AI Workspace UI +{{- end }} +{{- if $dp.enabled }} + • Developer Portal +{{- end }} + +To learn more about the release, try: + + $ helm status {{ .Release.Name }} --namespace {{ .Release.Namespace }} + $ helm get all {{ .Release.Name }} --namespace {{ .Release.Namespace }} + +{{- if $pa.enabled }} + +Platform API: + - Service: {{ include "apip.platformApi.fullname" . }} (port {{ dig "service" "port" 9243 $pa }}) + - Database: {{ dig "config" "database" "driver" "sqlite3" $pa }} + + $ kubectl port-forward svc/{{ include "apip.platformApi.fullname" . }} {{ dig "service" "port" 9243 $pa }}:{{ dig "service" "port" 9243 $pa }} -n {{ .Release.Namespace }} + Then: {{ ternary "https" "http" (dig "platformApi" "tlsEnabled" true .Values.global) }}://localhost:{{ dig "service" "port" 9243 $pa }}/health +{{- end }} + +{{- if $aiw.enabled }} + +AI Workspace UI: + - Service: {{ include "apip.aiWorkspace.fullname" . }} (port {{ dig "service" "port" 9643 $aiw }}, type {{ dig "service" "type" "LoadBalancer" $aiw }}) + - Auth mode: {{ dig "config" "auth" "mode" "basic" $aiw }} + - Platform API URL: {{ default (include "apip.platformApi.internalURL" .) (dig "config" "controlPlane" "url" "" $aiw) }} + +To access it: + {{- if eq (dig "service" "type" "LoadBalancer" $aiw) "NodePort" }} + $ export NODE_PORT=$(kubectl get svc {{ include "apip.aiWorkspace.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}') + $ export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}') + $ echo https://$NODE_IP:$NODE_PORT + {{- else if eq (dig "service" "type" "LoadBalancer" $aiw) "LoadBalancer" }} + $ kubectl get svc {{ include "apip.aiWorkspace.fullname" . }} -n {{ .Release.Namespace }} -w # wait for EXTERNAL-IP + $ echo https://:{{ dig "service" "port" 9643 $aiw }} + {{- else }} + $ kubectl port-forward svc/{{ include "apip.aiWorkspace.fullname" . }} {{ dig "service" "port" 9643 $aiw }}:{{ dig "service" "port" 9643 $aiw }} -n {{ .Release.Namespace }} + Then visit: https://localhost:{{ dig "service" "port" 9643 $aiw }} + {{- end }} +{{- if eq (dig "config" "auth" "mode" "basic" $aiw) "basic" }} + + Log in with the file-mode admin credential generated by ./generate-secrets.sh + (username + password shown once at creation; the bcrypt hash is stored in the + Platform API Secret — there is no default admin/admin). +{{- end }} +{{- end }} + +{{- if $dp.enabled }} + +Developer Portal: + - Service: {{ include "apip.developerPortal.fullname" . }} (port {{ dig "service" "port" 3000 $dp }}, type {{ dig "service" "type" "LoadBalancer" $dp }}) + - Platform API URL: {{ default (include "apip.platformApi.internalURL" .) (dig "config" "platformApi" "baseUrl" "" $dp) }} + +To access it: + {{- if eq (dig "service" "type" "LoadBalancer" $dp) "NodePort" }} + $ export NODE_PORT=$(kubectl get svc {{ include "apip.developerPortal.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}') + $ export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}') + $ echo {{ $dpScheme }}://$NODE_IP:$NODE_PORT/default/views/default + {{- else if eq (dig "service" "type" "LoadBalancer" $dp) "LoadBalancer" }} + $ kubectl get svc {{ include "apip.developerPortal.fullname" . }} -n {{ .Release.Namespace }} -w # wait for EXTERNAL-IP + $ echo {{ $dpScheme }}://:{{ dig "service" "port" 3000 $dp }}/default/views/default + {{- else }} + $ kubectl port-forward svc/{{ include "apip.developerPortal.fullname" . }} {{ dig "service" "port" 3000 $dp }}:{{ dig "service" "port" 3000 $dp }} -n {{ .Release.Namespace }} + Then visit: {{ $dpScheme }}://localhost:{{ dig "service" "port" 3000 $dp }}/default/views/default + {{- end }} + + Log in with the file-mode admin credential generated by ./generate-secrets.sh + (shown once at creation; no default admin/admin), unless you configured OIDC. +{{- end }} + +Documentation: {{ .Chart.Home }} diff --git a/kubernetes/helm/developer-portal-helm-chart/templates/serviceaccount.yaml b/kubernetes/helm/developer-portal-helm-chart/templates/serviceaccount.yaml new file mode 100644 index 0000000000..bb003ec3b6 --- /dev/null +++ b/kubernetes/helm/developer-portal-helm-chart/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- $g := default (dict) .Values.global -}} +{{- $sa := default (dict) $g.serviceAccount -}} +{{- if $sa.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "apip.serviceAccountName" . }} + labels: + {{- include "apip.labels" . | nindent 4 }} + {{- with (include "apip.annotations" (list . $sa.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ $sa.automountServiceAccountToken }} +{{- end }} diff --git a/kubernetes/helm/developer-portal-helm-chart/templates/validation.yaml b/kubernetes/helm/developer-portal-helm-chart/templates/validation.yaml new file mode 100644 index 0000000000..f1e432d898 --- /dev/null +++ b/kubernetes/helm/developer-portal-helm-chart/templates/validation.yaml @@ -0,0 +1,18 @@ +{{/* +Render-time guardrails for the umbrella. Renders no Kubernetes objects; only +fails the install early with an actionable message on misconfiguration. +*/}} +{{- $pa := index .Values "platform-api" -}} +{{- $aiw := index .Values "ai-workspace-ui" -}} +{{- $dp := index .Values "developer-portal-ui" -}} +{{- if not (or $aiw.enabled $dp.enabled) }} +{{- fail "No portal component is enabled. Set ai-workspace-ui.enabled=true and/or developer-portal-ui.enabled=true (the shared platform-api alone serves no UI)." }} +{{- end }} +{{- if not $pa.enabled }} +{{- if and $aiw.enabled (not (dig "config" "controlPlane" "url" "" (default (dict) $aiw))) }} +{{- fail "ai-workspace-ui.enabled=true but platform-api.enabled=false and no external Platform API URL is set. Set platform-api.enabled=true, or set ai-workspace-ui.config.controlPlane.url to an external control plane." }} +{{- end }} +{{- if and $dp.enabled (not (dig "config" "platformApi" "baseUrl" "" (default (dict) $dp))) }} +{{- fail "developer-portal-ui.enabled=true but platform-api.enabled=false and no external Platform API URL is set. Set platform-api.enabled=true, or set developer-portal-ui.config.platformApi.baseUrl to an external control plane." }} +{{- end }} +{{- end }} diff --git a/kubernetes/helm/developer-portal-helm-chart/values-local.yaml b/kubernetes/helm/developer-portal-helm-chart/values-local.yaml new file mode 100644 index 0000000000..e5b0e49859 --- /dev/null +++ b/kubernetes/helm/developer-portal-helm-chart/values-local.yaml @@ -0,0 +1,30 @@ +# Local development values for the developer-portal umbrella chart. +# Usage: +# helm install developer-portal ./developer-portal-helm-chart -f values-secrets.yaml -f values-local.yaml +# +# Overrides the defaults for local development, per component: +# - locally-built images (latest tag, IfNotPresent pull policy) +# - debug logging +# - the portal skips TLS verification to the Platform API and relaxes role checks +# +# NOTE: the components have no self-signed TLS fallback. For a local install, +# provide a TLS Secret (tls.certificateProvider=secret) or install cert-manager. +platform-api: + image: + tag: "latest" + pullPolicy: IfNotPresent + config: + logging: + level: debug + +developer-portal-ui: + image: + tag: "latest" + pullPolicy: IfNotPresent + config: + logging: + level: debug + platformApi: + insecure: true + security: + roleValidation: false diff --git a/kubernetes/helm/developer-portal-helm-chart/values.yaml b/kubernetes/helm/developer-portal-helm-chart/values.yaml new file mode 100644 index 0000000000..4e74055ca5 --- /dev/null +++ b/kubernetes/helm/developer-portal-helm-chart/values.yaml @@ -0,0 +1,80 @@ +# ============================================================================ +# developer-portal — product values +# ============================================================================ +# This is the Developer Portal product package: the shared Platform API plus the +# Developer Portal. You normally keep this file as-is and put your changes in a +# separate `-f my_values.yaml`; both are layered at install time (later wins). +# +# How settings are organised: +# global.* Settings shared by EVERY component (image pull secrets, +# service account, the Platform API address, …). +# .* Settings for one component, keyed by its chart name in +# kebab-case: `platform-api`, `developer-portal-ui`. Each component +# documents its full option list in charts//values.yaml — +# override any of those keys under the same top-level key here. +# +# Secrets are never written in this file. Run ./generate-secrets.sh first: it +# creates the Kubernetes Secrets and writes values-secrets.yaml holding only the +# Secret *names*. Install with `-f values-secrets.yaml`. See README.md. + +# ---------------------------------------------------------------------------- +# global — merged into every component as `.Values.global` +# ---------------------------------------------------------------------------- +global: + # Resource naming. By default every resource is prefixed with the Helm release + # name (e.g. `myrelease-platform-api`). Set fullnameOverride to force a fixed + # prefix regardless of release name; nameOverride tweaks the chart-name part. + nameOverride: "" + fullnameOverride: "" + + # Extra labels / annotations stamped onto every rendered Kubernetes object. + commonLabels: {} + commonAnnotations: {} + + # Names of image-pull Secrets to attach to every pod (for private registries). + # Merged with any per-component imagePullSecrets. + imagePullSecrets: [] + + # A single ServiceAccount is created once per release and shared by all pods. + serviceAccount: + create: true # false → use an existing SA (set name) + name: "" # "" → defaults to the release fullname + annotations: {} # e.g. an IRSA / workload-identity role ARN + automountServiceAccountToken: true + + # WSO2 Subscription (https://wso2.com/subscription/). Set imagePullSecret to the + # name of a docker-registry Secret and two things happen automatically: it's + # added to every pod, and the default ghcr.io/wso2/api-platform/* image repos are + # rewritten to registry.wso2.com/wso2-api-platform/*. Leave "" for public images. + wso2: + subscription: + imagePullSecret: "" + + # How the portal reaches the shared Platform API in-cluster. These build the URL + # the portal calls, so they MUST match the Platform API's actual service port and + # TLS setting below (platform-api.service.port / platform-api.config.tls.enabled). + platformApi: + port: 9243 + tlsEnabled: true # true → https://, false → http:// + +# ---------------------------------------------------------------------------- +# Components — turn each on/off with .enabled +# ---------------------------------------------------------------------------- +# `enabled` maps to the dependency condition in Chart.yaml, so a disabled +# component is not installed at all. For anything deeper (image tag, database, +# auth, resources, …) add keys under the same top-level key — see each +# component's own values.yaml, e.g.: +# +# developer-portal-ui: +# config: +# server: +# baseUrl: https://devportal.example.com # public URL browsers use + +# Shared control plane. Required by the portal; leave enabled unless you point the +# portal at an external Platform API. +platform-api: + enabled: true + +# The Developer Portal — this package's headline component. +developer-portal-ui: + enabled: true diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml index c2c95bc3b9..054f2cc5fd 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml @@ -6,6 +6,9 @@ {{- $tls := $dp.tls -}} {{- $selfSigned := eq $tls.certificateProvider "selfSigned" -}} {{- $tlsFromSecret := or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret") -}} +{{- if and $dp.deployment.enabled (not (or $tlsFromSecret (eq $tls.certificateProvider "none"))) }} +{{- fail "developer-portal-ui: tls.certificateProvider must be \"cert-manager\", \"secret\", or \"none\" (plain HTTP) — there is no self-signed fallback (the image does not generate a certificate at startup). Provide a TLS Secret (certificateProvider=secret), use cert-manager, or set certificateProvider=none to serve plain HTTP behind a TLS-terminating proxy." }} +{{- end }} {{- $secretName := include "apip.developerPortal.secretName" . -}} {{- $imageTag := default .Chart.AppVersion $dp.image.tag -}} {{- $certDir := $tls.mountPath -}} diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml index 086a4bb25b..e4beb5aae4 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml @@ -228,10 +228,14 @@ containerPort: 3000 # tls — certificate for the portal's HTTPS listener (APIP_DP_TLS_*) # ---------------------------------------------------------------------------- tls: - # selfSigned → the app auto-generates a self-signed pair at startup (demo). - # secret → mount an existing Secret's tls.crt / tls.key. - # cert-manager → provision a Certificate via cert-manager and mount it. - certificateProvider: selfSigned + # How the portal's HTTPS listener gets its certificate. The image does NOT + # self-generate one, so supply a working certificate (or disable TLS): + # secret → mount an existing Secret's tls.crt / tls.key. + # cert-manager → provision a Certificate via cert-manager and mount it. + # none → serve plain HTTP (no TLS) — only behind a TLS-terminating proxy. + # NOTE: "selfSigned" is NOT supported — the image has no in-container cert + # generation, so the chart rejects it at render time (fail-fast, like platform-api). + certificateProvider: cert-manager # Where certs are mounted / read from (matches APIP_DP_TLS_CERTFILE etc.). mountPath: /app/certs certManager: