diff --git a/.github/workflows/ai-workspace-ui-helm-release.yml b/.github/workflows/ai-workspace-ui-helm-release.yml new file mode 100644 index 0000000000..9776e143a5 --- /dev/null +++ b/.github/workflows/ai-workspace-ui-helm-release.yml @@ -0,0 +1,158 @@ +name: Release AI Workspace UI Helm Chart + +# Publishes the `ai-workspace-ui` component chart (from the packaging model) to +# GHCR as an OCI artifact. Self-contained — embeds the shared apip.* helpers, no +# chart dependencies. + +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-alpha2)' + required: true + type: string + +env: + CHART_NAME: ai-workspace-ui + CHART_PATH: kubernetes/helm/ai-workspace-ui-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-alpha2)." + 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: Validate image tag matches appVersion + working-directory: ${{ env.CHART_PATH }} + run: | + IMAGE_TAG=$(yq '.image.tag' values.yaml) + + echo "App Version: ${APP_VERSION}" + echo "Image tag: $IMAGE_TAG" + + if [ "$IMAGE_TAG" != "${APP_VERSION}" ]; then + echo "::error::image tag '$IMAGE_TAG' does not match appVersion '${APP_VERSION}' in values.yaml" + echo "::error::Please update image.tag in ${{ env.CHART_PATH }}/values.yaml to match the appVersion '${APP_VERSION}' before releasing." + exit 1 + fi + + echo "✅ Image tag matches appVersion '${APP_VERSION}'" + + - 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: Package Helm chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm package . + echo "Packaged chart:" + ls -la *.tgz + + - 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: 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 UI Helm Chart ${{ inputs.version }} + body: | + ## AI Workspace UI Helm Chart Release ${{ inputs.version }} + + **App Version:** `${{ inputs.appVersion }}` + + ### 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 UI 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-ui-helm-release.yml b/.github/workflows/developer-portal-ui-helm-release.yml new file mode 100644 index 0000000000..3de09174f0 --- /dev/null +++ b/.github/workflows/developer-portal-ui-helm-release.yml @@ -0,0 +1,158 @@ +name: Release Developer Portal UI Helm Chart + +# Publishes the `developer-portal-ui` component chart (from the packaging model) +# to GHCR as an OCI artifact. Self-contained — embeds the shared apip.* helpers, +# no chart dependencies. + +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-alpha2)' + required: true + type: string + +env: + CHART_NAME: developer-portal-ui + CHART_PATH: kubernetes/helm/developer-portal-ui-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-alpha2)." + 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: Validate image tag matches appVersion + working-directory: ${{ env.CHART_PATH }} + run: | + IMAGE_TAG=$(yq '.image.tag' values.yaml) + + echo "App Version: ${APP_VERSION}" + echo "Image tag: $IMAGE_TAG" + + if [ "$IMAGE_TAG" != "${APP_VERSION}" ]; then + echo "::error::image tag '$IMAGE_TAG' does not match appVersion '${APP_VERSION}' in values.yaml" + echo "::error::Please update image.tag in ${{ env.CHART_PATH }}/values.yaml to match the appVersion '${APP_VERSION}' before releasing." + exit 1 + fi + + echo "✅ Image tag matches appVersion '${APP_VERSION}'" + + - 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: Package Helm chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm package . + echo "Packaged chart:" + ls -la *.tgz + + - 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: 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 UI Helm Chart ${{ inputs.version }} + body: | + ## Developer Portal UI Helm Chart Release ${{ inputs.version }} + + **App Version:** `${{ inputs.appVersion }}` + + ### 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 UI 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/platform-api-helm-release.yml b/.github/workflows/platform-api-helm-release.yml new file mode 100644 index 0000000000..fa7ae8b511 --- /dev/null +++ b/.github/workflows/platform-api-helm-release.yml @@ -0,0 +1,158 @@ +name: Release Platform API Helm Chart + +# Publishes the `platform-api` component chart (from the packaging model) to GHCR +# as an OCI artifact. Self-contained — embeds the shared apip.* helpers, no +# chart dependencies. + +on: + workflow_dispatch: + inputs: + version: + description: 'Chart version (e.g., 0.1.0)' + required: true + type: string + appVersion: + description: 'App version (e.g., 0.12.0)' + required: true + type: string + +env: + CHART_NAME: platform-api + CHART_PATH: kubernetes/helm/platform-api-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. 0.12.0)." + 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: Validate image tag matches appVersion + working-directory: ${{ env.CHART_PATH }} + run: | + IMAGE_TAG=$(yq '.image.tag' values.yaml) + + echo "App Version: ${APP_VERSION}" + echo "Image tag: $IMAGE_TAG" + + if [ "$IMAGE_TAG" != "${APP_VERSION}" ]; then + echo "::error::image tag '$IMAGE_TAG' does not match appVersion '${APP_VERSION}' in values.yaml" + echo "::error::Please update image.tag in ${{ env.CHART_PATH }}/values.yaml to match the appVersion '${APP_VERSION}' before releasing." + exit 1 + fi + + echo "✅ Image tag matches appVersion '${APP_VERSION}'" + + - 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: Package Helm chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm package . + echo "Packaged chart:" + ls -la *.tgz + + - 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: 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: Platform API Helm Chart ${{ inputs.version }} + body: | + ## Platform API Helm Chart Release ${{ inputs.version }} + + **App Version:** `${{ inputs.appVersion }}` + + ### 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 "### Platform API 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/.gitignore b/kubernetes/helm/.gitignore new file mode 100644 index 0000000000..8320388e0b --- /dev/null +++ b/kubernetes/helm/.gitignore @@ -0,0 +1,9 @@ +# Vendored subchart archives + lockfiles fetched by `helm dependency update`. +# Build artifacts (the umbrellas pull their components in) — never commit them. +# Generic so they cover every chart (component and umbrella), regardless of path. +**/charts/ +**/Chart.lock + +# Generated secret references (created by generate-secrets.sh; no secret values, +# but environment-specific). +**/values-secrets.yaml diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/.helmignore b/kubernetes/helm/ai-workspace-ui-helm-chart/.helmignore new file mode 100644 index 0000000000..4620fd7ee3 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/.helmignore @@ -0,0 +1,16 @@ +# 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 diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml new file mode 100644 index 0000000000..b40389ffde --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v2 +name: ai-workspace-ui +description: >- + WSO2 API Platform — AI Workspace UI (React SPA + Go BFF). Independently + released component chart; consumed by the ai-workspace product package + (and pluggable into others) and installable standalone. +type: application +version: 1.0.0-alpha +appVersion: "1.0.0-beta" +home: https://github.com/wso2/api-platform +sources: + - https://github.com/wso2/api-platform + \ No newline at end of file diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl new file mode 100644 index 0000000000..0dbb30bf87 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl @@ -0,0 +1,221 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Shared helpers for the api-platform-portals suite. + +Cross-cutting configuration (labels/annotations, image pull secrets, service +account, subscription registry, and the shared Platform API service coordinates) +is read from `.Values.global.*` so every component subchart resolves it +identically. Component-specific config is read from the subchart's own `.Values` +by the calling templates, not here. + +Component resource names are derived from the release name with a fixed suffix +(NOT from .Chart.Name), so a portal subchart can compute the Platform API's +in-cluster Service name even though it lives in a different subchart. +*/}} + +{{/* Base name: release name, or global.fullnameOverride when set. */}} +{{- define "apip.fullname" -}} +{{- $g := default (dict) .Values.global -}} +{{- if $g.fullnameOverride -}} +{{- $g.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{/* Component full names — fixed suffixes, release-derived, cross-subchart stable. */}} +{{/* Trunc the base first to reserve room for the suffix — otherwise a long + release name is truncated AFTER the suffix is appended, chopping the suffix + and risking cross-component name collisions. */}} +{{- define "apip.platformApi.fullname" -}} +{{- printf "%s-platform-api" (include "apip.fullname" . | trunc 50 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.aiWorkspace.fullname" -}} +{{- printf "%s-ai-workspace" (include "apip.fullname" . | trunc 50 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.developerPortal.fullname" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" . | trunc 45 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "apip.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Render a string-keyed metadata map (labels/annotations) as YAML. Values are +coerced to strings so numbers/bools render quoted. Null values are skipped. +*/}} +{{- define "apip.renderStringMap" -}} +{{- $out := dict -}} +{{- range $k, $v := . -}} +{{- if not (kindIs "invalid" $v) -}} +{{- $_ := set $out $k (toString $v) -}} +{{- end -}} +{{- end -}} +{{- toYaml $out -}} +{{- end -}} + +{{/* Standard labels. `name` = subchart chart name; all components share part-of. */}} +{{- define "apip.labels" -}} +{{- $g := default (dict) .Values.global -}} +{{- $std := dict + "helm.sh/chart" (include "apip.chart" .) + "app.kubernetes.io/name" .Chart.Name + "app.kubernetes.io/managed-by" .Release.Service + "app.kubernetes.io/instance" .Release.Name + "app.kubernetes.io/part-of" "api-platform-portals" + "app.kubernetes.io/version" .Chart.AppVersion -}} +{{- include "apip.renderStringMap" (merge (dict) (default (dict) $g.commonLabels) $std) -}} +{{- end -}} + +{{/* Standard labels + extra (extra wins). Args (list): root, extraLabels|nil */}} +{{- define "apip.resourceLabels" -}} +{{- $root := index . 0 -}} +{{- $extra := default (dict) (index . 1) -}} +{{- $base := fromYaml (include "apip.labels" $root) -}} +{{- include "apip.renderStringMap" (merge (dict) $extra $base) -}} +{{- end -}} + +{{- define "apip.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* Standard + component label + extra (extra > component > commonLabels > std). Args: root, component, extra|nil */}} +{{- define "apip.componentLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{- $extra := default (dict) (index . 2) -}} +{{- $base := fromYaml (include "apip.labels" $root) -}} +{{- include "apip.renderStringMap" (merge (dict) $extra (dict "app.kubernetes.io/component" $component) $base) -}} +{{- end -}} + +{{/* Pod-template labels: selector keys always win. Args: root, component, podLabels|nil */}} +{{- define "apip.componentPodLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{- $podLabels := default (dict) (index . 2) -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $selector := fromYaml (include "apip.componentSelectorLabels" (list $root $component)) -}} +{{- include "apip.renderStringMap" (merge (dict) $selector $podLabels (default (dict) $g.commonLabels)) -}} +{{- end -}} + +{{/* +Merge global.commonAnnotations with per-resource annotations (specific wins). +Emits nothing when both empty. Args (list): root, specificAnnotations|nil +*/}} +{{- define "apip.annotations" -}} +{{- $root := index . 0 -}} +{{- $specific := default (dict) (index . 1) -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $merged := merge (dict) $specific (default (dict) $g.commonAnnotations) -}} +{{- if $merged -}} +{{- include "apip.renderStringMap" $merged -}} +{{- end -}} +{{- end -}} + +{{- define "apip.componentSelectorLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{ include "apip.selectorLabels" $root }} +app.kubernetes.io/component: {{ $component }} +{{- end -}} + +{{/* Shared service account name (one SA per release, created at umbrella level). */}} +{{- define "apip.serviceAccountName" -}} +{{- $g := default (dict) .Values.global -}} +{{- $sa := default (dict) $g.serviceAccount -}} +{{- if $sa.create -}} +{{- default (include "apip.fullname" .) $sa.name -}} +{{- else -}} +{{- default "default" $sa.name -}} +{{- end -}} +{{- end -}} + +{{/* +Default in-cluster URL portals use to reach the shared Platform API. Scheme and +port come from global.platformApi so portals need not read the platform-api +subchart's own values. +*/}} +{{- define "apip.platformApi.internalURL" -}} +{{- $g := default (dict) .Values.global -}} +{{- $pa := default (dict) $g.platformApi -}} +{{- $tlsEnabled := true -}} +{{- if hasKey $pa "tlsEnabled" -}}{{- $tlsEnabled = $pa.tlsEnabled -}}{{- end -}} +{{- $scheme := ternary "https" "http" $tlsEnabled -}} +{{- printf "%s://%s:%d" $scheme (include "apip.platformApi.fullname" .) (int (default 9243 $pa.port)) -}} +{{- end -}} + +{{/* +Render a component image reference, applying the WSO2 subscription registry +rewrite only when global.wso2.subscription.imagePullSecret is set AND the +repository is exactly the chart-canonical default. Explicit overrides pass +through unchanged. Args (dict): root, repository, defaultRepository, tag +*/}} +{{- define "apip.componentImage" -}} +{{- $root := .root -}} +{{- $repo := .repository -}} +{{- $defaultRepo := .defaultRepository -}} +{{- $tag := .tag -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $wso2sub := default (dict) (default (dict) $g.wso2).subscription -}} +{{- $sub := default "" $wso2sub.imagePullSecret -}} +{{- $defaultPrefix := "ghcr.io/wso2/api-platform/" -}} +{{- $wso2Prefix := "registry.wso2.com/wso2-api-platform/" -}} +{{- if and (ne $sub "") (eq $repo $defaultRepo) (hasPrefix $defaultPrefix $repo) -}} +{{- printf "%s%s:%s" $wso2Prefix (trimPrefix $defaultPrefix $repo) $tag -}} +{{- else -}} +{{- printf "%s:%s" $repo $tag -}} +{{- end -}} +{{- end -}} + +{{/* +Render an `imagePullSecrets:` block by merging global.wso2.subscription secret, +global.imagePullSecrets, and component-level pull secrets. Empty string when none. +Args (dict): root, componentPullSecrets +*/}} +{{- define "apip.componentImagePullSecretsBlock" -}} +{{- $root := .root -}} +{{- $componentPullSecrets := default (list) .componentPullSecrets -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $globalPullSecrets := default (list) $g.imagePullSecrets -}} +{{- $wso2sub := default (dict) (default (dict) $g.wso2).subscription -}} +{{- $sub := default "" $wso2sub.imagePullSecret -}} +{{- $subList := ternary (list $sub) (list) (ne $sub "") -}} +{{- $all := concat $subList $globalPullSecrets $componentPullSecrets -}} +{{- if $all -}} +imagePullSecrets: +{{- range $all }} + - name: {{ . }} +{{- end }} +{{- end -}} +{{- end -}} + +{{/* +External-secrets model ("setup generates, startup only checks"). Each helper runs +in its OWN subchart's context, so `.Values.secrets.existingSecret` resolves to +that component's secret. Fails the render when a required secret is unset. +*/}} +{{- define "apip.platformApi.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "platformApi.secrets.existingSecret is required. Run ./generate-secrets.sh to create the Secrets and write values-secrets.yaml, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} + +{{- define "apip.aiWorkspace.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "aiWorkspace.secrets.existingSecret is required when aiWorkspace.config.authMode is \"oidc\". Run ./generate-secrets.sh with AIW_OIDC_CLIENT_SECRET set, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} + +{{- define "apip.developerPortal.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "developerPortal.secrets.existingSecret is required. Run ./generate-secrets.sh to create the Secrets and write values-secrets.yaml, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/certificate.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/certificate.yaml new file mode 100644 index 0000000000..174cb082e8 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/certificate.yaml @@ -0,0 +1,37 @@ +{{- $ui := .Values -}} +{{- $tls := $ui.tls -}} +{{- if and $ui.deployment.enabled (eq $tls.certificateProvider "cert-manager") $tls.certManager.create }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "apip.aiWorkspace.fullname" . }}-tls + labels: + {{- include "apip.componentLabels" (list . "ai-workspace" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + secretName: {{ include "apip.aiWorkspace.fullname" . }}-tls + commonName: {{ $tls.certManager.commonName }} + dnsNames: + {{- range $tls.certManager.dnsNames }} + - {{ . | quote }} + {{- end }} + duration: {{ $tls.certManager.duration | quote }} + renewBefore: {{ $tls.certManager.renewBefore | quote }} + issuerRef: + {{- if $tls.certManager.createIssuer }} + name: {{ include "apip.aiWorkspace.fullname" . }}-selfsigned-issuer + {{- else }} + name: {{ $tls.certManager.issuerRef.name }} + {{- end }} + kind: {{ $tls.certManager.issuerRef.kind }} + {{- if $tls.certManager.issuerRef.group }} + group: {{ $tls.certManager.issuerRef.group }} + {{- end }} + privateKey: + algorithm: RSA + size: 2048 +{{- end }} + diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml new file mode 100644 index 0000000000..4bb42cc9ff --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml @@ -0,0 +1,91 @@ +{{- $ui := .Values -}} +{{- $cfg := $ui.config -}} +{{- $tls := $ui.tls -}} +{{- $controlPlaneUrl := default (include "apip.platformApi.internalURL" .) $cfg.controlPlane.url -}} +{{- $domain := default (printf "localhost:%d" (int $ui.containerPort)) $cfg.server.domain -}} +{{- if $ui.deployment.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "apip.aiWorkspace.fullname" . }}-config + labels: + {{- include "apip.componentLabels" (list . "ai-workspace" $ui.configMap.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $ui.configMap.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +data: + # Full [ai_workspace.*] config, mounted at /etc/ai-workspace/config.toml. The + # BFF reads it via its own {{ "{{ env \"APIP_AIW_*\" }}" }} tokens. Non-secret + # values are literals from values.yaml; the OIDC client secret is a token, + # supplied as an env var from the referenced Secret (Secret -> env -> config). + config.toml: | + [ai_workspace] + default_org_region = {{ $cfg.defaultOrgRegion | quote }} + domain = {{ $domain | quote }} + + [ai_workspace.server.http] + enabled = {{ $cfg.server.http.enabled }} + port = {{ $cfg.server.http.port | int }} + + [ai_workspace.server.https] + enabled = true + port = {{ $ui.containerPort | int }} + cert_file = "/etc/ai-workspace/tls/cert.pem" + key_file = "/etc/ai-workspace/tls/key.pem" + + [ai_workspace.logging] + level = {{ $cfg.logging.level | quote }} + format = {{ $cfg.logging.format | quote }} + browser_debug = {{ $cfg.logging.browserDebug | toString | quote }} + + [ai_workspace.control_plane] + url = {{ $controlPlaneUrl | quote }} + tls_skip_verify = {{ $cfg.controlPlane.tlsSkipVerify | toString | quote }} + ca_file = {{ $cfg.controlPlane.caFile | quote }} + proxy_prefix = {{ $cfg.controlPlane.proxyPrefix | quote }} + portal_base_path = {{ $cfg.controlPlane.portalBasePath | quote }} + + [ai_workspace.gateway] + controlplane_host = {{ $cfg.gateway.controlplaneHost | quote }} + platform_gateway_versions = {{ $cfg.gateway.platformGatewayVersions | quote }} + + [ai_workspace.session] + store = {{ $cfg.session.store | quote }} + idle_timeout = {{ $cfg.session.idleTimeout | quote }} + absolute_ttl = {{ $cfg.session.absoluteTtl | quote }} + + [ai_workspace.auth] + mode = {{ $cfg.auth.mode | quote }} + + [ai_workspace.auth.claim_mappings] + organization = {{ $cfg.auth.claimMappings.organization | quote }} + org_name = {{ $cfg.auth.claimMappings.orgName | quote }} + org_handle = {{ $cfg.auth.claimMappings.orgHandle | quote }} + username = {{ $cfg.auth.claimMappings.username | quote }} + email = {{ $cfg.auth.claimMappings.email | quote }} + scope = {{ $cfg.auth.claimMappings.scope | quote }} + roles = {{ $cfg.auth.claimMappings.roles | quote }} + + {{- if eq $cfg.auth.mode "oidc" }} + + [ai_workspace.auth.oidc] + enabled = "true" + authority = {{ required "config.auth.oidc.authority is required when auth.mode is \"oidc\"" $cfg.auth.oidc.authority | quote }} + client_id = {{ required "config.auth.oidc.clientId is required when auth.mode is \"oidc\"" $cfg.auth.oidc.clientId | quote }} + client_secret = {{ `'{{ env "APIP_AIW_AUTH_OIDC_CLIENT_SECRET" }}'` }} + {{- with $cfg.auth.oidc.redirectUrl }} + redirect_url = {{ . | quote }} + {{- end }} + {{- with $cfg.auth.oidc.postLogoutRedirectUrl }} + post_logout_redirect_url = {{ . | quote }} + {{- end }} + {{- with $cfg.auth.oidc.scope }} + scope = {{ . | quote }} + {{- end }} + {{- end }} + {{- with $ui.configToml }} + + {{ . | nindent 4 | trim }} + {{- end }} +{{- end }} diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml new file mode 100644 index 0000000000..5a30a88932 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml @@ -0,0 +1,170 @@ +{{- $ui := .Values -}} +{{- $deployment := $ui.deployment -}} +{{- $cfg := $ui.config -}} +{{- $secrets := $ui.secrets -}} +{{- $tls := $ui.tls -}} +{{- $selfSigned := eq $tls.certificateProvider "selfSigned" -}} +{{- $tlsMounted := or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret") -}} +{{- $imageTag := default .Chart.AppVersion $ui.image.tag -}} +{{- if $deployment.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "apip.aiWorkspace.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "ai-workspace" $deployment.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $deployment.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + replicas: {{ $deployment.replicaCount }} + {{- with $deployment.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "apip.componentSelectorLabels" (list . "ai-workspace") | nindent 6 }} + template: + metadata: + labels: + {{- include "apip.componentPodLabels" (list . "ai-workspace" $deployment.podLabels) | nindent 8 }} + {{- $podAnnotations := omit (merge (dict) (default (dict) $deployment.podAnnotations) (default (dict) .Values.global.commonAnnotations)) "checksum/config" }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with $podAnnotations }} + {{- include "apip.renderStringMap" . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "apip.serviceAccountName" . }} + {{- with (include "apip.componentImagePullSecretsBlock" (dict "root" . "componentPullSecrets" $ui.imagePullSecrets)) }} + {{- . | nindent 6 }} + {{- end }} + {{- with $deployment.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with $deployment.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if hasKey $deployment "terminationGracePeriodSeconds" }} + terminationGracePeriodSeconds: {{ $deployment.terminationGracePeriodSeconds }} + {{- end }} + {{- with $deployment.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.dnsPolicy }} + dnsPolicy: {{ . }} + {{- end }} + {{- with $deployment.dnsConfig }} + dnsConfig: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if hasKey $deployment "automountServiceAccountToken" }} + automountServiceAccountToken: {{ $deployment.automountServiceAccountToken }} + {{- end }} + containers: + - name: ai-workspace + image: {{ include "apip.componentImage" (dict "root" . "repository" $ui.image.repository "defaultRepository" "ghcr.io/wso2/api-platform/ai-workspace" "tag" $imageTag) | quote }} + imagePullPolicy: {{ $ui.image.pullPolicy }} + {{- with $deployment.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if eq $cfg.auth.mode "oidc" }} + env: + # Config is delivered via the mounted config.toml (see configmap.yaml); + # only the OIDC client secret is injected here, resolved into that + # config by its {{ "{{ env \"APIP_AIW_AUTH_OIDC_CLIENT_SECRET\" }}" }} + # token. The secretName helper fails the render if secrets.existingSecret + # is unset. + - name: APIP_AIW_AUTH_OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "apip.aiWorkspace.secretName" . }} + key: {{ $secrets.keys.oidcClientSecret }} + {{- range $deployment.extraEnv }} + - {{- toYaml . | nindent 14 }} + {{- end }} + {{- else if $deployment.extraEnv }} + env: + {{- range $deployment.extraEnv }} + - {{- toYaml . | nindent 14 }} + {{- end }} + {{- end }} + {{- with $deployment.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ $ui.containerPort }} + protocol: TCP + livenessProbe: + {{- toYaml $deployment.livenessProbe | nindent 12 }} + {{- with $deployment.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + readinessProbe: + {{- toYaml $deployment.readinessProbe | nindent 12 }} + resources: + {{- toYaml ($deployment.resources | default dict) | nindent 12 }} + volumeMounts: + - name: config + mountPath: /etc/ai-workspace/config.toml + subPath: config.toml + {{- if $tlsMounted }} + - name: tls-certs + mountPath: /etc/ai-workspace/tls + readOnly: true + {{- end }} + {{- with $deployment.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: config + configMap: + name: {{ include "apip.aiWorkspace.fullname" . }}-config + items: + - key: config.toml + path: config.toml + {{- if $tlsMounted }} + - name: tls-certs + secret: + {{- if eq $tls.certificateProvider "cert-manager" }} + secretName: {{ include "apip.aiWorkspace.fullname" . }}-tls + {{- else if eq $tls.certificateProvider "secret" }} + secretName: {{ $tls.secret.name }} + {{- end }} + items: + # Mount as cert.pem/key.pem to match config cert_file/key_file + # (/etc/ai-workspace/tls/cert.pem, .../key.pem). + - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.crt{{ else }}{{ $tls.secret.certKey }}{{ end }} + path: cert.pem + - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.key{{ else }}{{ $tls.secret.keyKey }}{{ end }} + path: key.pem + {{- end }} + {{- with $deployment.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/hpa.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/hpa.yaml new file mode 100644 index 0000000000..7e5185ce91 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/hpa.yaml @@ -0,0 +1,48 @@ +{{- $ui := .Values -}} +{{- $hpa := $ui.hpa -}} +{{- if and $ui.deployment.enabled $hpa.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "apip.aiWorkspace.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "ai-workspace" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "apip.aiWorkspace.fullname" . }} + minReplicas: {{ $hpa.minReplicas }} + maxReplicas: {{ $hpa.maxReplicas }} + {{- if or $hpa.targetCPUUtilizationPercentage $hpa.targetMemoryUtilizationPercentage $hpa.customMetrics }} + metrics: + {{- if $hpa.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $hpa.targetCPUUtilizationPercentage }} + {{- end }} + {{- if $hpa.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $hpa.targetMemoryUtilizationPercentage }} + {{- end }} + {{- with $hpa.customMetrics }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with $hpa.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/issuer.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/issuer.yaml new file mode 100644 index 0000000000..871640205a --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/issuer.yaml @@ -0,0 +1,22 @@ +{{- $ui := .Values -}} +{{- $tls := $ui.tls -}} +{{- if and $ui.deployment.enabled (eq $tls.certificateProvider "cert-manager") }} +{{- if and (eq $tls.certManager.issuerRef.kind "Issuer") $tls.certManager.createIssuer }} +--- +# Self-signed Issuer for development/testing. In production replace this with a +# proper CA issuer (Let's Encrypt, corporate CA, etc.) via createIssuer=false. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "apip.aiWorkspace.fullname" . }}-selfsigned-issuer + labels: + {{- include "apip.componentLabels" (list . "ai-workspace" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + selfSigned: {} +{{- end }} +{{- end }} + diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/pdb.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/pdb.yaml new file mode 100644 index 0000000000..9d3f5e1b60 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/pdb.yaml @@ -0,0 +1,30 @@ +{{- $ui := .Values -}} +{{- $pdb := $ui.podDisruptionBudget -}} +{{- if and $ui.deployment.enabled $pdb.enabled }} +{{- if and $pdb.minAvailable $pdb.maxUnavailable }} +{{- fail "ui.podDisruptionBudget: only one of minAvailable or maxUnavailable may be set" }} +{{- end }} +{{- if not (or $pdb.minAvailable $pdb.maxUnavailable) }} +{{- fail "ui.podDisruptionBudget: one of minAvailable or maxUnavailable must be set" }} +{{- end }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "apip.aiWorkspace.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "ai-workspace" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "apip.componentSelectorLabels" (list . "ai-workspace") | nindent 6 }} + {{- if $pdb.minAvailable }} + minAvailable: {{ $pdb.minAvailable }} + {{- else if $pdb.maxUnavailable }} + maxUnavailable: {{ $pdb.maxUnavailable }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/service.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/service.yaml new file mode 100644 index 0000000000..8578d33566 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/service.yaml @@ -0,0 +1,50 @@ +{{- $ui := .Values -}} +{{- $service := $ui.service -}} +{{- $isNodePortCapable := or (eq $service.type "NodePort") (eq $service.type "LoadBalancer") -}} +{{- if $ui.deployment.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "apip.aiWorkspace.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "ai-workspace" $service.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $service.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + type: {{ $service.type }} + {{- with $service.clusterIP }} + clusterIP: {{ . }} + {{- end }} + {{- if and $service.externalTrafficPolicy $isNodePortCapable }} + externalTrafficPolicy: {{ $service.externalTrafficPolicy }} + {{- end }} + {{- if eq $service.type "LoadBalancer" }} + {{- with $service.loadBalancerClass }} + loadBalancerClass: {{ . }} + {{- end }} + {{- with $service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with $service.ipFamilyPolicy }} + ipFamilyPolicy: {{ . }} + {{- end }} + {{- with $service.ipFamilies }} + ipFamilies: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + {{- include "apip.componentSelectorLabels" (list . "ai-workspace") | nindent 4 }} + ports: + - name: http + port: {{ $service.port }} + targetPort: http + protocol: TCP + {{- if and $isNodePortCapable $service.nodePort }} + nodePort: {{ $service.nodePort }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml new file mode 100644 index 0000000000..69aa259dee --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml @@ -0,0 +1,17 @@ +# Local development values for the ai-workspace-ui chart. +# Usage (standalone, from kubernetes/helm/): +# helm install ai-workspace-ui ./ai-workspace-ui-helm-chart \ +# -f ai-workspace-ui-helm-chart/values.yaml -f ai-workspace-ui-helm-chart/values-local.yaml +# +# Overrides the defaults for local development: +# - locally-built image (latest tag, IfNotPresent pull policy) +# - debug logging; skip TLS verification to the Platform API's self-signed cert +image: + tag: "latest" + pullPolicy: IfNotPresent + +config: + logging: + level: debug + controlPlane: + tlsSkipVerify: true diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml new file mode 100644 index 0000000000..b493e613a4 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml @@ -0,0 +1,276 @@ +# ============================================================================ +# ai-workspace — component chart values +# ============================================================================ +# The AI Workspace UI: a React single-page app served by a small Go BFF (backend +# for frontend). It is stateless (no database) and talks only to the Platform API. +# Released on its own; used as an umbrella subchart or standalone. +# +# global.* Cross-cutting settings. Under an umbrella these come from the +# umbrella; the block below is the STANDALONE default, overridden +# when nested. +# (rest) This component's own settings. +# +# `enabled` is read by the umbrella's dependency condition (ignored standalone). + +# ---------------------------------------------------------------------------- +# global — STANDALONE defaults only (an umbrella's global.* overrides these) +# ---------------------------------------------------------------------------- +global: + nameOverride: "" + fullnameOverride: "" + commonLabels: {} + commonAnnotations: {} + imagePullSecrets: [] + serviceAccount: + # Standalone default is `false` → pods use the namespace "default" SA (this + # component ships no ServiceAccount template; the umbrella creates one). + create: false + name: "" + annotations: {} + automountServiceAccountToken: true + wso2: + subscription: + imagePullSecret: "" + platformApi: + port: 9243 + tlsEnabled: true + +# ---------------------------------------------------------------------------- +# Component toggle + container image +# ---------------------------------------------------------------------------- +# Install the AI Workspace UI. Opt in per install (the umbrella sets this). +enabled: false +image: + repository: ghcr.io/wso2/api-platform/ai-workspace + tag: "1.0.0-alpha2" + pullPolicy: IfNotPresent +# Pull secrets for THIS component only (merged with global.imagePullSecrets). +imagePullSecrets: [] + +# ---------------------------------------------------------------------------- +# config — rendered into /etc/ai-workspace/config.toml under [ai_workspace.*] +# (portals/ai-workspace/configs/config-template.toml). Non-secret values are +# literals; the OIDC client secret comes from the referenced Secret via an +# APIP_AIW_* env token. +# ---------------------------------------------------------------------------- +config: + # Region assigned to a new organization the first time someone logs in. + defaultOrgRegion: us + + server: + # Public host (or host:port) the browser uses to reach the UI; shown in the + # SPA and used to build redirect URLs. Leave "" to derive from the service. + domain: "" + # Optional plain-HTTP listener — enable only behind a TLS-terminating + # proxy/ingress. The HTTPS listener always runs on containerPort. + http: + enabled: false + port: 9080 + + logging: + level: info # debug | info | warn | error + format: text # text | json + browserDebug: false # verbose console logging in the SPA + + # How the BFF reaches the Platform API (server-to-server hop). The scheme in + # `url` decides whether the hop uses TLS. + controlPlane: + # Leave "" to auto-derive the in-cluster Service URL from global.platformApi. + # Set only to point at an external Platform API. + url: "" + # Skip TLS verification on the BFF → Platform API hop (needed while the + # Platform API uses a self-signed cert). Prefer caFile in production. + tlsSkipVerify: false + # CA file to verify the Platform API's certificate instead of skipping + # verification (mount it via deployment.extraVolumes/extraVolumeMounts). + caFile: "" + # Browser-facing proxy prefix and the Platform API portal base path — the + # SPA never talks to the Platform API directly; the BFF proxies under these. + proxyPrefix: /proxy + portalBasePath: /api/portal/v0.9 + + gateway: + # Public host:port that DEPLOYED GATEWAYS dial to reach the Platform API. + # Shown verbatim in the in-UI "add a gateway" instructions (must be + # externally reachable, not a proxy path). + controlplaneHost: "" + # Gateway build options offered in the create-gateway dropdown (JSON string). + platformGatewayVersions: '[{"version":"1.2","latestVersion":"v1.2.0-M1","channel":"STS"}]' + + session: + # Server-side session store. Only "memory" is supported today. + store: memory + idleTimeout: 30m # sliding inactivity window + absoluteTtl: 8h # hard session lifetime regardless of activity + + auth: + # How users sign in: + # basic → the Platform API's file-based login + # oidc → an external OIDC provider (fill in auth.oidc below + the secret) + mode: basic + # Token claim-name mappings — must agree with the Platform API's own + # claim_mappings (basic mode signs tokens using these names). + claimMappings: + organization: organization + orgName: org_name + orgHandle: org_handle + username: username + email: email + scope: scope + roles: roles + # OIDC settings — used only when mode=oidc. The client secret is NOT here; + # it comes from the Secret (secrets.keys.oidcClientSecret). + oidc: + authority: "" # issuer / discovery base URL + clientId: "" + redirectUrl: "" # login callback URL + postLogoutRedirectUrl: "" + scope: "" # e.g. "openid profile email" + +# Escape hatch: raw TOML appended verbatim to the generated config.toml. +configToml: "" + +# ---------------------------------------------------------------------------- +# secrets — OIDC client secret (REQUIRED only in OIDC mode) +# ---------------------------------------------------------------------------- +# The BFF is a confidential OIDC client, so it needs a client secret when +# authMode=oidc. In basic mode this is unused. Point existingSecret at a Secret +# created by ./generate-secrets.sh; the chart never embeds the value. +secrets: + existingSecret: "" + keys: + oidcClientSecret: OIDC_CLIENT_SECRET + +# ---------------------------------------------------------------------------- +# service — how the UI is exposed +# ---------------------------------------------------------------------------- +service: + type: LoadBalancer # LoadBalancer | NodePort | ClusterIP + annotations: {} + labels: {} + port: 9643 + clusterIP: "" + externalTrafficPolicy: "" + loadBalancerClass: "" + loadBalancerSourceRanges: [] + ipFamilyPolicy: "" + ipFamilies: [] + nodePort: "" + +# Port the BFF process listens on (the BFF's native default). +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 + certManager: + create: true + createIssuer: true + issuerRef: + name: selfsigned-issuer + kind: Issuer + # group: cert-manager.io + commonName: ai-workspace.localhost + dnsNames: + - ai-workspace.localhost + - "*.localhost" + duration: 2160h + renewBefore: 720h + secret: + name: ai-workspace-tls # used when certificateProvider=secret + certKey: tls.crt + keyKey: tls.key + +# ---------------------------------------------------------------------------- +# deployment — the pod/Deployment spec +# ---------------------------------------------------------------------------- +deployment: + enabled: true + replicaCount: 1 + extraEnv: [] + extraEnvFrom: [] + extraVolumeMounts: [] + extraVolumes: [] + labels: {} + annotations: {} + podAnnotations: {} + podLabels: {} + priorityClassName: "" + livenessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTPS + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /healthz + port: http + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + # Optional startup probe for slow first starts (uncomment to enable): + # startupProbe: + # httpGet: + # path: /healthz + # port: http + # scheme: HTTPS + # periodSeconds: 5 + # failureThreshold: 30 + # Set CPU requests if you enable the HPA, so it can compute utilization. + resources: {} + # limits: + # cpu: 500m + # memory: 256Mi + # requests: + # cpu: 100m + # memory: 128Mi + podSecurityContext: {} + securityContext: {} + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + strategy: {} + # terminationGracePeriodSeconds: 30 + hostAliases: [] + dnsPolicy: "" + dnsConfig: {} + # automountServiceAccountToken: false + +# ---------------------------------------------------------------------------- +# Autoscaling & disruption budget +# ---------------------------------------------------------------------------- +# HorizontalPodAutoscaler. The UI is stateless (no database), so — unlike the +# Platform API / Developer Portal — there is NO postgres requirement; it can scale +# freely. Off by default. +hpa: + enabled: false + minReplicas: 2 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 # 0 or "" to disable CPU-based scaling + targetMemoryUtilizationPercentage: "" # set a number to also scale on memory + customMetrics: [] # extra autoscaling/v2 MetricSpec entries + behavior: {} # autoscaling/v2 scaleUp/scaleDown tuning + +# PodDisruptionBudget. Only meaningful at replicaCount >= 2 (with one replica +# minAvailable: 1 blocks node drains). Set exactly ONE of the two below. +podDisruptionBudget: + enabled: false + minAvailable: 1 # integer or percentage string, e.g. "50%" + maxUnavailable: "" # integer or percentage string, e.g. "25%" + +# Extra metadata for the generated ConfigMap. +configMap: + annotations: {} + labels: {} diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/.helmignore b/kubernetes/helm/developer-portal-ui-helm-chart/.helmignore new file mode 100644 index 0000000000..4620fd7ee3 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/.helmignore @@ -0,0 +1,16 @@ +# 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 diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml new file mode 100644 index 0000000000..068c34dccd --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +name: developer-portal-ui +description: >- + WSO2 API Platform — Developer Portal (Node.js app). Independently released + component chart; consumed by the developer-portal product package + (and pluggable into others) and installable standalone. +type: application +version: 1.0.0-alpha +appVersion: "1.0.0-beta" +home: https://github.com/wso2/api-platform +sources: + - https://github.com/wso2/api-platform diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl b/kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl new file mode 100644 index 0000000000..0dbb30bf87 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl @@ -0,0 +1,221 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Shared helpers for the api-platform-portals suite. + +Cross-cutting configuration (labels/annotations, image pull secrets, service +account, subscription registry, and the shared Platform API service coordinates) +is read from `.Values.global.*` so every component subchart resolves it +identically. Component-specific config is read from the subchart's own `.Values` +by the calling templates, not here. + +Component resource names are derived from the release name with a fixed suffix +(NOT from .Chart.Name), so a portal subchart can compute the Platform API's +in-cluster Service name even though it lives in a different subchart. +*/}} + +{{/* Base name: release name, or global.fullnameOverride when set. */}} +{{- define "apip.fullname" -}} +{{- $g := default (dict) .Values.global -}} +{{- if $g.fullnameOverride -}} +{{- $g.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{/* Component full names — fixed suffixes, release-derived, cross-subchart stable. */}} +{{/* Trunc the base first to reserve room for the suffix — otherwise a long + release name is truncated AFTER the suffix is appended, chopping the suffix + and risking cross-component name collisions. */}} +{{- define "apip.platformApi.fullname" -}} +{{- printf "%s-platform-api" (include "apip.fullname" . | trunc 50 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.aiWorkspace.fullname" -}} +{{- printf "%s-ai-workspace" (include "apip.fullname" . | trunc 50 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.developerPortal.fullname" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" . | trunc 45 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "apip.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Render a string-keyed metadata map (labels/annotations) as YAML. Values are +coerced to strings so numbers/bools render quoted. Null values are skipped. +*/}} +{{- define "apip.renderStringMap" -}} +{{- $out := dict -}} +{{- range $k, $v := . -}} +{{- if not (kindIs "invalid" $v) -}} +{{- $_ := set $out $k (toString $v) -}} +{{- end -}} +{{- end -}} +{{- toYaml $out -}} +{{- end -}} + +{{/* Standard labels. `name` = subchart chart name; all components share part-of. */}} +{{- define "apip.labels" -}} +{{- $g := default (dict) .Values.global -}} +{{- $std := dict + "helm.sh/chart" (include "apip.chart" .) + "app.kubernetes.io/name" .Chart.Name + "app.kubernetes.io/managed-by" .Release.Service + "app.kubernetes.io/instance" .Release.Name + "app.kubernetes.io/part-of" "api-platform-portals" + "app.kubernetes.io/version" .Chart.AppVersion -}} +{{- include "apip.renderStringMap" (merge (dict) (default (dict) $g.commonLabels) $std) -}} +{{- end -}} + +{{/* Standard labels + extra (extra wins). Args (list): root, extraLabels|nil */}} +{{- define "apip.resourceLabels" -}} +{{- $root := index . 0 -}} +{{- $extra := default (dict) (index . 1) -}} +{{- $base := fromYaml (include "apip.labels" $root) -}} +{{- include "apip.renderStringMap" (merge (dict) $extra $base) -}} +{{- end -}} + +{{- define "apip.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* Standard + component label + extra (extra > component > commonLabels > std). Args: root, component, extra|nil */}} +{{- define "apip.componentLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{- $extra := default (dict) (index . 2) -}} +{{- $base := fromYaml (include "apip.labels" $root) -}} +{{- include "apip.renderStringMap" (merge (dict) $extra (dict "app.kubernetes.io/component" $component) $base) -}} +{{- end -}} + +{{/* Pod-template labels: selector keys always win. Args: root, component, podLabels|nil */}} +{{- define "apip.componentPodLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{- $podLabels := default (dict) (index . 2) -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $selector := fromYaml (include "apip.componentSelectorLabels" (list $root $component)) -}} +{{- include "apip.renderStringMap" (merge (dict) $selector $podLabels (default (dict) $g.commonLabels)) -}} +{{- end -}} + +{{/* +Merge global.commonAnnotations with per-resource annotations (specific wins). +Emits nothing when both empty. Args (list): root, specificAnnotations|nil +*/}} +{{- define "apip.annotations" -}} +{{- $root := index . 0 -}} +{{- $specific := default (dict) (index . 1) -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $merged := merge (dict) $specific (default (dict) $g.commonAnnotations) -}} +{{- if $merged -}} +{{- include "apip.renderStringMap" $merged -}} +{{- end -}} +{{- end -}} + +{{- define "apip.componentSelectorLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{ include "apip.selectorLabels" $root }} +app.kubernetes.io/component: {{ $component }} +{{- end -}} + +{{/* Shared service account name (one SA per release, created at umbrella level). */}} +{{- define "apip.serviceAccountName" -}} +{{- $g := default (dict) .Values.global -}} +{{- $sa := default (dict) $g.serviceAccount -}} +{{- if $sa.create -}} +{{- default (include "apip.fullname" .) $sa.name -}} +{{- else -}} +{{- default "default" $sa.name -}} +{{- end -}} +{{- end -}} + +{{/* +Default in-cluster URL portals use to reach the shared Platform API. Scheme and +port come from global.platformApi so portals need not read the platform-api +subchart's own values. +*/}} +{{- define "apip.platformApi.internalURL" -}} +{{- $g := default (dict) .Values.global -}} +{{- $pa := default (dict) $g.platformApi -}} +{{- $tlsEnabled := true -}} +{{- if hasKey $pa "tlsEnabled" -}}{{- $tlsEnabled = $pa.tlsEnabled -}}{{- end -}} +{{- $scheme := ternary "https" "http" $tlsEnabled -}} +{{- printf "%s://%s:%d" $scheme (include "apip.platformApi.fullname" .) (int (default 9243 $pa.port)) -}} +{{- end -}} + +{{/* +Render a component image reference, applying the WSO2 subscription registry +rewrite only when global.wso2.subscription.imagePullSecret is set AND the +repository is exactly the chart-canonical default. Explicit overrides pass +through unchanged. Args (dict): root, repository, defaultRepository, tag +*/}} +{{- define "apip.componentImage" -}} +{{- $root := .root -}} +{{- $repo := .repository -}} +{{- $defaultRepo := .defaultRepository -}} +{{- $tag := .tag -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $wso2sub := default (dict) (default (dict) $g.wso2).subscription -}} +{{- $sub := default "" $wso2sub.imagePullSecret -}} +{{- $defaultPrefix := "ghcr.io/wso2/api-platform/" -}} +{{- $wso2Prefix := "registry.wso2.com/wso2-api-platform/" -}} +{{- if and (ne $sub "") (eq $repo $defaultRepo) (hasPrefix $defaultPrefix $repo) -}} +{{- printf "%s%s:%s" $wso2Prefix (trimPrefix $defaultPrefix $repo) $tag -}} +{{- else -}} +{{- printf "%s:%s" $repo $tag -}} +{{- end -}} +{{- end -}} + +{{/* +Render an `imagePullSecrets:` block by merging global.wso2.subscription secret, +global.imagePullSecrets, and component-level pull secrets. Empty string when none. +Args (dict): root, componentPullSecrets +*/}} +{{- define "apip.componentImagePullSecretsBlock" -}} +{{- $root := .root -}} +{{- $componentPullSecrets := default (list) .componentPullSecrets -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $globalPullSecrets := default (list) $g.imagePullSecrets -}} +{{- $wso2sub := default (dict) (default (dict) $g.wso2).subscription -}} +{{- $sub := default "" $wso2sub.imagePullSecret -}} +{{- $subList := ternary (list $sub) (list) (ne $sub "") -}} +{{- $all := concat $subList $globalPullSecrets $componentPullSecrets -}} +{{- if $all -}} +imagePullSecrets: +{{- range $all }} + - name: {{ . }} +{{- end }} +{{- end -}} +{{- end -}} + +{{/* +External-secrets model ("setup generates, startup only checks"). Each helper runs +in its OWN subchart's context, so `.Values.secrets.existingSecret` resolves to +that component's secret. Fails the render when a required secret is unset. +*/}} +{{- define "apip.platformApi.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "platformApi.secrets.existingSecret is required. Run ./generate-secrets.sh to create the Secrets and write values-secrets.yaml, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} + +{{- define "apip.aiWorkspace.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "aiWorkspace.secrets.existingSecret is required when aiWorkspace.config.authMode is \"oidc\". Run ./generate-secrets.sh with AIW_OIDC_CLIENT_SECRET set, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} + +{{- define "apip.developerPortal.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "developerPortal.secrets.existingSecret is required. Run ./generate-secrets.sh to create the Secrets and write values-secrets.yaml, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/certificate.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/certificate.yaml new file mode 100644 index 0000000000..61c3588ff7 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/certificate.yaml @@ -0,0 +1,37 @@ +{{- $ui := .Values -}} +{{- $tls := $ui.tls -}} +{{- if and $ui.deployment.enabled (eq $tls.certificateProvider "cert-manager") $tls.certManager.create }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "apip.developerPortal.fullname" . }}-tls + labels: + {{- include "apip.componentLabels" (list . "developer-portal" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + secretName: {{ include "apip.developerPortal.fullname" . }}-tls + commonName: {{ $tls.certManager.commonName }} + dnsNames: + {{- range $tls.certManager.dnsNames }} + - {{ . | quote }} + {{- end }} + duration: {{ $tls.certManager.duration | quote }} + renewBefore: {{ $tls.certManager.renewBefore | quote }} + issuerRef: + {{- if $tls.certManager.createIssuer }} + name: {{ include "apip.developerPortal.fullname" . }}-selfsigned-issuer + {{- else }} + name: {{ $tls.certManager.issuerRef.name }} + {{- end }} + kind: {{ $tls.certManager.issuerRef.kind }} + {{- if $tls.certManager.issuerRef.group }} + group: {{ $tls.certManager.issuerRef.group }} + {{- end }} + privateKey: + algorithm: RSA + size: 2048 +{{- end }} + diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml new file mode 100644 index 0000000000..0c9afc1ca9 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml @@ -0,0 +1,139 @@ +{{- $dp := .Values -}} +{{- $cfg := $dp.config -}} +{{- $db := $cfg.database -}} +{{- $auth := $cfg.auth -}} +{{- $tls := $dp.tls -}} +{{- $certDir := $tls.mountPath -}} +{{- $baseUrl := default (printf "https://localhost:%d" (int $dp.containerPort)) $cfg.server.baseUrl -}} +{{- $platformApiUrl := default (include "apip.platformApi.internalURL" .) $cfg.platformApi.baseUrl -}} +{{- $tlsEnabled := ne $tls.certificateProvider "none" -}} +{{- if $dp.deployment.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "apip.developerPortal.fullname" . }}-config + labels: + {{- include "apip.componentLabels" (list . "developer-portal" $dp.configMap.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $dp.configMap.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +data: + # Full [developer_portal.*] config, mounted at /app/configs/config.toml to + # replace the image's shipped config. configLoader.js unwraps [developer_portal] + # and ignores anything outside it. Non-secret values are literals from + # values.yaml; secrets are the portal's own {{ "{{ env \"APIP_DP_*\" }}" }} tokens, + # supplied as env vars from the referenced Secret (Secret -> env -> config). + config.toml: | + [developer_portal.server] + port = {{ $dp.containerPort | int }} + base_url = {{ $baseUrl | quote }} + + [developer_portal.server.https] + enabled = {{ $tlsEnabled }} + {{- if $tlsEnabled }} + cert_file = {{ printf "%s/server.crt" $certDir | quote }} + key_file = {{ printf "%s/server.key" $certDir | quote }} + {{- end }} + + [developer_portal.logging] + level = {{ $cfg.logging.level | quote }} + format = {{ $cfg.logging.format | quote }} + console_only = {{ $cfg.logging.consoleOnly }} + + [developer_portal.database] + driver = {{ $db.type | quote }} + {{- if eq $db.type "sqlite" }} + path = {{ $db.file | quote }} + {{- else }} + host = {{ required "config.database.host is required when database.type is \"postgres\"" $db.host | quote }} + port = {{ $db.port | int }} + name = {{ $db.database | quote }} + user = {{ $db.user | quote }} + password = {{ `'{{ env "APIP_DP_DATABASE_PASSWORD" }}'` }} + ssl_mode = {{ $db.sslmode | quote }} + {{- with $db.sslRootCert }} + ssl_root_cert = {{ . | quote }} + {{- end }} + max_open_conns = {{ $db.maxOpenConns | int }} + min_open_conns = {{ $db.minOpenConns | int }} + pool_idle_timeout_ms = {{ $db.poolIdleTimeoutMs | int }} + pool_connection_timeout_ms = {{ $db.poolConnectionTimeoutMs | int }} + pool_request_timeout_ms = {{ $db.poolRequestTimeoutMs | int }} + {{- end }} + + [developer_portal.security] + encryption_key = {{ `'{{ env "APIP_DP_SECURITY_ENCRYPTION_KEY" }}'` }} + session_secret = {{ `'{{ env "APIP_DP_SECURITY_SESSION_SECRET" }}'` }} + + [developer_portal.security.service_api_key] + enabled = {{ $cfg.security.serviceApiKey.enabled }} + header_name = {{ $cfg.security.serviceApiKey.headerName | quote }} + {{- if $dp.secrets.hasServiceApiKeyValue }} + value = {{ `'{{ env "APIP_DP_SECURITY_SERVICE_API_KEY_VALUE" }}'` }} + {{- end }} + + [developer_portal.auth] + mode = {{ ternary "idp" "local" (ne $auth.idp.clientId "") | quote }} + role_validation = {{ $cfg.security.roleValidation }} + + [developer_portal.auth.claim_mappings] + organization = {{ $auth.claimMappings.organization | quote }} + roles = {{ $auth.claimMappings.roles | quote }} + groups = {{ $auth.claimMappings.groups | quote }} + + [developer_portal.auth.local] + platform_api_url = {{ $platformApiUrl | quote }} + public_key_path = {{ $auth.publicKeyPath | quote }} + tls_skip_verify = {{ $cfg.platformApi.insecure }} + + {{- if ne $auth.idp.clientId "" }} + + [developer_portal.auth.idp] + name = {{ $auth.idp.name | quote }} + issuer = {{ $auth.idp.issuer | quote }} + authorization_url = {{ $auth.idp.authorizationUrl | quote }} + token_url = {{ $auth.idp.tokenUrl | quote }} + user_info_url = {{ $auth.idp.userInfoUrl | quote }} + jwks_url = {{ $auth.idp.jwksUrl | quote }} + client_id = {{ $auth.idp.clientId | quote }} + client_secret = {{ `'{{ env "APIP_DP_AUTH_IDP_CLIENT_SECRET" }}'` }} + callback_url = {{ $auth.idp.callbackUrl | quote }} + logout_url = {{ $auth.idp.logoutUrl | quote }} + scope = {{ $auth.idp.scope | quote }} + audience = {{ $auth.idp.audience | quote }} + certificate = {{ $auth.idp.certificate | quote }} + sign_up_url = {{ $auth.idp.signUpUrl | quote }} + logout_redirect_uri = {{ $auth.idp.logoutRedirectUri | quote }} + org_callback = {{ $auth.idp.orgCallback }} + silent_sso = {{ $auth.idp.silentSso }} + token_refresh_timeout_ms = {{ $auth.idp.tokenRefreshTimeoutMs | int }} + + [developer_portal.auth.idp.roles] + admin = {{ $auth.idp.roles.admin | quote }} + subscriber = {{ $auth.idp.roles.subscriber | quote }} + super_admin = {{ $auth.idp.roles.superAdmin | quote }} + {{- end }} + + [developer_portal.organization] + default_name = {{ $cfg.organization.defaultName | quote }} + auto_create_subscription_plans = {{ $cfg.organization.autoCreateSubscriptionPlans }} + + [developer_portal.webhooks.delivery] + poll_interval_ms = {{ $cfg.webhooksDelivery.pollIntervalMs | int }} + batch_size = {{ $cfg.webhooksDelivery.batchSize | int }} + signature_tolerance_sec = {{ $cfg.webhooksDelivery.signatureToleranceSec | int }} + + [developer_portal.tryout] + enabled = {{ $cfg.tryout.enabled }} + allow_http_endpoints = {{ $cfg.tryout.allowHttpEndpoints }} + allow_private_endpoints = {{ $cfg.tryout.allowPrivateEndpoints }} + tls_skip_verify = {{ $cfg.tryout.tlsSkipVerify }} + timeout_ms = {{ $cfg.tryout.timeoutMs | int }} + max_request_bytes = {{ $cfg.tryout.maxRequestBytes | int }} + max_response_bytes = {{ $cfg.tryout.maxResponseBytes | int }} + {{- with $dp.configToml }} + + {{ . | nindent 4 | trim }} + {{- end }} +{{- end }} diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml new file mode 100644 index 0000000000..c2c95bc3b9 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml @@ -0,0 +1,215 @@ +{{- $dp := .Values -}} +{{- $deployment := $dp.deployment -}} +{{- $cfg := $dp.config -}} +{{- $auth := $cfg.auth -}} +{{- $secrets := $dp.secrets -}} +{{- $tls := $dp.tls -}} +{{- $selfSigned := eq $tls.certificateProvider "selfSigned" -}} +{{- $tlsFromSecret := or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret") -}} +{{- $secretName := include "apip.developerPortal.secretName" . -}} +{{- $imageTag := default .Chart.AppVersion $dp.image.tag -}} +{{- $certDir := $tls.mountPath -}} +{{- $jwtKeyDir := regexReplaceAll "/[^/]+$" $auth.publicKeyPath "" -}} +{{- if $deployment.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "apip.developerPortal.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "developer-portal" $deployment.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $deployment.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + replicas: {{ $deployment.replicaCount }} + {{- with $deployment.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "apip.componentSelectorLabels" (list . "developer-portal") | nindent 6 }} + template: + metadata: + labels: + {{- include "apip.componentPodLabels" (list . "developer-portal" $deployment.podLabels) | nindent 8 }} + {{- with (merge (dict) (default (dict) $deployment.podAnnotations) (default (dict) .Values.global.commonAnnotations)) }} + annotations: + {{- include "apip.renderStringMap" . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "apip.serviceAccountName" . }} + {{- with (include "apip.componentImagePullSecretsBlock" (dict "root" . "componentPullSecrets" $dp.imagePullSecrets)) }} + {{- . | nindent 6 }} + {{- end }} + {{- with $deployment.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with $deployment.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if hasKey $deployment "terminationGracePeriodSeconds" }} + terminationGracePeriodSeconds: {{ $deployment.terminationGracePeriodSeconds }} + {{- end }} + {{- with $deployment.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.dnsPolicy }} + dnsPolicy: {{ . }} + {{- end }} + {{- with $deployment.dnsConfig }} + dnsConfig: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if hasKey $deployment "automountServiceAccountToken" }} + automountServiceAccountToken: {{ $deployment.automountServiceAccountToken }} + {{- end }} + containers: + - name: devportal + image: {{ include "apip.componentImage" (dict "root" . "repository" $dp.image.repository "defaultRepository" "ghcr.io/wso2/api-platform/developer-portal" "tag" $imageTag) | quote }} + imagePullPolicy: {{ $dp.image.pullPolicy }} + {{- with $deployment.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + env: + # Config is delivered via the mounted config.toml (see configmap.yaml); + # only secrets are injected here, resolved into that config by its + # {{ "{{ env \"APIP_DP_*\" }}" }} tokens. + - name: APIP_DP_SECURITY_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.encryptionKey }} + - name: APIP_DP_SECURITY_SESSION_SECRET + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.sessionSecret }} + {{- if eq $cfg.database.type "postgres" }} + - name: APIP_DP_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.databasePassword }} + {{- end }} + {{- if $secrets.hasServiceApiKeyValue }} + - name: APIP_DP_SECURITY_SERVICE_API_KEY_VALUE + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.serviceApiKeyValue }} + {{- end }} + {{- if and (ne $auth.idp.clientId "") $secrets.hasIdpClientSecret }} + - name: APIP_DP_AUTH_IDP_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.idpClientSecret }} + {{- end }} + {{- range $deployment.extraEnv }} + - {{- toYaml . | nindent 14 }} + {{- end }} + {{- with $deployment.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ $dp.containerPort }} + protocol: TCP + livenessProbe: + {{- toYaml $deployment.livenessProbe | nindent 12 }} + {{- with $deployment.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + readinessProbe: + {{- toYaml $deployment.readinessProbe | nindent 12 }} + resources: + {{- toYaml ($deployment.resources | default dict) | nindent 12 }} + volumeMounts: + - name: data + mountPath: {{ $deployment.volumeMountPath }} + - name: certs + mountPath: {{ $certDir }} + {{- if $tlsFromSecret }} + readOnly: true + {{- end }} + - name: config + mountPath: /app/configs/config.toml + subPath: config.toml + {{- if $secrets.hasPublicKey }} + - name: platform-api-key + mountPath: {{ $jwtKeyDir }} + readOnly: true + {{- end }} + {{- with $deployment.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + {{- if $dp.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ if $dp.persistence.existingClaim }}{{ $dp.persistence.existingClaim }}{{ else }}{{ include "apip.developerPortal.fullname" . }}-data{{ end }} + {{- else }} + emptyDir: {} + {{- end }} + - name: certs + {{- if $tlsFromSecret }} + secret: + {{- if eq $tls.certificateProvider "cert-manager" }} + secretName: {{ include "apip.developerPortal.fullname" . }}-tls + {{- else }} + secretName: {{ $tls.secret.name }} + {{- end }} + items: + - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.crt{{ else }}{{ $tls.secret.certKey }}{{ end }} + path: server.crt + - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.key{{ else }}{{ $tls.secret.keyKey }}{{ end }} + path: server.key + {{- else }} + # selfSigned: writable dir where the entrypoint generates server.crt/server.key. + emptyDir: {} + {{- end }} + - name: config + configMap: + name: {{ include "apip.developerPortal.fullname" . }}-config + items: + - key: config.toml + path: config.toml + {{- if $secrets.hasPublicKey }} + # Platform API RS256 public key (PEM) mounted for auth.local.public_key_path, + # used to verify Platform API-issued tokens. Tokens are asymmetric — there + # is no shared HMAC secret. + - name: platform-api-key + secret: + secretName: {{ $secretName }} + items: + - key: {{ $secrets.keys.publicKey }} + path: {{ base $auth.publicKeyPath }} + {{- end }} + {{- with $deployment.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/hpa.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/hpa.yaml new file mode 100644 index 0000000000..6bf3e9c735 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/hpa.yaml @@ -0,0 +1,51 @@ +{{- $dp := .Values -}} +{{- $hpa := $dp.hpa -}} +{{- if and $dp.deployment.enabled $hpa.enabled }} +{{- if ne $dp.config.database.type "postgres" }} +{{- fail "developer-portal.hpa requires developer-portal.config.database.type=postgres — SQLite does not support multiple replicas." }} +{{- end }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "apip.developerPortal.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "developer-portal" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "apip.developerPortal.fullname" . }} + minReplicas: {{ $hpa.minReplicas }} + maxReplicas: {{ $hpa.maxReplicas }} + {{- if or $hpa.targetCPUUtilizationPercentage $hpa.targetMemoryUtilizationPercentage $hpa.customMetrics }} + metrics: + {{- if $hpa.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $hpa.targetCPUUtilizationPercentage }} + {{- end }} + {{- if $hpa.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $hpa.targetMemoryUtilizationPercentage }} + {{- end }} + {{- with $hpa.customMetrics }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with $hpa.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/issuer.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/issuer.yaml new file mode 100644 index 0000000000..5341926580 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/issuer.yaml @@ -0,0 +1,22 @@ +{{- $ui := .Values -}} +{{- $tls := $ui.tls -}} +{{- if and $ui.deployment.enabled (eq $tls.certificateProvider "cert-manager") }} +{{- if and (eq $tls.certManager.issuerRef.kind "Issuer") $tls.certManager.createIssuer }} +--- +# Self-signed Issuer for development/testing. In production replace this with a +# proper CA issuer (Let's Encrypt, corporate CA, etc.) via createIssuer=false. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "apip.developerPortal.fullname" . }}-selfsigned-issuer + labels: + {{- include "apip.componentLabels" (list . "developer-portal" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + selfSigned: {} +{{- end }} +{{- end }} + diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/pdb.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/pdb.yaml new file mode 100644 index 0000000000..1fc411ab06 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/pdb.yaml @@ -0,0 +1,30 @@ +{{- $dp := .Values -}} +{{- $pdb := $dp.podDisruptionBudget -}} +{{- if and $dp.deployment.enabled $pdb.enabled }} +{{- if and $pdb.minAvailable $pdb.maxUnavailable }} +{{- fail "developer-portal.podDisruptionBudget: only one of minAvailable or maxUnavailable may be set" }} +{{- end }} +{{- if not (or $pdb.minAvailable $pdb.maxUnavailable) }} +{{- fail "developer-portal.podDisruptionBudget: one of minAvailable or maxUnavailable must be set" }} +{{- end }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "apip.developerPortal.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "developer-portal" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "apip.componentSelectorLabels" (list . "developer-portal") | nindent 6 }} + {{- if $pdb.minAvailable }} + minAvailable: {{ $pdb.minAvailable }} + {{- else if $pdb.maxUnavailable }} + maxUnavailable: {{ $pdb.maxUnavailable }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/pvc.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/pvc.yaml new file mode 100644 index 0000000000..dbff9105d2 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/pvc.yaml @@ -0,0 +1,26 @@ +{{- $dp := .Values -}} +{{- $persistence := $dp.persistence -}} +{{- if and $dp.deployment.enabled $persistence.enabled (not $persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "apip.developerPortal.fullname" . }}-data + labels: + {{- include "apip.componentLabels" (list . "developer-portal" $persistence.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $persistence.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- range $persistence.accessModes }} + - {{ . }} + {{- end }} + resources: + requests: + storage: {{ $persistence.size }} + {{- if $persistence.storageClass }} + storageClassName: {{ $persistence.storageClass | quote }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/service.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/service.yaml new file mode 100644 index 0000000000..dedf085fc3 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/service.yaml @@ -0,0 +1,50 @@ +{{- $ui := .Values -}} +{{- $service := $ui.service -}} +{{- $isNodePortCapable := or (eq $service.type "NodePort") (eq $service.type "LoadBalancer") -}} +{{- if $ui.deployment.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "apip.developerPortal.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "developer-portal" $service.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $service.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + type: {{ $service.type }} + {{- with $service.clusterIP }} + clusterIP: {{ . }} + {{- end }} + {{- if and $service.externalTrafficPolicy $isNodePortCapable }} + externalTrafficPolicy: {{ $service.externalTrafficPolicy }} + {{- end }} + {{- if eq $service.type "LoadBalancer" }} + {{- with $service.loadBalancerClass }} + loadBalancerClass: {{ . }} + {{- end }} + {{- with $service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with $service.ipFamilyPolicy }} + ipFamilyPolicy: {{ . }} + {{- end }} + {{- with $service.ipFamilies }} + ipFamilies: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + {{- include "apip.componentSelectorLabels" (list . "developer-portal") | nindent 4 }} + ports: + - name: http + port: {{ $service.port }} + targetPort: http + protocol: TCP + {{- if and $isNodePortCapable $service.nodePort }} + nodePort: {{ $service.nodePort }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml new file mode 100644 index 0000000000..77a9409fa5 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml @@ -0,0 +1,20 @@ +# Local development values for the developer-portal-ui chart. +# Usage (standalone, from kubernetes/helm/): +# helm install developer-portal-ui ./developer-portal-ui-helm-chart \ +# -f developer-portal-ui-helm-chart/values.yaml -f developer-portal-ui-helm-chart/values-local.yaml +# +# Overrides the defaults for local development: +# - locally-built image (latest tag, IfNotPresent pull policy) +# - debug logging; skip TLS verification to the Platform API's self-signed cert +image: + tag: "latest" + pullPolicy: IfNotPresent + +config: + logging: + level: debug + platformApi: + insecure: true + security: + # Relax role checks for local dev only (secure default is true). + roleValidation: false diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml new file mode 100644 index 0000000000..dc24b24fed --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml @@ -0,0 +1,340 @@ +# ============================================================================ +# developer-portal — component chart values +# ============================================================================ +# The Developer Portal (Node.js app). It has its OWN database (separate from the +# Platform API's) and authenticates users against the Platform API. Released on +# its own; used as an umbrella subchart or standalone. +# +# global.* Cross-cutting settings. Under an umbrella these come from the +# umbrella; the block below is the STANDALONE default, overridden +# when nested. +# (rest) This component's own settings. Structured config here is rendered +# into APIP_DP_* environment variables on the container. +# +# `enabled` is read by the umbrella's dependency condition (ignored standalone). + +# ---------------------------------------------------------------------------- +# global — STANDALONE defaults only (an umbrella's global.* overrides these) +# ---------------------------------------------------------------------------- +global: + nameOverride: "" + fullnameOverride: "" + commonLabels: {} + commonAnnotations: {} + imagePullSecrets: [] + serviceAccount: + # Standalone default is `false` → pods use the namespace "default" SA (this + # component ships no ServiceAccount template; the umbrella creates one). + create: false + name: "" + annotations: {} + automountServiceAccountToken: true + wso2: + subscription: + imagePullSecret: "" + platformApi: + port: 9243 + tlsEnabled: true + +# ---------------------------------------------------------------------------- +# Component toggle + container image +# ---------------------------------------------------------------------------- +# Install the Developer Portal. Opt in per install (the umbrella sets this). +enabled: false +image: + repository: ghcr.io/wso2/api-platform/developer-portal + tag: "1.0.0-alpha2" + pullPolicy: IfNotPresent +# Pull secrets for THIS component only (merged with global.imagePullSecrets). +imagePullSecrets: [] + +# ---------------------------------------------------------------------------- +# config — rendered into /app/configs/config.toml under [developer_portal.*] +# (portals/developer-portal/configs/config-template.toml). Non-secret values are +# literals; secrets come from the referenced Secret via APIP_DP_* env tokens. +# ---------------------------------------------------------------------------- +config: + server: + # Public URL browsers use to reach the portal (scheme follows the TLS setting). + # Leave "" to derive it from the service + TLS; set it when fronting with a + # real hostname so login redirects and asset URLs are correct. + baseUrl: "" + logging: + level: info # debug | info | warn | error + format: text # text | json + consoleOnly: true # log to stdout only (no file) + + # --- Database (the portal's OWN store, separate from the Platform API's) --- + database: + # sqlite → file on the PVC; single replica; HPA unavailable. + # postgres → external server (configured below); enables multi-replica + HPA. + type: sqlite + # SQLite file (type=sqlite). Must sit under the mounted data volume. + file: /app/data/devportal.db + # PostgreSQL connection (type=postgres only). The password is NOT here — it + # comes from the Secret key secrets.keys.databasePassword. Leave host empty + # for sqlite. + host: "" + port: 5432 + database: devportal # database (schema) name → rendered as `name` + user: postgres # login role; password from the Secret + sslmode: disable # disable | require | verify-ca | verify-full + sslRootCert: "" # CA cert — used by verify-ca / verify-full + # Connection-pool tuning (postgres only). + maxOpenConns: 50 + minOpenConns: 2 + poolIdleTimeoutMs: 10000 + poolConnectionTimeoutMs: 30000 + poolRequestTimeoutMs: 30000 + + # --- Security --- + security: + # Enforce per-operation role checks on incoming tokens. Secure by default; + # override to false locally (values-local.yaml) only if a deployment + # genuinely cannot supply role claims yet. + roleValidation: true + # A shared API key some server-to-server callers present in a header. + serviceApiKey: + enabled: true + headerName: x-wso2-api-key + # The key value itself lives in the Secret (secrets.keys.serviceApiKeyValue), + # wired only when secrets.hasServiceApiKeyValue is true. + + # --- How the portal reaches the Platform API --- + platformApi: + # Leave "" to derive the in-cluster Platform API URL automatically. Set to + # target an external Platform API. + baseUrl: "" + # Skip TLS verification on the portal → Platform API hop. Secure by default; + # enable only for local dev against a self-signed Platform API cert + # (values-local.yaml sets this true). In production supply a real/trusted cert. + insecure: false + + # --- Authentication --- + auth: + # Filesystem path where the Platform API's RS256 public key PEM is mounted; + # used (auth.local mode) to verify Platform API-issued tokens. The key is + # supplied via the Secret (secrets.keys.publicKey) when secrets.hasPublicKey + # is true, and mounted at this path. + publicKeyPath: /etc/devportal/keys/jwt_public.pem + # JWT claim-name mappings — which token claim carries each field. + claimMappings: + organization: org_name # claim carrying the org ID + roles: roles # claim carrying the user's roles + groups: groups + # OIDC login (optional). Rendered as auth.mode=idp when idp.clientId is set; + # otherwise the portal uses local (Platform API) login. The client secret + # comes from the Secret (secrets.keys.idpClientSecret), wired only when + # secrets.hasIdpClientSecret=true. + idp: + name: "" + issuer: "" + authorizationUrl: "" + tokenUrl: "" + userInfoUrl: "" + jwksUrl: "" + clientId: "" + callbackUrl: "" + logoutUrl: "" + scope: "openid profile email" + audience: "" + certificate: "" # PEM to pin the IDP's signing cert (optional) + signUpUrl: "" + logoutRedirectUri: "" + orgCallback: false # redirect to the org's own landing page after login + silentSso: true + tokenRefreshTimeoutMs: 10000 + # Map IDP role names to the portal's internal roles (idp mode). + roles: + admin: admin + subscriber: Internal/subscriber + superAdmin: superAdmin + + organization: + defaultName: default # default organization name; "" disables auto-seeding + autoCreateSubscriptionPlans: true # auto-create Bronze/Silver/Gold/Unlimited/AsyncUnlimited + + # --- Webhook delivery tuning (global) --- + webhooksDelivery: + pollIntervalMs: 2000 + batchSize: 50 + signatureToleranceSec: 300 + + # --- "Try It" console (calls user-supplied endpoints; SSRF-sensitive) --- + tryout: + enabled: true + # false → only https:// endpoints may be called. + allowHttpEndpoints: true + # Deny-by-default: allow the portal to call private/internal addresses only + # when the gateway legitimately sits on a private address, after confirming + # only intended services are reachable. Link-local/cloud-metadata addresses + # are refused regardless. + allowPrivateEndpoints: false + tlsSkipVerify: false # development only + timeoutMs: 15000 + maxRequestBytes: 1048576 # 1 MiB + maxResponseBytes: 5242880 # 5 MiB + +# Escape hatch: raw config.toml appended verbatim (env vars still override it). +configToml: "" + +# ---------------------------------------------------------------------------- +# secrets — reference to a pre-created Kubernetes Secret (REQUIRED) +# ---------------------------------------------------------------------------- +# The chart never creates or embeds secret values. Point existingSecret at a +# Secret made by ./generate-secrets.sh. Render FAILS if it's unset. Required keys: +# ENCRYPTION_KEY and SESSION_SECRET (both 64-char hex; the portal fails closed at +# startup without them). Optional keys — DB password (postgres), OIDC client +# secret, service API key value, and the Platform API RS256 public key PEM (to +# verify Platform API-issued tokens; tokens are asymmetric, so there is no shared +# HMAC secret) — are wired only when the matching has* flag below is true. +# `keys` maps each logical name to the key inside that Secret. +secrets: + existingSecret: "" + keys: + encryptionKey: ENCRYPTION_KEY # → APIP_DP_SECURITY_ENCRYPTION_KEY + sessionSecret: SESSION_SECRET # → APIP_DP_SECURITY_SESSION_SECRET + databasePassword: DATABASE_PASSWORD # → APIP_DP_DATABASE_PASSWORD (postgres) + idpClientSecret: IDP_CLIENT_SECRET # → APIP_DP_AUTH_IDP_CLIENT_SECRET (idp mode) + serviceApiKeyValue: SERVICE_API_KEY_VALUE # → APIP_DP_SECURITY_SERVICE_API_KEY_VALUE + publicKey: jwt_public.pem # mounted file → config.auth.publicKeyPath + # Flip to true ONLY if the referenced Secret actually carries that optional key, + # so the deployment wires it. generate-secrets.sh sets these for you in the + # values-secrets.yaml it writes. + hasPublicKey: true + hasIdpClientSecret: false + hasServiceApiKeyValue: false + +# ---------------------------------------------------------------------------- +# service — how the portal is exposed +# ---------------------------------------------------------------------------- +service: + type: LoadBalancer # LoadBalancer | NodePort | ClusterIP + annotations: {} + labels: {} + port: 3000 + clusterIP: "" + externalTrafficPolicy: "" + loadBalancerClass: "" + loadBalancerSourceRanges: [] + ipFamilyPolicy: "" + ipFamilies: [] + nodePort: "" + +# Port the portal process listens on. +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 + # Where certs are mounted / read from (matches APIP_DP_TLS_CERTFILE etc.). + mountPath: /app/certs + certManager: + create: true + createIssuer: true + issuerRef: + name: selfsigned-issuer + kind: Issuer + commonName: devportal.localhost + dnsNames: + - devportal.localhost + - "*.localhost" + duration: 2160h + renewBefore: 720h + secret: + name: devportal-tls # used when certificateProvider=secret + certKey: tls.crt + keyKey: tls.key + +# ---------------------------------------------------------------------------- +# persistence — the PVC backing SQLite +# ---------------------------------------------------------------------------- +persistence: + enabled: true # required for database.type=sqlite + existingClaim: "" # reuse a PVC you created yourself instead + accessModes: + - ReadWriteOnce + size: 1Gi + storageClass: "" # "" = cluster default StorageClass + labels: {} + # Annotations for the PVC ONLY (rendered only when persistence.enabled). The + # resource-policy=keep annotation makes the PVC — and its data — SURVIVE + # `helm uninstall`; a later `helm install` with the SAME release name re-adopts + # it. To intentionally wipe it: + # kubectl delete pvc -developer-portal-data + # Set to {} if you'd rather the PVC be deleted on uninstall. + annotations: + helm.sh/resource-policy: keep + +# ---------------------------------------------------------------------------- +# deployment — the pod/Deployment spec +# ---------------------------------------------------------------------------- +deployment: + enabled: true + replicaCount: 1 # >1 requires database.type=postgres + volumeMountPath: /app/data + extraEnv: [] + extraEnvFrom: [] + extraVolumeMounts: [] + extraVolumes: [] + labels: {} + annotations: {} + podAnnotations: {} + podLabels: {} + priorityClassName: "" + livenessProbe: + httpGet: { path: /, port: http, scheme: HTTPS } + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: { path: /, port: http, scheme: HTTPS } + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + # Set CPU requests if you enable the HPA, so it can compute utilization. + resources: {} + podSecurityContext: {} + securityContext: {} + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + strategy: {} + hostAliases: [] + dnsPolicy: "" + dnsConfig: {} + +# ---------------------------------------------------------------------------- +# Autoscaling & disruption budget +# ---------------------------------------------------------------------------- +# HorizontalPodAutoscaler. REQUIRES config.database.type=postgres — on sqlite the +# chart refuses to render it (SQLite is single-replica). Enable only after +# switching to postgres, and set deployment.resources.requests.cpu. +hpa: + enabled: false + minReplicas: 2 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 # 0 or "" to disable CPU-based scaling + targetMemoryUtilizationPercentage: "" # set a number to also scale on memory + customMetrics: [] # extra autoscaling/v2 MetricSpec entries + behavior: {} # autoscaling/v2 scaleUp/scaleDown tuning + +# PodDisruptionBudget. Only meaningful at replicaCount >= 2 (with one replica +# minAvailable: 1 blocks node drains). Set exactly ONE of the two below. +podDisruptionBudget: + enabled: false + minAvailable: 1 # integer or percentage string, e.g. "50%" + maxUnavailable: "" # integer or percentage string, e.g. "25%" + +# Extra metadata for the generated ConfigMap. +configMap: + annotations: {} + labels: {} diff --git a/kubernetes/helm/platform-api-helm-chart/.helmignore b/kubernetes/helm/platform-api-helm-chart/.helmignore new file mode 100644 index 0000000000..4620fd7ee3 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/.helmignore @@ -0,0 +1,16 @@ +# 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 diff --git a/kubernetes/helm/platform-api-helm-chart/Chart.yaml b/kubernetes/helm/platform-api-helm-chart/Chart.yaml new file mode 100644 index 0000000000..34cb9e6aa3 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +name: platform-api +description: >- + WSO2 API Platform — Platform API control plane. Independently + released component chart; consumed by the product charts + (ai-workspace, developer-portal) and installable standalone. +type: application +version: 0.13.0 +appVersion: "0.13.0" +home: https://github.com/wso2/api-platform +sources: + - https://github.com/wso2/api-platform diff --git a/kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl b/kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl new file mode 100644 index 0000000000..0dbb30bf87 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl @@ -0,0 +1,221 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Shared helpers for the api-platform-portals suite. + +Cross-cutting configuration (labels/annotations, image pull secrets, service +account, subscription registry, and the shared Platform API service coordinates) +is read from `.Values.global.*` so every component subchart resolves it +identically. Component-specific config is read from the subchart's own `.Values` +by the calling templates, not here. + +Component resource names are derived from the release name with a fixed suffix +(NOT from .Chart.Name), so a portal subchart can compute the Platform API's +in-cluster Service name even though it lives in a different subchart. +*/}} + +{{/* Base name: release name, or global.fullnameOverride when set. */}} +{{- define "apip.fullname" -}} +{{- $g := default (dict) .Values.global -}} +{{- if $g.fullnameOverride -}} +{{- $g.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{/* Component full names — fixed suffixes, release-derived, cross-subchart stable. */}} +{{/* Trunc the base first to reserve room for the suffix — otherwise a long + release name is truncated AFTER the suffix is appended, chopping the suffix + and risking cross-component name collisions. */}} +{{- define "apip.platformApi.fullname" -}} +{{- printf "%s-platform-api" (include "apip.fullname" . | trunc 50 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.aiWorkspace.fullname" -}} +{{- printf "%s-ai-workspace" (include "apip.fullname" . | trunc 50 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.developerPortal.fullname" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" . | trunc 45 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "apip.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Render a string-keyed metadata map (labels/annotations) as YAML. Values are +coerced to strings so numbers/bools render quoted. Null values are skipped. +*/}} +{{- define "apip.renderStringMap" -}} +{{- $out := dict -}} +{{- range $k, $v := . -}} +{{- if not (kindIs "invalid" $v) -}} +{{- $_ := set $out $k (toString $v) -}} +{{- end -}} +{{- end -}} +{{- toYaml $out -}} +{{- end -}} + +{{/* Standard labels. `name` = subchart chart name; all components share part-of. */}} +{{- define "apip.labels" -}} +{{- $g := default (dict) .Values.global -}} +{{- $std := dict + "helm.sh/chart" (include "apip.chart" .) + "app.kubernetes.io/name" .Chart.Name + "app.kubernetes.io/managed-by" .Release.Service + "app.kubernetes.io/instance" .Release.Name + "app.kubernetes.io/part-of" "api-platform-portals" + "app.kubernetes.io/version" .Chart.AppVersion -}} +{{- include "apip.renderStringMap" (merge (dict) (default (dict) $g.commonLabels) $std) -}} +{{- end -}} + +{{/* Standard labels + extra (extra wins). Args (list): root, extraLabels|nil */}} +{{- define "apip.resourceLabels" -}} +{{- $root := index . 0 -}} +{{- $extra := default (dict) (index . 1) -}} +{{- $base := fromYaml (include "apip.labels" $root) -}} +{{- include "apip.renderStringMap" (merge (dict) $extra $base) -}} +{{- end -}} + +{{- define "apip.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* Standard + component label + extra (extra > component > commonLabels > std). Args: root, component, extra|nil */}} +{{- define "apip.componentLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{- $extra := default (dict) (index . 2) -}} +{{- $base := fromYaml (include "apip.labels" $root) -}} +{{- include "apip.renderStringMap" (merge (dict) $extra (dict "app.kubernetes.io/component" $component) $base) -}} +{{- end -}} + +{{/* Pod-template labels: selector keys always win. Args: root, component, podLabels|nil */}} +{{- define "apip.componentPodLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{- $podLabels := default (dict) (index . 2) -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $selector := fromYaml (include "apip.componentSelectorLabels" (list $root $component)) -}} +{{- include "apip.renderStringMap" (merge (dict) $selector $podLabels (default (dict) $g.commonLabels)) -}} +{{- end -}} + +{{/* +Merge global.commonAnnotations with per-resource annotations (specific wins). +Emits nothing when both empty. Args (list): root, specificAnnotations|nil +*/}} +{{- define "apip.annotations" -}} +{{- $root := index . 0 -}} +{{- $specific := default (dict) (index . 1) -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $merged := merge (dict) $specific (default (dict) $g.commonAnnotations) -}} +{{- if $merged -}} +{{- include "apip.renderStringMap" $merged -}} +{{- end -}} +{{- end -}} + +{{- define "apip.componentSelectorLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{ include "apip.selectorLabels" $root }} +app.kubernetes.io/component: {{ $component }} +{{- end -}} + +{{/* Shared service account name (one SA per release, created at umbrella level). */}} +{{- define "apip.serviceAccountName" -}} +{{- $g := default (dict) .Values.global -}} +{{- $sa := default (dict) $g.serviceAccount -}} +{{- if $sa.create -}} +{{- default (include "apip.fullname" .) $sa.name -}} +{{- else -}} +{{- default "default" $sa.name -}} +{{- end -}} +{{- end -}} + +{{/* +Default in-cluster URL portals use to reach the shared Platform API. Scheme and +port come from global.platformApi so portals need not read the platform-api +subchart's own values. +*/}} +{{- define "apip.platformApi.internalURL" -}} +{{- $g := default (dict) .Values.global -}} +{{- $pa := default (dict) $g.platformApi -}} +{{- $tlsEnabled := true -}} +{{- if hasKey $pa "tlsEnabled" -}}{{- $tlsEnabled = $pa.tlsEnabled -}}{{- end -}} +{{- $scheme := ternary "https" "http" $tlsEnabled -}} +{{- printf "%s://%s:%d" $scheme (include "apip.platformApi.fullname" .) (int (default 9243 $pa.port)) -}} +{{- end -}} + +{{/* +Render a component image reference, applying the WSO2 subscription registry +rewrite only when global.wso2.subscription.imagePullSecret is set AND the +repository is exactly the chart-canonical default. Explicit overrides pass +through unchanged. Args (dict): root, repository, defaultRepository, tag +*/}} +{{- define "apip.componentImage" -}} +{{- $root := .root -}} +{{- $repo := .repository -}} +{{- $defaultRepo := .defaultRepository -}} +{{- $tag := .tag -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $wso2sub := default (dict) (default (dict) $g.wso2).subscription -}} +{{- $sub := default "" $wso2sub.imagePullSecret -}} +{{- $defaultPrefix := "ghcr.io/wso2/api-platform/" -}} +{{- $wso2Prefix := "registry.wso2.com/wso2-api-platform/" -}} +{{- if and (ne $sub "") (eq $repo $defaultRepo) (hasPrefix $defaultPrefix $repo) -}} +{{- printf "%s%s:%s" $wso2Prefix (trimPrefix $defaultPrefix $repo) $tag -}} +{{- else -}} +{{- printf "%s:%s" $repo $tag -}} +{{- end -}} +{{- end -}} + +{{/* +Render an `imagePullSecrets:` block by merging global.wso2.subscription secret, +global.imagePullSecrets, and component-level pull secrets. Empty string when none. +Args (dict): root, componentPullSecrets +*/}} +{{- define "apip.componentImagePullSecretsBlock" -}} +{{- $root := .root -}} +{{- $componentPullSecrets := default (list) .componentPullSecrets -}} +{{- $g := default (dict) $root.Values.global -}} +{{- $globalPullSecrets := default (list) $g.imagePullSecrets -}} +{{- $wso2sub := default (dict) (default (dict) $g.wso2).subscription -}} +{{- $sub := default "" $wso2sub.imagePullSecret -}} +{{- $subList := ternary (list $sub) (list) (ne $sub "") -}} +{{- $all := concat $subList $globalPullSecrets $componentPullSecrets -}} +{{- if $all -}} +imagePullSecrets: +{{- range $all }} + - name: {{ . }} +{{- end }} +{{- end -}} +{{- end -}} + +{{/* +External-secrets model ("setup generates, startup only checks"). Each helper runs +in its OWN subchart's context, so `.Values.secrets.existingSecret` resolves to +that component's secret. Fails the render when a required secret is unset. +*/}} +{{- define "apip.platformApi.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "platformApi.secrets.existingSecret is required. Run ./generate-secrets.sh to create the Secrets and write values-secrets.yaml, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} + +{{- define "apip.aiWorkspace.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "aiWorkspace.secrets.existingSecret is required when aiWorkspace.config.authMode is \"oidc\". Run ./generate-secrets.sh with AIW_OIDC_CLIENT_SECRET set, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} + +{{- define "apip.developerPortal.secretName" -}} +{{- $name := .Values.secrets.existingSecret -}} +{{- if not $name -}} +{{- fail "developerPortal.secrets.existingSecret is required. Run ./generate-secrets.sh to create the Secrets and write values-secrets.yaml, then install with -f values-secrets.yaml." -}} +{{- end -}} +{{- $name -}} +{{- end -}} diff --git a/kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml b/kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml new file mode 100644 index 0000000000..ed3fd6e78a --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml @@ -0,0 +1,42 @@ +{{- $pa := .Values -}} +{{- $tls := $pa.tls -}} +{{- if and $pa.deployment.enabled $pa.config.server.https.enabled (eq $tls.certificateProvider "cert-manager") }} +{{- if and $tls.certManager.createIssuer (ne $tls.certManager.issuerRef.kind "Issuer") }} +{{- fail "platform-api: tls.certManager.createIssuer=true only creates a namespaced Issuer, but issuerRef.kind is not \"Issuer\". Set issuerRef.kind: Issuer, or set createIssuer=false and reference your own ClusterIssuer." }} +{{- end }} +{{- end }} +{{- if and $pa.deployment.enabled $pa.config.server.https.enabled (eq $tls.certificateProvider "cert-manager") $tls.certManager.create }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "apip.platformApi.fullname" . }}-tls + labels: + {{- include "apip.componentLabels" (list . "platform-api" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + secretName: {{ include "apip.platformApi.fullname" . }}-tls + commonName: {{ $tls.certManager.commonName }} + dnsNames: + {{- range $tls.certManager.dnsNames }} + - {{ . | quote }} + {{- end }} + duration: {{ $tls.certManager.duration | quote }} + renewBefore: {{ $tls.certManager.renewBefore | quote }} + issuerRef: + {{- if $tls.certManager.createIssuer }} + name: {{ include "apip.platformApi.fullname" . }}-selfsigned-issuer + {{- else }} + name: {{ $tls.certManager.issuerRef.name }} + {{- end }} + kind: {{ $tls.certManager.issuerRef.kind }} + {{- if $tls.certManager.issuerRef.group }} + group: {{ $tls.certManager.issuerRef.group }} + {{- end }} + privateKey: + algorithm: RSA + size: 2048 +{{- end }} + diff --git a/kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml b/kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml new file mode 100644 index 0000000000..5e704fafe8 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml @@ -0,0 +1,175 @@ +{{- $pa := .Values -}} +{{- $cfg := $pa.config -}} +{{- $db := $cfg.database -}} +{{- $auth := $cfg.auth -}} +{{- $srv := $cfg.server -}} +{{- $https := $srv.https -}} +{{- if $pa.deployment.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "apip.platformApi.fullname" . }}-config + labels: + {{- include "apip.componentLabels" (list . "platform-api" $pa.configMap.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $pa.configMap.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +data: + # Rendered under the [platform_api] table the binary unmarshals (config.go). + # Non-secret values are literals from values.yaml; secrets are emitted as the + # Platform API's own {{ "{{ env \"APIP_CP_*\" }}" }} interpolation tokens and + # supplied to the container as env vars from the referenced Secret — the secret + # value flows Secret -> env -> config interpolation and never lands here. + config-platform-api.toml: | + [platform_api] + db_schema_path = {{ $cfg.resources.dbSchemaPath | quote }} + openapi_spec_path = {{ $cfg.resources.openapiSpecPath | quote }} + llm_template_definitions_path = {{ $cfg.resources.llmTemplateDefinitionsPath | quote }} + openapi_spec_max_fetch_bytes = {{ $cfg.resources.openapiSpecMaxFetchBytes | int }} + + [platform_api.logging] + level = {{ $cfg.logging.level | quote }} + format = {{ $cfg.logging.format | quote }} + + [platform_api.security] + encryption_key = {{ `'{{ env "APIP_CP_ENCRYPTION_KEY" }}'` }} + + [platform_api.security.api_key] + hashing_algorithms = {{ $cfg.security.apiKey.hashingAlgorithms | quote }} + + [platform_api.database] + driver = {{ $db.driver | quote }} + {{- if eq $db.driver "sqlite3" }} + path = {{ $db.path | quote }} + {{- else }} + host = {{ required "config.database.postgres.host is required when database.driver is not sqlite3" $db.postgres.host | quote }} + port = {{ $db.postgres.port | int }} + name = {{ $db.postgres.name | quote }} + user = {{ $db.postgres.user | quote }} + password = {{ `'{{ env "APIP_CP_DATABASE_PASSWORD" }}'` }} + ssl_mode = {{ $db.postgres.sslMode | quote }} + {{- with $db.postgres.sslRootCert }} + ssl_root_cert = {{ . | quote }} + {{- end }} + {{- with $db.postgres.sslCert }} + ssl_cert = {{ . | quote }} + {{- end }} + {{- with $db.postgres.sslKey }} + ssl_key = {{ . | quote }} + {{- end }} + max_open_conns = {{ $db.postgres.maxOpenConns | int }} + max_idle_conns = {{ $db.postgres.maxIdleConns | int }} + conn_max_lifetime = {{ $db.postgres.connMaxLifetime | int }} + {{- end }} + + [platform_api.auth] + mode = {{ $auth.mode | quote }} + scope_validation = {{ $auth.scopeValidation }} + {{- with $auth.skipPaths }} + skip_paths = {{ toJson . }} + {{- end }} + + [platform_api.auth.claim_mappings] + organization = {{ $auth.claimMappings.organization | quote }} + org_name = {{ $auth.claimMappings.orgName | quote }} + org_handle = {{ $auth.claimMappings.orgHandle | quote }} + user_id = {{ $auth.claimMappings.userId | quote }} + username = {{ $auth.claimMappings.username | quote }} + email = {{ $auth.claimMappings.email | quote }} + scope = {{ $auth.claimMappings.scope | quote }} + roles = {{ $auth.claimMappings.roles | quote }} + + [platform_api.auth.jwt] + issuer = {{ $auth.jwt.issuer | quote }} + public_key_file = {{ $auth.jwt.publicKeyFile | quote }} + private_key_file = {{ $auth.jwt.privateKeyFile | quote }} + token_ttl = {{ $auth.jwt.tokenTtl | quote }} + + {{- if eq $auth.mode "idp" }} + + [platform_api.auth.idp] + name = {{ $auth.idp.name | quote }} + jwks_url = {{ required "config.auth.idp.jwksUrl is required when auth.mode is \"idp\"" $auth.idp.jwksUrl | quote }} + issuer = {{ toJson $auth.idp.issuer }} + audience = {{ toJson $auth.idp.audience }} + validation_mode = {{ $auth.idp.validationMode | quote }} + role_mappings = {{ $auth.idp.roleMappings | quote }} + {{- end }} + + {{- if eq $auth.mode "file" }} + + [platform_api.auth.file.organization] + id = {{ $auth.file.organization.id | quote }} + display_name = {{ $auth.file.organization.displayName | quote }} + region = {{ $auth.file.organization.region | quote }} + uuid = {{ $auth.file.organization.uuid | quote }} + + [[platform_api.auth.file.users]] + # username/password_hash come from the Secret via env interpolation — never + # a baked-in default. The 1-arg {{ "{{ env }}" }} form fails closed if the env + # var is unset, so there is no admin/admin fallback. generate-secrets.sh + # provisions a generated username and a bcrypt password hash. + username = {{ `'{{ env "APIP_CP_ADMIN_USERNAME" }}'` }} + password_hash = {{ `'{{ env "APIP_CP_ADMIN_PASSWORD_HASH" }}'` }} + scopes = {{ $auth.file.admin.scopes | quote }} + {{- end }} + + [platform_api.server.http] + enabled = {{ $srv.http.enabled }} + port = {{ $srv.http.port | int }} + + [platform_api.server.https] + enabled = {{ $https.enabled }} + port = {{ $pa.containerPort | int }} + cert_file = {{ printf "%s/cert.pem" $https.certDir | quote }} + key_file = {{ printf "%s/key.pem" $https.certDir | quote }} + + [platform_api.server.timeouts] + read_header = {{ $srv.timeouts.readHeader | quote }} + read = {{ $srv.timeouts.read | quote }} + write = {{ $srv.timeouts.write | quote }} + idle = {{ $srv.timeouts.idle | quote }} + + [platform_api.server.cors] + allowed_origins = {{ toJson $srv.cors.allowedOrigins }} + + [platform_api.server.websocket] + max_connections = {{ $srv.websocket.maxConnections | int }} + connection_timeout = {{ $srv.websocket.connectionTimeout | int }} + rate_limit_per_min = {{ $srv.websocket.rateLimitPerMin | int }} + metrics_log_enabled = {{ $srv.websocket.metricsLogEnabled }} + metrics_log_interval = {{ $srv.websocket.metricsLogInterval | int }} + + [platform_api.gateway] + enable_version_verification = {{ $cfg.gateway.enableVersionVerification }} + enable_functionality_type_verification = {{ $cfg.gateway.enableFunctionalityTypeVerification }} + + [platform_api.deployments] + max_per_api_gateway = {{ $cfg.deployments.maxPerApiGateway | int }} + transitional_status_enabled = {{ $cfg.deployments.transitionalStatusEnabled }} + timeout_enabled = {{ $cfg.deployments.timeoutEnabled }} + timeout_interval = {{ $cfg.deployments.timeoutInterval | int }} + timeout_duration = {{ $cfg.deployments.timeoutDuration | int }} + + [platform_api.event_hub] + poll_interval = {{ $cfg.eventHub.pollInterval | quote }} + cleanup_interval = {{ $cfg.eventHub.cleanupInterval | quote }} + retention_period = {{ $cfg.eventHub.retentionPeriod | quote }} + + [platform_api.webhook] + enabled = {{ $cfg.webhook.enabled }} + {{- if $cfg.webhook.enabled }} + secret = {{ `'{{ env "APIP_CP_WEBHOOK_SECRET" }}'` }} + {{- with $cfg.webhook.privateKeyPath }} + private_key_path = {{ . | quote }} + {{- end }} + signature_tolerance = {{ $cfg.webhook.signatureTolerance | quote }} + max_body_size = {{ $cfg.webhook.maxBodySize | int }} + signature_header = {{ $cfg.webhook.signatureHeader | quote }} + {{- end }} + {{- with $pa.configToml }} + + {{ . | nindent 4 | trim }} + {{- end }} +{{- end }} diff --git a/kubernetes/helm/platform-api-helm-chart/templates/deployment.yaml b/kubernetes/helm/platform-api-helm-chart/templates/deployment.yaml new file mode 100644 index 0000000000..a28018a997 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/deployment.yaml @@ -0,0 +1,218 @@ +{{- $pa := .Values -}} +{{- $deployment := $pa.deployment -}} +{{- $secrets := $pa.secrets -}} +{{- $tls := $pa.tls -}} +{{- $https := $pa.config.server.https -}} +{{- $jwt := $pa.config.auth.jwt -}} +{{- $jwtKeyDir := regexReplaceAll "/[^/]+$" $jwt.publicKeyFile "" -}} +{{- $tlsMounted := and $https.enabled (or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret")) -}} +{{- if and $deployment.enabled $https.enabled (not (or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret"))) }} +{{- fail "platform-api: config.server.https.enabled=true requires tls.certificateProvider to be \"cert-manager\" or \"secret\" — the Platform API has no self-signed fallback." }} +{{- end }} +{{- $secretName := include "apip.platformApi.secretName" . -}} +{{- if $deployment.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "apip.platformApi.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "platform-api" $deployment.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $deployment.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + replicas: {{ $deployment.replicaCount }} + {{- with $deployment.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "apip.componentSelectorLabels" (list . "platform-api") | nindent 6 }} + template: + metadata: + labels: + {{- include "apip.componentPodLabels" (list . "platform-api" $deployment.podLabels) | nindent 8 }} + {{- $podAnnotations := omit (merge (dict) (default (dict) $deployment.podAnnotations) (default (dict) .Values.global.commonAnnotations)) "checksum/config" }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with $podAnnotations }} + {{- include "apip.renderStringMap" . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "apip.serviceAccountName" . }} + {{- with (include "apip.componentImagePullSecretsBlock" (dict "root" . "componentPullSecrets" $pa.imagePullSecrets)) }} + {{- . | nindent 6 }} + {{- end }} + {{- with $deployment.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with $deployment.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if hasKey $deployment "terminationGracePeriodSeconds" }} + terminationGracePeriodSeconds: {{ $deployment.terminationGracePeriodSeconds }} + {{- end }} + {{- with $deployment.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $deployment.dnsPolicy }} + dnsPolicy: {{ . }} + {{- end }} + {{- with $deployment.dnsConfig }} + dnsConfig: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if hasKey $deployment "automountServiceAccountToken" }} + automountServiceAccountToken: {{ $deployment.automountServiceAccountToken }} + {{- end }} + containers: + - name: platform-api + image: {{ include "apip.componentImage" (dict "root" . "repository" $pa.image.repository "defaultRepository" "ghcr.io/wso2/api-platform/platform-api" "tag" $pa.image.tag) | quote }} + imagePullPolicy: {{ $pa.image.pullPolicy }} + {{- with $deployment.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + args: + - "-config" + - "/etc/platform-api/config-platform-api.toml" + env: + # Secrets sourced from the external Secret (created by generate-secrets.sh); + # resolved into the config file via its {{ "{{ env \"APIP_CP_*\" }}" }} tokens. + - name: APIP_CP_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.encryptionKey }} + {{- if ne $pa.config.database.driver "sqlite3" }} + - name: APIP_CP_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.databasePassword }} + {{- end }} + {{- if $pa.config.webhook.enabled }} + - name: APIP_CP_WEBHOOK_SECRET + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.webhookSecret }} + {{- end }} + {{- if eq $pa.config.auth.mode "file" }} + # file-mode admin credentials — generated username + bcrypt hash from + # the Secret; no admin/admin default (the config token fails closed). + - name: APIP_CP_ADMIN_USERNAME + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.adminUsername }} + - name: APIP_CP_ADMIN_PASSWORD_HASH + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.adminPasswordHash }} + {{- end }} + {{- range $deployment.extraEnv }} + - {{- toYaml . | nindent 14 }} + {{- end }} + {{- with $deployment.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ $pa.containerPort }} + protocol: TCP + livenessProbe: + {{- toYaml $deployment.livenessProbe | nindent 12 }} + {{- with $deployment.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + readinessProbe: + {{- toYaml $deployment.readinessProbe | nindent 12 }} + resources: + {{- toYaml ($deployment.resources | default dict) | nindent 12 }} + volumeMounts: + - name: data + mountPath: {{ $deployment.volumeMountPath }} + - name: config + mountPath: /etc/platform-api/config-platform-api.toml + subPath: config-platform-api.toml + - name: jwt-keys + mountPath: {{ $jwtKeyDir }} + readOnly: true + {{- if $tlsMounted }} + - name: tls-certs + mountPath: {{ $https.certDir }} + readOnly: true + {{- end }} + {{- with $deployment.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + {{- if $pa.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ if $pa.persistence.existingClaim }}{{ $pa.persistence.existingClaim }}{{ else }}{{ include "apip.platformApi.fullname" . }}-data{{ end }} + {{- else }} + emptyDir: {} + {{- end }} + - name: config + configMap: + name: {{ include "apip.platformApi.fullname" . }}-config + items: + - key: config-platform-api.toml + path: config-platform-api.toml + # RS256 JWT keys mounted as PEM files from the external Secret. The + # public key verifies tokens (every mode); the private key signs + # file-mode login tokens (mounted only in file mode). + - name: jwt-keys + secret: + secretName: {{ $secretName }} + items: + - key: {{ $secrets.keys.jwtPublicKey }} + path: {{ base $jwt.publicKeyFile }} + {{- if eq $pa.config.auth.mode "file" }} + - key: {{ $secrets.keys.jwtPrivateKey }} + path: {{ base $jwt.privateKeyFile }} + {{- end }} + {{- if $tlsMounted }} + - name: tls-certs + secret: + {{- if eq $tls.certificateProvider "cert-manager" }} + secretName: {{ include "apip.platformApi.fullname" . }}-tls + {{- else if eq $tls.certificateProvider "secret" }} + secretName: {{ $tls.secret.name }} + {{- end }} + items: + - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.crt{{ else }}{{ $tls.secret.certKey }}{{ end }} + path: cert.pem + - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.key{{ else }}{{ $tls.secret.keyKey }}{{ end }} + path: key.pem + {{- end }} + {{- with $deployment.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/platform-api-helm-chart/templates/hpa.yaml b/kubernetes/helm/platform-api-helm-chart/templates/hpa.yaml new file mode 100644 index 0000000000..1c812c3416 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/hpa.yaml @@ -0,0 +1,51 @@ +{{- $pa := .Values -}} +{{- $hpa := $pa.hpa -}} +{{- if and $pa.deployment.enabled $hpa.enabled }} +{{- if ne $pa.config.database.driver "postgres" }} +{{- fail "platformApi.hpa requires platformApi.config.database.driver=postgres — SQLite does not support multiple replicas." }} +{{- end }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "apip.platformApi.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "platform-api" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "apip.platformApi.fullname" . }} + minReplicas: {{ $hpa.minReplicas }} + maxReplicas: {{ $hpa.maxReplicas }} + {{- if or $hpa.targetCPUUtilizationPercentage $hpa.targetMemoryUtilizationPercentage $hpa.customMetrics }} + metrics: + {{- if $hpa.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $hpa.targetCPUUtilizationPercentage }} + {{- end }} + {{- if $hpa.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $hpa.targetMemoryUtilizationPercentage }} + {{- end }} + {{- with $hpa.customMetrics }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with $hpa.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/platform-api-helm-chart/templates/issuer.yaml b/kubernetes/helm/platform-api-helm-chart/templates/issuer.yaml new file mode 100644 index 0000000000..1e729edca8 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/issuer.yaml @@ -0,0 +1,22 @@ +{{- $pa := .Values -}} +{{- $tls := $pa.tls -}} +{{- if and $pa.deployment.enabled $pa.config.server.https.enabled (eq $tls.certificateProvider "cert-manager") }} +{{- if and (eq $tls.certManager.issuerRef.kind "Issuer") $tls.certManager.createIssuer }} +--- +# Self-signed Issuer for development/testing. In production replace this with a +# proper CA issuer (Let's Encrypt, corporate CA, etc.) via createIssuer=false. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "apip.platformApi.fullname" . }}-selfsigned-issuer + labels: + {{- include "apip.componentLabels" (list . "platform-api" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + selfSigned: {} +{{- end }} +{{- end }} + diff --git a/kubernetes/helm/platform-api-helm-chart/templates/pdb.yaml b/kubernetes/helm/platform-api-helm-chart/templates/pdb.yaml new file mode 100644 index 0000000000..989518f364 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/pdb.yaml @@ -0,0 +1,30 @@ +{{- $pa := .Values -}} +{{- $pdb := $pa.podDisruptionBudget -}} +{{- if and $pa.deployment.enabled $pdb.enabled }} +{{- if and $pdb.minAvailable $pdb.maxUnavailable }} +{{- fail "platformApi.podDisruptionBudget: only one of minAvailable or maxUnavailable may be set" }} +{{- end }} +{{- if not (or $pdb.minAvailable $pdb.maxUnavailable) }} +{{- fail "platformApi.podDisruptionBudget: one of minAvailable or maxUnavailable must be set" }} +{{- end }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "apip.platformApi.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "platform-api" dict) | nindent 4 }} + {{- with (include "apip.annotations" (list . dict)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "apip.componentSelectorLabels" (list . "platform-api") | nindent 6 }} + {{- if $pdb.minAvailable }} + minAvailable: {{ $pdb.minAvailable }} + {{- else if $pdb.maxUnavailable }} + maxUnavailable: {{ $pdb.maxUnavailable }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/platform-api-helm-chart/templates/pvc.yaml b/kubernetes/helm/platform-api-helm-chart/templates/pvc.yaml new file mode 100644 index 0000000000..53c27264fe --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/pvc.yaml @@ -0,0 +1,26 @@ +{{- $pa := .Values -}} +{{- $persistence := $pa.persistence -}} +{{- if and $pa.deployment.enabled $persistence.enabled (not $persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "apip.platformApi.fullname" . }}-data + labels: + {{- include "apip.componentLabels" (list . "platform-api" $persistence.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $persistence.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- range $persistence.accessModes }} + - {{ . }} + {{- end }} + resources: + requests: + storage: {{ $persistence.size }} + {{- if $persistence.storageClass }} + storageClassName: {{ $persistence.storageClass | quote }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/platform-api-helm-chart/templates/service.yaml b/kubernetes/helm/platform-api-helm-chart/templates/service.yaml new file mode 100644 index 0000000000..58308d1084 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/service.yaml @@ -0,0 +1,50 @@ +{{- $pa := .Values -}} +{{- $service := $pa.service -}} +{{- $isNodePortCapable := or (eq $service.type "NodePort") (eq $service.type "LoadBalancer") -}} +{{- if $pa.deployment.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "apip.platformApi.fullname" . }} + labels: + {{- include "apip.componentLabels" (list . "platform-api" $service.labels) | nindent 4 }} + {{- with (include "apip.annotations" (list . $service.annotations)) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + type: {{ $service.type }} + {{- with $service.clusterIP }} + clusterIP: {{ . }} + {{- end }} + {{- if and $service.externalTrafficPolicy $isNodePortCapable }} + externalTrafficPolicy: {{ $service.externalTrafficPolicy }} + {{- end }} + {{- if eq $service.type "LoadBalancer" }} + {{- with $service.loadBalancerClass }} + loadBalancerClass: {{ . }} + {{- end }} + {{- with $service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with $service.ipFamilyPolicy }} + ipFamilyPolicy: {{ . }} + {{- end }} + {{- with $service.ipFamilies }} + ipFamilies: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + {{- include "apip.componentSelectorLabels" (list . "platform-api") | nindent 4 }} + ports: + - name: http + port: {{ $service.port }} + targetPort: http + protocol: TCP + {{- if and $isNodePortCapable $service.nodePort }} + nodePort: {{ $service.nodePort }} + {{- end }} +{{- end }} + diff --git a/kubernetes/helm/platform-api-helm-chart/values-local.yaml b/kubernetes/helm/platform-api-helm-chart/values-local.yaml new file mode 100644 index 0000000000..6a03644516 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/values-local.yaml @@ -0,0 +1,15 @@ +# Local development values for the platform-api chart. +# Usage (standalone, from kubernetes/helm/): +# helm install platform-api ./platform-api-helm-chart \ +# -f platform-api-helm-chart/values.yaml -f platform-api-helm-chart/values-local.yaml +# +# Overrides the defaults for local development: +# - locally-built image (latest tag, IfNotPresent pull policy) +# - debug logging +image: + tag: "latest" + pullPolicy: IfNotPresent + +config: + logging: + level: debug diff --git a/kubernetes/helm/platform-api-helm-chart/values.yaml b/kubernetes/helm/platform-api-helm-chart/values.yaml new file mode 100644 index 0000000000..bafe47212d --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/values.yaml @@ -0,0 +1,413 @@ +# ============================================================================ +# platform-api — component chart values +# ============================================================================ +# The shared Platform API control plane (Go backend). This chart is released on +# its own and is used two ways: +# • as a subchart of a product umbrella (ai-workspace / developer-portal) +# • standalone (helm install this chart directly) +# +# Two kinds of settings live here: +# global.* Cross-cutting settings shared by all components. Under an umbrella +# the umbrella supplies these; the block below is only the STANDALONE +# default and is overridden when this chart runs nested. +# (rest) This component's own settings — image, config, service, storage, … +# +# `enabled` is read by the umbrella's dependency condition (ignored standalone). + +# ---------------------------------------------------------------------------- +# global — STANDALONE defaults only (an umbrella's global.* overrides these) +# ---------------------------------------------------------------------------- +global: + nameOverride: "" + fullnameOverride: "" + commonLabels: {} + commonAnnotations: {} + imagePullSecrets: [] + serviceAccount: + # Standalone default is `false` → pods use the namespace "default" SA, because + # this component ships no ServiceAccount template (the umbrella owns SA + # creation and sets create: true). + create: false + name: "" + annotations: {} + automountServiceAccountToken: true + wso2: + subscription: + imagePullSecret: "" + platformApi: + port: 9243 + tlsEnabled: true + +# ---------------------------------------------------------------------------- +# Component toggle + container image +# ---------------------------------------------------------------------------- +# Install this component. Both portals need the Platform API, so leave it on +# unless you point them at an EXTERNAL Platform API via the portal URL overrides. +enabled: true +image: + repository: ghcr.io/wso2/api-platform/platform-api + tag: 0.13.0 + pullPolicy: IfNotPresent +# Pull secrets for THIS component only (merged with global.imagePullSecrets). +imagePullSecrets: [] + +# ---------------------------------------------------------------------------- +# config — rendered into /etc/platform-api/config-platform-api.toml under the +# [platform_api.*] tables the Go binary reads (platform-api/config/config.go). +# ---------------------------------------------------------------------------- +# Everything here becomes the app's config file. Secrets (encryption key, DB +# password, webhook secret) are NOT here — they are injected as env vars from a +# Kubernetes Secret and pulled in via the config's own {{ env }} tokens; the +# RS256 JWT keys are mounted as PEM files. See `secrets:` further down. +config: + # Resource paths loaded at startup. Defaults match the published container + # image — override only for a custom layout. + resources: + dbSchemaPath: ./internal/database/schema.sql + openapiSpecPath: ./resources/openapi.yaml + llmTemplateDefinitionsPath: ./resources/default-llm-provider-templates + # Byte cap when fetching a remote OpenAPI spec by URL. <= 0 uses the + # built-in 5 MiB default. + openapiSpecMaxFetchBytes: 5242880 + + logging: + level: info # debug | info | warn | error + format: json # text | json + + security: + apiKey: + # Accepted API-key hashing algorithms, e.g. "sha256" or "sha256,sha512". + hashingAlgorithms: sha256 + + # --- Database --- + database: + # sqlite3 — no external DB; the file lives on the PVC (see persistence). + # Single replica only → the HPA cannot be used. + # postgres — external PostgreSQL (configured below); enables multi-replica + HPA. + driver: sqlite3 + # SQLite database file (driver=sqlite3 only). Must sit under the mounted data + # volume (deployment.volumeMountPath), so it persists on the PVC. + path: /app/data/api_platform.db + # PostgreSQL connection (driver=postgres only). The password is NOT set here — + # it comes from the Secret key secrets.keys.databasePassword. + postgres: + host: "" # in-cluster Service or external endpoint + port: 5432 + name: platform_api # database (schema) name + user: platform_api # login role; password from the Secret + sslMode: disable # disable | require | verify-ca | verify-full + sslRootCert: "" # required for verify-ca / verify-full + sslCert: "" # client cert for mTLS (postgres only; with sslKey) + sslKey: "" + maxOpenConns: 25 + maxIdleConns: 10 + connMaxLifetime: 300 # seconds before a connection is recycled + + # --- Authentication --- + auth: + # Exactly one mode: + # external_token — verify externally-minted RS256 JWTs with the public key. + # file — external_token + local username/password login (issues + # RS256 tokens signed with the private key). + # idp — validate tokens against an external IDP's JWKS. + mode: file + # Enforce per-endpoint OAuth2 scopes on validated tokens. + scopeValidation: true + # Paths that bypass auth. Empty keeps the binary's built-in default list + # (health/metrics/login/internal gateway routes); setting it REPLACES that list. + skipPaths: [] + # Claim-name mappings shared by all modes. + claimMappings: + organization: organization + orgName: org_name + orgHandle: org_handle + userId: sub + username: username + email: email + scope: scope + roles: "" # e.g. "realm_access.roles" (Keycloak) + # Local RS256 JWT keys. public_key_file verifies tokens (every mode); + # private_key_file signs login tokens (file mode only). Both are mounted as + # PEM files from the Secret (secrets.keys.jwtPublicKey / jwtPrivateKey). + jwt: + issuer: platform-api + publicKeyFile: /etc/platform-api/keys/jwt_public.pem + privateKeyFile: /etc/platform-api/keys/jwt_private.pem + tokenTtl: 1h # lifetime of file-mode login tokens (Go duration) + # file mode — local username/password login (rendered only when mode=file). + file: + organization: + id: default + displayName: Default + region: us + # Platform organization UUID, emitted as the `organization` claim. Pin it + # to keep the org stable across fresh databases. + uuid: 99089a17-72e0-4dd8-a2f4-c8dfbb085295 + # The single file-mode admin user. Its username and bcrypt password hash + # are NEVER set here — they come from the Secret via APIP_CP_ADMIN_USERNAME / + # APIP_CP_ADMIN_PASSWORD_HASH (secrets.keys.adminUsername / adminPasswordHash), + # which generate-secrets.sh provisions with a generated username and a bcrypt + # hash. There is no admin/admin default: startup fails closed if unset. Only + # the granted scopes are configured here (add more users via configToml). + admin: + scopes: "ap:organization:manage ap:gateway:manage ap:gateway_custom_policy:manage ap:rest_api:manage ap:llm_provider:manage ap:llm_proxy:manage ap:mcp_proxy:manage ap:application:manage ap:subscription:manage ap:subscription_plan:manage ap:project:manage ap:llm_template:manage ap:devportal:manage ap:api_key:read ap:secret:manage" + # idp mode — external OIDC provider (rendered only when mode=idp). jwksUrl is + # required in that mode. + idp: + name: "" + jwksUrl: "" + issuer: [] # accepted token issuers + audience: [] # accepted audiences; empty = don't check + validationMode: scope # scope | role + roleMappings: "" # path to a role→scope mapping YAML + + # --- Server listeners --- + server: + # Plain-HTTP listener — enable only behind a TLS-terminating proxy/mesh. + http: + enabled: false + port: 9080 + # HTTPS listener (serves on containerPort). Certs are provisioned by the + # top-level `tls:` block and mounted into certDir as cert.pem/key.pem — the + # binary has NO self-signed fallback, so certificateProvider must be + # cert-manager or secret. + https: + enabled: true + certDir: /app/data/certs + # Connection-lifetime bounds (Go durations). 0 disables a timeout. + timeouts: + readHeader: 10s + read: 60s + write: 120s + idle: 120s + # Browser origins allowed to call the API with credentials. MUST be explicit + # origins — never "*" (rejected at startup). Empty disables cross-origin access. + cors: + allowedOrigins: [] + # - https://workspace.example.com + websocket: + maxConnections: 1000 + connectionTimeout: 30 # seconds before an idle connection closes + rateLimitPerMin: 1000 + metricsLogEnabled: true + metricsLogInterval: 10 + + # --- Gateway registration checks --- + gateway: + enableVersionVerification: false + enableFunctionalityTypeVerification: false + + # --- Deployments --- + deployments: + maxPerApiGateway: 20 + transitionalStatusEnabled: false + timeoutEnabled: true + timeoutInterval: 20 + timeoutDuration: 60 + + # --- EventHub (multi-replica HA event delivery) --- + eventHub: + pollInterval: 3s + cleanupInterval: 10m + retentionPeriod: 1h + + # --- Webhook receiver (signed events from the Developer Portal) --- + webhook: + enabled: false + # HMAC secret (required when enabled) comes from the Secret via + # APIP_CP_WEBHOOK_SECRET (secrets.keys.webhookSecret). + privateKeyPath: "" # PEM RSA key to decrypt encrypted_key fields + signatureTolerance: 5m + maxBodySize: 1048576 + signatureHeader: X-Devportal-Signature + +# Escape hatch: raw TOML appended verbatim to the generated config file, for keys +# not exposed as structured values above. +configToml: "" + +# ---------------------------------------------------------------------------- +# secrets — reference to a pre-created Kubernetes Secret (REQUIRED) +# ---------------------------------------------------------------------------- +# The chart never creates or embeds secret values. Point existingSecret at a +# Secret made by ./generate-secrets.sh. Render FAILS if it's unset. The Secret +# must hold ENCRYPTION_KEY and the RS256 JWT key files jwt_public.pem / +# jwt_private.pem (mounted into config.auth.jwt.*File), plus DATABASE_PASSWORD +# when driver=postgres and WEBHOOK_SECRET when config.webhook.enabled. `keys` +# maps each logical name to the key inside that Secret. +secrets: + existingSecret: "" + keys: + encryptionKey: ENCRYPTION_KEY # → APIP_CP_ENCRYPTION_KEY + databasePassword: DATABASE_PASSWORD # → APIP_CP_DATABASE_PASSWORD (postgres) + webhookSecret: WEBHOOK_SECRET # → APIP_CP_WEBHOOK_SECRET (webhook.enabled) + jwtPublicKey: jwt_public.pem # mounted file → config.auth.jwt.publicKeyFile + jwtPrivateKey: jwt_private.pem # mounted file → config.auth.jwt.privateKeyFile (file mode) + adminUsername: ADMIN_USERNAME # → APIP_CP_ADMIN_USERNAME (auth.mode=file) + adminPasswordHash: ADMIN_PASSWORD_HASH # → APIP_CP_ADMIN_PASSWORD_HASH (auth.mode=file) + +# ---------------------------------------------------------------------------- +# service — how the API is exposed inside the cluster +# ---------------------------------------------------------------------------- +service: + type: ClusterIP # ClusterIP | NodePort | LoadBalancer + annotations: {} + labels: {} + # Port the Service listens on. Keep equal to containerPort unless you front it + # on a different port. + port: 9243 + clusterIP: "" + externalTrafficPolicy: "" + loadBalancerClass: "" + loadBalancerSourceRanges: [] + ipFamilyPolicy: "" + ipFamilies: [] + nodePort: "" + +# Port the container process listens on (also written into config as `port`). +containerPort: 9243 + +# ---------------------------------------------------------------------------- +# tls — where the HTTPS listener's certificate comes from (used when +# config.server.https.enabled). The Platform API has NO self-signed fallback, +# so a real cert MUST be supplied via one of the providers below. +# ---------------------------------------------------------------------------- +tls: + # cert-manager → a cert-manager Certificate provisions the cert. + # secret → use an existing Secret containing tls.crt / tls.key. + certificateProvider: cert-manager + certManager: + create: true + # true → create a self-signed Issuer and use it (dev/testing). + # false → use an existing issuer referenced by issuerRef (production CA). + createIssuer: true + issuerRef: + name: selfsigned-issuer # used when createIssuer=false + kind: Issuer # Issuer | ClusterIssuer + # group: cert-manager.io + commonName: platform-api.localhost + dnsNames: + - platform-api.localhost + - "*.localhost" + duration: 2160h + renewBefore: 720h + secret: + name: platform-api-tls # used when certificateProvider=secret + certKey: tls.crt + keyKey: tls.key + +# ---------------------------------------------------------------------------- +# persistence — the PVC backing SQLite (and self-signed certs) +# ---------------------------------------------------------------------------- +persistence: + # Required for driver=sqlite3 (holds the DB file and any self-signed certs). + # Safe to disable for a postgres deployment that also uses cert-manager/secret TLS. + enabled: true + existingClaim: "" # reuse a PVC you created yourself instead + accessModes: + - ReadWriteOnce + size: 1Gi + storageClass: "" # "" = cluster default StorageClass + labels: {} + # Annotations for the PVC ONLY (rendered only when persistence.enabled). The + # resource-policy=keep annotation makes the PVC — and its data — SURVIVE + # `helm uninstall`; a later `helm install` with the SAME release name re-adopts + # it, so your data is preserved across reinstalls. To intentionally wipe it: + # kubectl delete pvc -platform-api-data + # Set to {} if you'd rather the PVC be deleted on uninstall. + annotations: + helm.sh/resource-policy: keep + +# ---------------------------------------------------------------------------- +# deployment — the pod/Deployment spec +# ---------------------------------------------------------------------------- +deployment: + enabled: true + replicaCount: 1 # >1 requires driver=postgres + # Where the data volume mounts. Must contain config.database.path and + # config.tls.certDir. + volumeMountPath: /app/data + extraEnv: [] # extra env vars (list of {name,value|valueFrom}) + extraEnvFrom: [] # extra envFrom sources (configMapRef/secretRef) + extraVolumeMounts: [] + extraVolumes: [] + labels: {} + annotations: {} + podAnnotations: {} + podLabels: {} + priorityClassName: "" + livenessProbe: + httpGet: + path: /health + port: http + scheme: HTTPS + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + scheme: HTTPS + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + # Optional startup probe for slow first starts (uncomment to enable): + # startupProbe: + # httpGet: + # path: /health + # port: http + # scheme: HTTPS + # periodSeconds: 5 + # failureThreshold: 30 + # CPU/memory requests & limits. NOTE: set CPU requests if you enable the HPA, + # otherwise it can't compute utilization. + resources: {} + # limits: + # cpu: 500m + # memory: 512Mi + # requests: + # cpu: 250m + # memory: 256Mi + podSecurityContext: {} + securityContext: {} + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + strategy: {} + # terminationGracePeriodSeconds: 30 + hostAliases: [] + dnsPolicy: "" + dnsConfig: {} + # automountServiceAccountToken: false + +# ---------------------------------------------------------------------------- +# Autoscaling & disruption budget +# ---------------------------------------------------------------------------- +# HorizontalPodAutoscaler. REQUIRES config.database.driver=postgres — on sqlite3 +# the chart refuses to render it (SQLite is single-replica). Enable only after +# switching to postgres, and set deployment.resources.requests.cpu so the HPA has +# a baseline to measure against. +hpa: + enabled: false + minReplicas: 2 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 # 0 or "" to disable CPU-based scaling + targetMemoryUtilizationPercentage: "" # set a number to also scale on memory + customMetrics: [] # extra autoscaling/v2 MetricSpec entries + behavior: {} # autoscaling/v2 scaleUp/scaleDown tuning + +# PodDisruptionBudget. Only meaningful at replicaCount >= 2 — with a single replica +# minAvailable: 1 blocks all voluntary evictions (node drains would hang). Set +# exactly ONE of minAvailable / maxUnavailable. +podDisruptionBudget: + enabled: false + minAvailable: 1 # integer or percentage string, e.g. "50%" + maxUnavailable: "" # integer or percentage string, e.g. "25%" + +# Extra metadata for the generated ConfigMap. +configMap: + annotations: {} + labels: {}