From 52b88417b138b4e95dc74830eae794ce11d19fcd Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Thu, 23 Jul 2026 12:37:32 +0530 Subject: [PATCH 1/9] Add helm releases for individual components(AIW, PA, DP) --- kubernetes/helm/.gitignore | 10 + .../ai-workspace-ui-helm-chart/.helmignore | 16 + .../ai-workspace-ui-helm-chart/Chart.yaml | 13 + .../templates/_helpers.tpl | 216 +++++++++++ .../templates/certificate.yaml | 37 ++ .../templates/configmap.yaml | 47 +++ .../templates/deployment.yaml | 183 ++++++++++ .../templates/hpa.yaml | 48 +++ .../templates/issuer.yaml | 22 ++ .../templates/pdb.yaml | 30 ++ .../templates/service.yaml | 50 +++ .../values-local.yaml | 13 + .../ai-workspace-ui-helm-chart/values.yaml | 253 +++++++++++++ .../.helmignore | 16 + .../developer-portal-ui-helm-chart/Chart.yaml | 12 + .../templates/_helpers.tpl | 216 +++++++++++ .../templates/certificate.yaml | 37 ++ .../templates/configmap.yaml | 19 + .../templates/deployment.yaml | 271 ++++++++++++++ .../templates/hpa.yaml | 51 +++ .../templates/issuer.yaml | 22 ++ .../templates/pdb.yaml | 30 ++ .../templates/pvc.yaml | 26 ++ .../templates/service.yaml | 50 +++ .../values-local.yaml | 13 + .../values.yaml | 279 ++++++++++++++ .../helm/platform-api-helm-chart/.helmignore | 16 + .../helm/platform-api-helm-chart/Chart.yaml | 12 + .../templates/_helpers.tpl | 216 +++++++++++ .../templates/certificate.yaml | 37 ++ .../templates/configmap.yaml | 99 +++++ .../templates/deployment.yaml | 183 ++++++++++ .../templates/hpa.yaml | 51 +++ .../templates/issuer.yaml | 22 ++ .../templates/pdb.yaml | 30 ++ .../templates/pvc.yaml | 26 ++ .../templates/service.yaml | 50 +++ .../platform-api-helm-chart/values-local.yaml | 13 + .../helm/platform-api-helm-chart/values.yaml | 344 ++++++++++++++++++ 39 files changed, 3079 insertions(+) create mode 100644 kubernetes/helm/.gitignore create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/.helmignore create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/templates/certificate.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/templates/hpa.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/templates/issuer.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/templates/pdb.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/templates/service.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml create mode 100644 kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/.helmignore create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/certificate.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/hpa.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/issuer.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/pdb.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/pvc.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/templates/service.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml create mode 100644 kubernetes/helm/developer-portal-ui-helm-chart/values.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/.helmignore create mode 100644 kubernetes/helm/platform-api-helm-chart/Chart.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/deployment.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/hpa.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/issuer.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/pdb.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/pvc.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/templates/service.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/values-local.yaml create mode 100644 kubernetes/helm/platform-api-helm-chart/values.yaml diff --git a/kubernetes/helm/.gitignore b/kubernetes/helm/.gitignore new file mode 100644 index 0000000000..d60835427e --- /dev/null +++ b/kubernetes/helm/.gitignore @@ -0,0 +1,10 @@ +# Vendored subchart archives + lockfiles fetched by `helm dependency update`. +# Build artifacts (the umbrellas pull their components in) — never commit them. +ai-workspace-helm-chart/charts/ +ai-workspace-helm-chart/Chart.lock +developer-portal-helm-chart/charts/ +developer-portal-helm-chart/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..764e5944f6 --- /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 umbrella + (and pluggable into others) and installable standalone. +type: application +version: 0.1.0 +appVersion: "1.1.0-SNAPSHOT" +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..30f5d5cdec --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl @@ -0,0 +1,216 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Shared helpers for the api-platform-portals suite. + +Cross-cutting configuration (developmentMode, 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. */}} +{{- define "apip.platformApi.fullname" -}} +{{- printf "%s-platform-api" (include "apip.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.aiWorkspace.fullname" -}} +{{- printf "%s-ai-workspace" (include "apip.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.developerPortal.fullname" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" .) | 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 -}} +{{- $scheme := ternary "https" "http" (default true $pa.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..7f4327b622 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml @@ -0,0 +1,47 @@ +{{- $ui := .Values -}} +{{- $cfg := $ui.config -}} +{{- 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: + config.toml: | + domain = {{ $cfg.domain | quote }} + default_org_region = {{ $cfg.defaultOrgRegion | quote }} + auth_mode = {{ $cfg.authMode | quote }} + {{- with $cfg.controlplaneHost }} + controlplane_host = {{ . | quote }} + {{- end }} + platform_gateway_versions = {{ $cfg.platformGatewayVersions | quote }} + {{- if eq $cfg.authMode "oidc" }} + + # --- OIDC (client secret is injected via environment, never written here) --- + oidc_authority = {{ $cfg.oidc.authority | quote }} + oidc_client_id = {{ $cfg.oidc.clientId | quote }} + {{- with $cfg.oidc.redirectUrl }} + oidc_redirect_url = {{ . | quote }} + {{- end }} + {{- with $cfg.oidc.postLogoutRedirectUrl }} + oidc_post_logout_redirect_url = {{ . | quote }} + {{- end }} + {{- with $cfg.oidc.scope }} + oidc_scope = {{ . | quote }} + {{- end }} + oidc_username_claim = {{ $cfg.oidc.usernameClaim | quote }} + oidc_org_id_claim = {{ $cfg.oidc.orgIdClaim | quote }} + oidc_org_name_claim = {{ $cfg.oidc.orgNameClaim | quote }} + oidc_org_handle_claim = {{ $cfg.oidc.orgHandleClaim | quote }} + {{- 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..52c9e449d8 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml @@ -0,0 +1,183 @@ +{{- $ui := .Values -}} +{{- $deployment := $ui.deployment -}} +{{- $secrets := $ui.secrets -}} +{{- $tls := $ui.tls -}} +{{- $selfSigned := eq $tls.certificateProvider "selfSigned" -}} +{{- $tlsMounted := or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret") -}} +{{- $platformApiUrl := default (include "apip.platformApi.internalURL" .) $ui.platformApi.url -}} +{{- $skipVerify := and $ui.platformApi.tlsSkipVerify .Values.global.developmentMode -}} +{{- $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 }} + env: + - name: BFF_ADDR + value: {{ printf ":%d" (int $ui.containerPort) | quote }} + - name: BFF_CONFIG_FILE + value: /etc/ai-workspace/config.toml + - name: PLATFORM_API_URL + value: {{ $platformApiUrl | quote }} + - name: PLATFORM_API_TLS_SKIP_VERIFY + value: {{ $skipVerify | toString | quote }} + {{- with $ui.platformApi.caFile }} + - name: PLATFORM_API_CA_FILE + value: {{ . | quote }} + {{- end }} + - name: SESSION_STORE + value: {{ $ui.sessionStore | quote }} + - name: COOKIE_SECURE + value: {{ $ui.cookie.secure | toString | quote }} + - name: COOKIE_SAMESITE + value: {{ $ui.cookie.sameSite | quote }} + - name: BFF_TLS_SELF_SIGNED + value: {{ $selfSigned | toString | quote }} + - name: VITE_AUTH_MODE + value: {{ $ui.config.authMode | quote }} + {{- if eq $ui.config.authMode "oidc" }} + # OIDC client secret from the external UI Secret (generate-secrets.sh). + # The secretName helper fails the render if ui.secrets.existingSecret is unset. + - name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "apip.aiWorkspace.secretName" . }} + key: {{ $secrets.keys.oidcClientSecret }} + {{- end }} + {{- range $deployment.extraEnv }} + - {{- toYaml . | nindent 14 }} + {{- 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: + - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.crt{{ else }}{{ $tls.secret.certKey }}{{ end }} + path: tls.crt + - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.key{{ else }}{{ $tls.secret.keyKey }}{{ end }} + path: tls.key + {{- 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..7e6a6ad964 --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml @@ -0,0 +1,13 @@ +# Local development values for the ai-workspace-ui chart. +# Usage (standalone): +# helm install ai-workspace-ui ./ai-workspace-ui-helm-chart -f values-local.yaml +# +# Overrides the defaults for local development: +# - developmentMode on (self-signed TLS, basic/file-based login) +# - locally-built image (latest tag, IfNotPresent pull policy) +global: + developmentMode: true + +image: + tag: "latest" + pullPolicy: IfNotPresent 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..95cd9accea --- /dev/null +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml @@ -0,0 +1,253 @@ +# ============================================================================ +# 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: + developmentMode: true + 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.1.0-SNAPSHOT" + pullPolicy: IfNotPresent +# Pull secrets for THIS component only (merged with global.imagePullSecrets). +imagePullSecrets: [] + +# ---------------------------------------------------------------------------- +# config — rendered into /etc/ai-workspace/config.toml +# ---------------------------------------------------------------------------- +config: + # Hostname (or host:port) the browser uses to reach the UI. Shown in the address + # bar and used to build redirect URLs. + domain: ai-workspace.localhost + # Region assigned to a new organization the first time someone logs in. + defaultOrgRegion: us + # How users sign in: + # basic → the Platform API's file-based login (admin/admin in dev) + # oidc → an external OIDC provider (fill in config.oidc below + the secret) + authMode: basic + # Public address of the control plane that GATEWAYS dial to reach the Platform + # API. Shown verbatim in the in-UI "add a gateway" instructions. + controlplaneHost: "" + # Gateway build options offered in the create-gateway dropdown (a JSON string). + platformGatewayVersions: '[{"version":"1.2","latestVersion":"v1.2.0-M1","channel":"STS"}]' + # OIDC settings — used only when authMode=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" + # Map the IDP's token claims to what the UI expects: + usernameClaim: username + orgIdClaim: org_id + orgNameClaim: org_name + orgHandleClaim: org_handle + +# Escape hatch: raw TOML appended verbatim to the generated config.toml. +configToml: "" + +# ---------------------------------------------------------------------------- +# platformApi — how the BFF reaches the Platform API (server-to-server) +# ---------------------------------------------------------------------------- +platformApi: + # 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. Forced to false when developmentMode is false. + tlsSkipVerify: true + # Instead of skipping verification, mount a CA file (via extraVolumes/Mounts) + # and point to it here to properly verify the Platform API's certificate. + caFile: "" + +# ---------------------------------------------------------------------------- +# Session cookie & store +# ---------------------------------------------------------------------------- +cookie: + secure: true # keep true when served over HTTPS + sameSite: lax # lax | strict | none +# Where server-side sessions live: "memory" works for a single replica; use an +# external store if you scale the UI to multiple replicas. +sessionStore: memory + +# ---------------------------------------------------------------------------- +# 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: 5380 + clusterIP: "" + externalTrafficPolicy: "" + loadBalancerClass: "" + loadBalancerSourceRanges: [] + ipFamilyPolicy: "" + ipFamilies: [] + nodePort: "" + +# Port the BFF process listens on. +containerPort: 5380 + +# ---------------------------------------------------------------------------- +# 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..426d4cd598 --- /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 umbrella + (and pluggable into others) and installable standalone. +type: application +version: 0.1.0 +appVersion: "1.0.0-beta-SNAPSHOT" +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..30f5d5cdec --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl @@ -0,0 +1,216 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Shared helpers for the api-platform-portals suite. + +Cross-cutting configuration (developmentMode, 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. */}} +{{- define "apip.platformApi.fullname" -}} +{{- printf "%s-platform-api" (include "apip.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.aiWorkspace.fullname" -}} +{{- printf "%s-ai-workspace" (include "apip.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.developerPortal.fullname" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" .) | 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 -}} +{{- $scheme := ternary "https" "http" (default true $pa.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..8f89d9bb1d --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml @@ -0,0 +1,19 @@ +{{- $dp := .Values -}} +{{- if and $dp.deployment.enabled $dp.configToml }} +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: + # Optional raw config.toml. The Developer Portal is configured primarily via + # APIP_DP_* env vars (set on the Deployment); anything here is overridden by them. + config.toml: | + {{- $dp.configToml | nindent 4 }} +{{- 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..174107ac73 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml @@ -0,0 +1,271 @@ +{{- $dp := .Values -}} +{{- $deployment := $dp.deployment -}} +{{- $cfg := $dp.config -}} +{{- $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 -}} +{{- $baseUrl := default (printf "https://localhost:%d" (int $dp.containerPort)) $cfg.server.baseUrl -}} +{{- $platformApiUrl := default (include "apip.platformApi.internalURL" .) $cfg.platformApi.baseUrl -}} +{{- $demo := $cfg.demoEnabled -}} +{{- if kindIs "invalid" $demo }}{{- $demo = .Values.global.developmentMode -}}{{- end }} +{{- 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: + # ── Server / TLS ────────────────────────────────────────────── + - name: APIP_DP_SERVER_PORT + value: {{ $dp.containerPort | toString | quote }} + - name: APIP_DP_SERVER_BASEURL + value: {{ $baseUrl | quote }} + - name: APIP_DP_TLS_ENABLED + value: "true" + - name: APIP_DP_TLS_CERTFILE + value: {{ printf "%s/server.crt" $certDir | quote }} + - name: APIP_DP_TLS_KEYFILE + value: {{ printf "%s/server.key" $certDir | quote }} + - name: APIP_DP_TLS_CAFILE + value: {{ printf "%s/server.crt" $certDir | quote }} + # ── Database ────────────────────────────────────────────────── + - name: APIP_DP_DATABASE_TYPE + value: {{ $cfg.database.type | quote }} + {{- if eq $cfg.database.type "postgres" }} + - name: APIP_DP_DATABASE_HOST + value: {{ $cfg.database.host | quote }} + - name: APIP_DP_DATABASE_PORT + value: {{ $cfg.database.port | toString | quote }} + - name: APIP_DP_DATABASE_NAME + value: {{ $cfg.database.database | quote }} + - name: APIP_DP_DATABASE_USERNAME + value: {{ $cfg.database.user | quote }} + - name: APIP_DP_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.databasePassword | default "APIP_DP_DATABASE_PASSWORD" }} + {{- else }} + - name: APIP_DP_DATABASE_FILE + value: {{ $cfg.database.file | quote }} + {{- end }} + # ── Behaviour ───────────────────────────────────────────────── + - name: APIP_DP_DEMO_ENABLED + value: {{ $demo | toString | quote }} + - name: APIP_DP_LOGGING_CONSOLEONLY + value: {{ $cfg.logging.consoleOnly | toString | quote }} + - name: APIP_DP_SECURITY_ROLEVALIDATION + value: {{ $cfg.security.roleValidation | toString | quote }} + - name: APIP_DP_SECURITY_SERVICEAPIKEY_ENABLED + value: {{ $cfg.security.serviceApiKey.enabled | toString | quote }} + - name: APIP_DP_SECURITY_SERVICEAPIKEY_HEADERNAME + value: {{ $cfg.security.serviceApiKey.headerName | quote }} + - name: APIP_DP_ORGANIZATION_DEFAULTNAME + value: {{ $cfg.organization.defaultName | quote }} + # ── Platform API connectivity ──────────────────────────────── + - name: APIP_DP_PLATFORMAPI_BASEURL + value: {{ $platformApiUrl | quote }} + - name: APIP_DP_PLATFORMAPI_INSECURE + value: {{ $cfg.platformApi.insecure | toString | quote }} + # ── Secrets (from the external Secret) ──────────────────────── + - name: APIP_DP_SECURITY_ENCRYPTIONKEY + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.encryptionKey }} + - name: APIP_DP_PLATFORMAPI_JWTSECRET + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.platformApiJwtSecret }} + {{- if $secrets.hasServiceApiKeyValue }} + - name: APIP_DP_SECURITY_SERVICEAPIKEY_VALUE + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.serviceApiKeyValue }} + {{- end }} + # ── OIDC IDP (active when clientId is set) ──────────────────── + {{- if $cfg.idp.clientId }} + - name: APIP_DP_IDP_NAME + value: {{ $cfg.idp.name | quote }} + - name: APIP_DP_IDP_ISSUER + value: {{ $cfg.idp.issuer | quote }} + - name: APIP_DP_IDP_AUTHORIZATIONURL + value: {{ $cfg.idp.authorizationUrl | quote }} + - name: APIP_DP_IDP_TOKENURL + value: {{ $cfg.idp.tokenUrl | quote }} + - name: APIP_DP_IDP_USERINFOURL + value: {{ $cfg.idp.userInfoUrl | quote }} + - name: APIP_DP_IDP_JWKSURL + value: {{ $cfg.idp.jwksUrl | quote }} + - name: APIP_DP_IDP_CLIENTID + value: {{ $cfg.idp.clientId | quote }} + - name: APIP_DP_IDP_CALLBACKURL + value: {{ $cfg.idp.callbackUrl | quote }} + - name: APIP_DP_IDP_LOGOUTURL + value: {{ $cfg.idp.logoutUrl | quote }} + - name: APIP_DP_IDP_SCOPE + value: {{ $cfg.idp.scope | quote }} + {{- if $secrets.hasIdpClientSecret }} + - name: APIP_DP_IDP_CLIENTSECRET + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.idpClientSecret }} + {{- end }} + {{- 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 }} + {{- if $dp.configToml }} + - name: config + mountPath: /app/configs/config.toml + subPath: config.toml + {{- 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 }} + {{- if $dp.configToml }} + - name: config + configMap: + name: {{ include "apip.developerPortal.fullname" . }}-config + items: + - key: config.toml + path: config.toml + {{- 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..6c978f2b00 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml @@ -0,0 +1,13 @@ +# Local development values for the developer-portal-ui chart. +# Usage (standalone): +# helm install developer-portal-ui ./developer-portal-ui-helm-chart -f values-local.yaml +# +# Overrides the defaults for local development: +# - developmentMode on (self-signed TLS, sample-API seeding, file-based login) +# - locally-built image (latest tag, IfNotPresent pull policy) +global: + developmentMode: true + +image: + tag: "latest" + pullPolicy: IfNotPresent 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..02e0a903c9 --- /dev/null +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml @@ -0,0 +1,279 @@ +# ============================================================================ +# 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: + developmentMode: true + 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-beta-SNAPSHOT" + pullPolicy: IfNotPresent +# Pull secrets for THIS component only (merged with global.imagePullSecrets). +imagePullSecrets: [] + +# ---------------------------------------------------------------------------- +# config — becomes APIP_DP_* env vars on the container +# ---------------------------------------------------------------------------- +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: "" + # Demo features (sample-API seeding, onboarding prompts). null → follow + # global.developmentMode; set true/false to force. + demoEnabled: null + logging: + 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. Good for demos. + # 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 (gateway-convention key) + user: postgres # login role; password from the Secret + sslmode: disable # disable | require | verify-ca | verify-full + + # --- Security --- + security: + roleValidation: false # enforce role checks on incoming tokens + # 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 (needed while the + # Platform API uses a self-signed cert). Forced false when developmentMode + # is false. + insecure: true + + # --- OIDC login (optional) --- + # Used when idp.clientId is set. 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" + + organization: + defaultName: default # default organization name + +# 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: +# APIP_DP_SECURITY_ENCRYPTIONKEY and APIP_DP_PLATFORMAPI_JWTSECRET (the latter = +# the Platform API's AUTH_JWT_SECRET_KEY, so the portal can verify its tokens). +# The optional keys (OIDC client secret, service API key, DB password) are only +# wired when the matching has* flag below is true. +secrets: + existingSecret: "" + keys: + encryptionKey: APIP_DP_SECURITY_ENCRYPTIONKEY + platformApiJwtSecret: APIP_DP_PLATFORMAPI_JWTSECRET + idpClientSecret: APIP_DP_IDP_CLIENTSECRET + serviceApiKeyValue: APIP_DP_SECURITY_SERVICEAPIKEY_VALUE + databasePassword: APIP_DP_DATABASE_PASSWORD # used only when database.type=postgres + # Flip to true ONLY if the referenced Secret actually carries that optional key, + # so the deployment wires it as an env var. generate-secrets.sh sets these for + # you in the values-secrets.yaml it writes. + 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..dcbddf5c58 --- /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 (Go backend). Independently + released component chart; consumed by the product umbrella charts + (ai-workspace, developer-portal) and installable standalone. +type: application +version: 0.1.0 +appVersion: "0.12.0-SNAPSHOT" +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..30f5d5cdec --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl @@ -0,0 +1,216 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Shared helpers for the api-platform-portals suite. + +Cross-cutting configuration (developmentMode, 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. */}} +{{- define "apip.platformApi.fullname" -}} +{{- printf "%s-platform-api" (include "apip.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.aiWorkspace.fullname" -}} +{{- printf "%s-ai-workspace" (include "apip.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- define "apip.developerPortal.fullname" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" .) | 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 -}} +{{- $scheme := ternary "https" "http" (default true $pa.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..aaa54f0eb8 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml @@ -0,0 +1,37 @@ +{{- $pa := .Values -}} +{{- $tls := $pa.tls -}} +{{- if and $pa.deployment.enabled $pa.config.tls.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..1636ef1802 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml @@ -0,0 +1,99 @@ +{{- $pa := .Values -}} +{{- $cfg := $pa.config -}} +{{- $db := $cfg.database -}} +{{- $auth := $cfg.auth -}} +{{- 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: + config-platform-api.toml: | + log_level = {{ $cfg.logLevel | quote }} + log_format = {{ $cfg.logFormat | quote }} + port = {{ $pa.containerPort | toString | quote }} + enable_scope_validation = {{ $cfg.enableScopeValidation }} + + [database] + driver = {{ $db.driver | quote }} + {{- if eq $db.driver "postgres" }} + host = {{ $db.postgres.host | quote }} + port = {{ $db.postgres.port }} + name = {{ $db.postgres.database | quote }} + user = {{ $db.postgres.user | quote }} + ssl_mode = {{ $db.postgres.sslmode | quote }} + max_open_conns = {{ $db.postgres.max_open_conns }} + max_idle_conns = {{ $db.postgres.max_idle_conns }} + conn_max_lifetime = {{ $db.postgres.conn_max_lifetime }} + {{- else }} + path = {{ $db.path | quote }} + {{- end }} + + [auth.jwt] + enabled = {{ $auth.jwt.enabled }} + issuer = {{ $auth.jwt.issuer | quote }} + skip_validation = {{ if .Values.global.developmentMode }}{{ $auth.jwt.skipValidation }}{{ else }}false{{ end }} + + [auth.idp] + enabled = {{ $auth.idp.enabled }} + {{- if $auth.idp.enabled }} + {{- with $auth.idp.name }} + name = {{ . | quote }} + {{- end }} + jwks_url = {{ $auth.idp.jwksUrl | quote }} + issuer = {{ toJson $auth.idp.issuer }} + audience = {{ toJson $auth.idp.audience }} + validation_mode = {{ $auth.idp.validationMode | quote }} + + [auth.idp.claim_mappings] + organization_claim_name = {{ $auth.idp.claimMappings.organizationClaimName | quote }} + org_name_claim_name = {{ $auth.idp.claimMappings.orgNameClaimName | quote }} + org_handle_claim_name = {{ $auth.idp.claimMappings.orgHandleClaimName | quote }} + user_id_claim_name = {{ $auth.idp.claimMappings.userIdClaimName | quote }} + username_claim_name = {{ $auth.idp.claimMappings.usernameClaimName | quote }} + email_claim_name = {{ $auth.idp.claimMappings.emailClaimName | quote }} + scope_claim_name = {{ $auth.idp.claimMappings.scopeClaimName | quote }} + roles_claim_path = {{ $auth.idp.claimMappings.rolesClaimPath | quote }} + {{- end }} + + [auth.file_based] + enabled = {{ $auth.fileBased.enabled }} + {{- if $auth.fileBased.enabled }} + + [auth.file_based.organization] + id = {{ $auth.fileBased.organization.id | quote }} + display_name = {{ $auth.fileBased.organization.displayName | quote }} + region = {{ $auth.fileBased.organization.region | quote }} + {{- range $auth.fileBased.users }} + + [[auth.file_based.users]] + username = {{ .username | quote }} + password_hash = {{ .passwordHash | quote }} + scopes = {{ .scopes | quote }} + {{- end }} + {{- end }} + + [tls] + enabled = {{ $cfg.tls.enabled }} + cert_dir = {{ $cfg.tls.certDir | quote }} + + {{- if $cfg.cors.allowedOrigins }} + + [cors] + allowed_origins = {{ toJson $cfg.cors.allowedOrigins }} + {{- end }} + + [default_devportal] + enabled = {{ $cfg.defaultDevportal.enabled }} + {{- 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..df94adece6 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/templates/deployment.yaml @@ -0,0 +1,183 @@ +{{- $pa := .Values -}} +{{- $deployment := $pa.deployment -}} +{{- $secrets := $pa.secrets -}} +{{- $tls := $pa.tls -}} +{{- $tlsMounted := and $pa.config.tls.enabled (or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret")) -}} +{{- $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: + - name: APIP_DEMO_MODE + value: {{ .Values.global.developmentMode | toString | quote }} + # Secrets sourced from the external Secret (created by generate-secrets.sh). + - name: ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.encryptionKey }} + {{- if $pa.config.auth.jwt.enabled }} + - name: AUTH_JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.authJwtSecretKey }} + {{- end }} + {{- if eq $pa.config.database.driver "postgres" }} + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secrets.keys.databasePassword }} + {{- 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 + {{- if $tlsMounted }} + - name: tls-certs + mountPath: {{ $pa.config.tls.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 + {{- 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..1ea2ad3349 --- /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.tls.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..bd0e8429a4 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/values-local.yaml @@ -0,0 +1,13 @@ +# Local development values for the platform-api chart. +# Usage (standalone): +# helm install platform-api ./platform-api-helm-chart -f values-local.yaml +# +# Overrides the defaults for local development: +# - developmentMode on (self-signed TLS, relaxed auth/CORS, file-based login) +# - locally-built image (latest tag, IfNotPresent pull policy) +global: + developmentMode: true + +image: + tag: "latest" + pullPolicy: IfNotPresent 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..8fc1da2841 --- /dev/null +++ b/kubernetes/helm/platform-api-helm-chart/values.yaml @@ -0,0 +1,344 @@ +# ============================================================================ +# 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: + developmentMode: true + 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.12.0-SNAPSHOT + pullPolicy: IfNotPresent +# Pull secrets for THIS component only (merged with global.imagePullSecrets). +imagePullSecrets: [] + +# ---------------------------------------------------------------------------- +# config — rendered into /etc/platform-api/config-platform-api.toml +# ---------------------------------------------------------------------------- +# Everything here becomes the app's config file. Secrets (encryption key, JWT +# signing key, DB password, OIDC client secret) are NOT here — they're injected +# as env vars from a Kubernetes Secret; see `secrets:` further down. +config: + # Logging. + logLevel: INFO # DEBUG | INFO | WARN | ERROR + logFormat: json # json | text + # Reject requests whose token lacks the required OAuth2 scope. + enableScopeValidation: true + + # --- Database --- + database: + # Which backend to use: + # sqlite3 — no external DB; the file lives on the PVC (see persistence). + # Single replica only → the HPA cannot be used. Great for demos. + # postgres — external PostgreSQL (configured below). Enables multiple + # replicas and the 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 reachable FROM THE CLUSTER: an in-cluster Service + # (e.g. my-postgresql.db.svc.cluster.local) or an external endpoint. + host: "" + port: 5432 + # Database (schema) name to connect to. (Key is `database` to match the + # gateway chart's DB-config convention.) + database: platformapi + # Login role; its password comes from the Secret (see above). + user: postgres + sslmode: disable # disable | require | verify-ca | verify-full + # Connection-pool tuning: + max_open_conns: 25 # max connections open at once + max_idle_conns: 10 # idle connections kept ready in the pool + conn_max_lifetime: 300 # recycle a connection after this many seconds + + # --- TLS on the Platform API's own listener --- + tls: + # true → the app serves HTTPS using certs from certDir (provisioned via the + # top-level `tls:` block below). + # false → serves plain HTTP; put TLS termination in front (ingress / mesh). + enabled: true + certDir: /app/data/certs + # --- CORS --- + # Browser origins allowed to call the API with credentials. MUST be set to + # explicit origins when developmentMode is false. When empty AND developmentMode + # is true, a wildcard origin is allowed (dev convenience only). + cors: + allowedOrigins: [] + # - https://workspace.example.com + + # --- Authentication --- + # Pick exactly ONE real mode (jwt / idp / file_based). Enabling more than one is + # rejected at startup. + auth: + # jwt — local HMAC-signed tokens issued after file-based login. The usual + # choice for self-hosted setups. Its signing key comes from + # secrets.keys.authJwtSecretKey (required when developmentMode is false). + jwt: + enabled: true + issuer: platform-api + # Skip signature verification. Forced to false when developmentMode is false. + skipValidation: true + # file_based — local username/password login. Rejected when developmentMode + # is false (use idp in production). + fileBased: + enabled: true + organization: + id: default + displayName: Default + region: us + users: + - username: admin + # bcrypt hash of the password "admin". Regenerate with: + # htpasswd -bnBC 12 "" | tr -d ':\n' + passwordHash: "$2y$10$U2yKMwGamGwDoMu0hRPT7u8nCuP8z/qxHFOKV6dhIxkJN9NJ0eVQ." + 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 — validate tokens from an external OIDC provider (Asgardeo, Keycloak, + # Auth0, …) against its JWKS. Use this instead of jwt/file_based in production. + idp: + enabled: false + name: "" + jwksUrl: "" + issuer: [] # accepted token issuers + audience: [] # accepted audiences; empty = don't check audience + validationMode: scope # scope → check scope claim; role → check roles claim + # Map your IDP's token claim names to what the Platform API expects. + claimMappings: + organizationClaimName: organization + orgNameClaimName: org_name + orgHandleClaimName: org_handle + userIdClaimName: sub + usernameClaimName: username + emailClaimName: email + scopeClaimName: scope + rolesClaimPath: "" + + # Seed a default Developer Portal on first start. Leave off for a standalone + # AI Workspace; the Developer Portal package turns this on itself. + defaultDevportal: + enabled: false + +# 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 AUTH_JWT_SECRET_KEY (plus DATABASE_PASSWORD when +# driver=postgres). `keys` maps each logical name to the key inside that Secret. +secrets: + existingSecret: "" + keys: + encryptionKey: ENCRYPTION_KEY + authJwtSecretKey: AUTH_JWT_SECRET_KEY + databasePassword: DATABASE_PASSWORD + +# ---------------------------------------------------------------------------- +# 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 listener's certificate comes from (used when config.tls.enabled) +# ---------------------------------------------------------------------------- +tls: + # cert-manager → a cert-manager Certificate provisions the cert. + # secret → use an existing Secret containing tls.crt / tls.key. + # selfSigned → the app generates a self-signed pair at startup (demo only; + # requires developmentMode=true). + certificateProvider: selfSigned + certManager: + create: true + createIssuer: true + issuerRef: + name: selfsigned-issuer # ignored when createIssuer=true + kind: Issuer + # 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: {} From ee60aa2158fccced5ab8d981a3e400e107f41919 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Thu, 23 Jul 2026 12:38:22 +0530 Subject: [PATCH 2/9] Add workflow for releasing helm charts --- .../ai-workspace-ui-helm-release.yml | 126 ++++++++++++++++++ .../developer-portal-ui-helm-release.yml | 126 ++++++++++++++++++ .../workflows/platform-api-helm-release.yml | 126 ++++++++++++++++++ 3 files changed, 378 insertions(+) create mode 100644 .github/workflows/ai-workspace-ui-helm-release.yml create mode 100644 .github/workflows/developer-portal-ui-helm-release.yml create mode 100644 .github/workflows/platform-api-helm-release.yml 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..85c4db1add --- /dev/null +++ b/.github/workflows/ai-workspace-ui-helm-release.yml @@ -0,0 +1,126 @@ +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. The AI Workspace UI is stateless (React SPA + Go BFF). + +on: + workflow_dispatch: + inputs: + version: + description: 'Chart version (semver, e.g. 0.1.0)' + required: true + type: string + appVersion: + description: 'App version to stamp into Chart.yaml (optional; blank = keep current)' + required: false + type: string + dryRun: + description: 'Package only — do not push, tag, or release' + required: false + type: boolean + default: false + +env: + REGISTRY: ghcr.io + REGISTRY_USERNAME: api-platform-bot + ORGANIZATION: wso2 + REGISTRY_PATH: api-platform/helm-charts + CHART_PATH: kubernetes/helm/ai-workspace-ui-helm-chart + CHART_VERSION: ${{ inputs.version }} + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate version format + 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 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: 'latest' + + - name: Set chart version (and appVersion if provided) + working-directory: ${{ env.CHART_PATH }} + run: | + sed -i "s/^version:.*/version: ${CHART_VERSION}/" Chart.yaml + if [ -n "${{ inputs.appVersion }}" ]; then + sed -i "s/^appVersion:.*/appVersion: \"${{ inputs.appVersion }}\"/" Chart.yaml + fi + CHART_NAME=$(grep '^name:' Chart.yaml | head -1 | awk '{print $2}') + echo "CHART_NAME=${CHART_NAME}" >> "$GITHUB_ENV" + echo "== Chart.yaml =="; grep -E '^(name|version|appVersion):' Chart.yaml + + - name: Check release tag is free + if: ${{ !inputs.dryRun }} + 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 + + - name: Package chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm package . + ls -la *.tgz + + - name: Login to GHCR + if: ${{ !inputs.dryRun }} + run: | + echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login "${REGISTRY}" --username "${REGISTRY_USERNAME}" --password-stdin + + - name: Push chart + if: ${{ !inputs.dryRun }} + working-directory: ${{ env.CHART_PATH }} + run: | + helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}" + + - name: Create and push git tag + if: ${{ !inputs.dryRun }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "helm-${CHART_NAME}-${CHART_VERSION}" + git push origin "refs/tags/helm-${CHART_NAME}-${CHART_VERSION}" + + - name: Create draft GitHub Release + if: ${{ !inputs.dryRun }} + uses: softprops/action-gh-release@v2 + with: + tag_name: helm-${{ env.CHART_NAME }}-${{ inputs.version }} + name: ${{ env.CHART_NAME }} Helm Chart ${{ inputs.version }} + body: | + ## `${{ env.CHART_NAME }}` Helm Chart ${{ inputs.version }} + + ```bash + helm install ${{ env.CHART_NAME }} oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}/${{ env.CHART_NAME }} --version ${{ inputs.version }} + ``` + draft: true + prerelease: ${{ contains(inputs.version, '-') }} + token: ${{ secrets.API_PLATFORM_BOT_TOKEN }} + + - name: Summary + run: | + { + echo "### \`${CHART_NAME}\` Helm chart :package:" + echo "- Version: \`${CHART_VERSION}\`" + echo "- Registry: \`oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME}\`" + [ "${{ inputs.dryRun }}" = "true" ] && echo "- **Dry run** — not pushed." + echo "" + echo "\`\`\`bash" + echo "helm install ${CHART_NAME} oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME} --version ${CHART_VERSION}" + 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..eed3605628 --- /dev/null +++ b/.github/workflows/developer-portal-ui-helm-release.yml @@ -0,0 +1,126 @@ +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. The Developer Portal is a Node.js web app. + +on: + workflow_dispatch: + inputs: + version: + description: 'Chart version (semver, e.g. 0.1.0)' + required: true + type: string + appVersion: + description: 'App version to stamp into Chart.yaml (optional; blank = keep current)' + required: false + type: string + dryRun: + description: 'Package only — do not push, tag, or release' + required: false + type: boolean + default: false + +env: + REGISTRY: ghcr.io + REGISTRY_USERNAME: api-platform-bot + ORGANIZATION: wso2 + REGISTRY_PATH: api-platform/helm-charts + CHART_PATH: kubernetes/helm/developer-portal-ui-helm-chart + CHART_VERSION: ${{ inputs.version }} + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate version format + 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 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: 'latest' + + - name: Set chart version (and appVersion if provided) + working-directory: ${{ env.CHART_PATH }} + run: | + sed -i "s/^version:.*/version: ${CHART_VERSION}/" Chart.yaml + if [ -n "${{ inputs.appVersion }}" ]; then + sed -i "s/^appVersion:.*/appVersion: \"${{ inputs.appVersion }}\"/" Chart.yaml + fi + CHART_NAME=$(grep '^name:' Chart.yaml | head -1 | awk '{print $2}') + echo "CHART_NAME=${CHART_NAME}" >> "$GITHUB_ENV" + echo "== Chart.yaml =="; grep -E '^(name|version|appVersion):' Chart.yaml + + - name: Check release tag is free + if: ${{ !inputs.dryRun }} + 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 + + - name: Package chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm package . + ls -la *.tgz + + - name: Login to GHCR + if: ${{ !inputs.dryRun }} + run: | + echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login "${REGISTRY}" --username "${REGISTRY_USERNAME}" --password-stdin + + - name: Push chart + if: ${{ !inputs.dryRun }} + working-directory: ${{ env.CHART_PATH }} + run: | + helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}" + + - name: Create and push git tag + if: ${{ !inputs.dryRun }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "helm-${CHART_NAME}-${CHART_VERSION}" + git push origin "refs/tags/helm-${CHART_NAME}-${CHART_VERSION}" + + - name: Create draft GitHub Release + if: ${{ !inputs.dryRun }} + uses: softprops/action-gh-release@v2 + with: + tag_name: helm-${{ env.CHART_NAME }}-${{ inputs.version }} + name: ${{ env.CHART_NAME }} Helm Chart ${{ inputs.version }} + body: | + ## `${{ env.CHART_NAME }}` Helm Chart ${{ inputs.version }} + + ```bash + helm install ${{ env.CHART_NAME }} oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}/${{ env.CHART_NAME }} --version ${{ inputs.version }} + ``` + draft: true + prerelease: ${{ contains(inputs.version, '-') }} + token: ${{ secrets.API_PLATFORM_BOT_TOKEN }} + + - name: Summary + run: | + { + echo "### \`${CHART_NAME}\` Helm chart :package:" + echo "- Version: \`${CHART_VERSION}\`" + echo "- Registry: \`oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME}\`" + [ "${{ inputs.dryRun }}" = "true" ] && echo "- **Dry run** — not pushed." + echo "" + echo "\`\`\`bash" + echo "helm install ${CHART_NAME} oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME} --version ${CHART_VERSION}" + 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..580649f30e --- /dev/null +++ b/.github/workflows/platform-api-helm-release.yml @@ -0,0 +1,126 @@ +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 (semver, e.g. 0.1.0)' + required: true + type: string + appVersion: + description: 'App version to stamp into Chart.yaml (optional; blank = keep current)' + required: false + type: string + dryRun: + description: 'Package only — do not push, tag, or release' + required: false + type: boolean + default: false + +env: + REGISTRY: ghcr.io + REGISTRY_USERNAME: api-platform-bot + ORGANIZATION: wso2 + REGISTRY_PATH: api-platform/helm-charts + CHART_PATH: kubernetes/helm/platform-api-helm-chart + CHART_VERSION: ${{ inputs.version }} + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate version format + 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 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: 'latest' + + - name: Set chart version (and appVersion if provided) + working-directory: ${{ env.CHART_PATH }} + run: | + sed -i "s/^version:.*/version: ${CHART_VERSION}/" Chart.yaml + if [ -n "${{ inputs.appVersion }}" ]; then + sed -i "s/^appVersion:.*/appVersion: \"${{ inputs.appVersion }}\"/" Chart.yaml + fi + CHART_NAME=$(grep '^name:' Chart.yaml | head -1 | awk '{print $2}') + echo "CHART_NAME=${CHART_NAME}" >> "$GITHUB_ENV" + echo "== Chart.yaml =="; grep -E '^(name|version|appVersion):' Chart.yaml + + - name: Check release tag is free + if: ${{ !inputs.dryRun }} + 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 + + - name: Package chart + working-directory: ${{ env.CHART_PATH }} + run: | + helm package . + ls -la *.tgz + + - name: Login to GHCR + if: ${{ !inputs.dryRun }} + run: | + echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login "${REGISTRY}" --username "${REGISTRY_USERNAME}" --password-stdin + + - name: Push chart + if: ${{ !inputs.dryRun }} + working-directory: ${{ env.CHART_PATH }} + run: | + helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}" + + - name: Create and push git tag + if: ${{ !inputs.dryRun }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "helm-${CHART_NAME}-${CHART_VERSION}" + git push origin "refs/tags/helm-${CHART_NAME}-${CHART_VERSION}" + + - name: Create draft GitHub Release + if: ${{ !inputs.dryRun }} + uses: softprops/action-gh-release@v2 + with: + tag_name: helm-${{ env.CHART_NAME }}-${{ inputs.version }} + name: ${{ env.CHART_NAME }} Helm Chart ${{ inputs.version }} + body: | + ## `${{ env.CHART_NAME }}` Helm Chart ${{ inputs.version }} + + ```bash + helm install ${{ env.CHART_NAME }} oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}/${{ env.CHART_NAME }} --version ${{ inputs.version }} + ``` + draft: true + prerelease: ${{ contains(inputs.version, '-') }} + token: ${{ secrets.API_PLATFORM_BOT_TOKEN }} + + - name: Summary + run: | + { + echo "### \`${CHART_NAME}\` Helm chart :package:" + echo "- Version: \`${CHART_VERSION}\`" + echo "- Registry: \`oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME}\`" + [ "${{ inputs.dryRun }}" = "true" ] && echo "- **Dry run** — not pushed." + echo "" + echo "\`\`\`bash" + echo "helm install ${CHART_NAME} oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME} --version ${CHART_VERSION}" + echo "\`\`\`" + } >> "$GITHUB_STEP_SUMMARY" From 485ed47331aafe0949801f855f32d8eb0533f1e7 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Thu, 23 Jul 2026 14:44:29 +0530 Subject: [PATCH 3/9] Update versions to the current release versions --- kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml | 6 +++--- kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml | 2 +- kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml | 6 +++--- .../helm/developer-portal-ui-helm-chart/values.yaml | 2 +- kubernetes/helm/platform-api-helm-chart/Chart.yaml | 8 ++++---- kubernetes/helm/platform-api-helm-chart/values.yaml | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml index 764e5944f6..502cf87b45 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml @@ -2,11 +2,11 @@ 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 umbrella + released component chart; consumed by the ai-workspace product package (and pluggable into others) and installable standalone. type: application -version: 0.1.0 -appVersion: "1.1.0-SNAPSHOT" +version: 1.0.0-alpha2 +appVersion: "1.0.0-alpha2" home: https://github.com/wso2/api-platform sources: - https://github.com/wso2/api-platform diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml index 95cd9accea..364e394e14 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml @@ -43,7 +43,7 @@ global: enabled: false image: repository: ghcr.io/wso2/api-platform/ai-workspace - tag: "1.1.0-SNAPSHOT" + tag: "1.0.0-alpha2" pullPolicy: IfNotPresent # Pull secrets for THIS component only (merged with global.imagePullSecrets). imagePullSecrets: [] diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml index 426d4cd598..a0031f7727 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml @@ -2,11 +2,11 @@ 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 umbrella + component chart; consumed by the developer-portal product package (and pluggable into others) and installable standalone. type: application -version: 0.1.0 -appVersion: "1.0.0-beta-SNAPSHOT" +version: 1.0.0-alpha2 +appVersion: "1.0.0-alpha2" home: https://github.com/wso2/api-platform sources: - https://github.com/wso2/api-platform diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml index 02e0a903c9..2c7536fb46 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml @@ -44,7 +44,7 @@ global: enabled: false image: repository: ghcr.io/wso2/api-platform/developer-portal - tag: "1.0.0-beta-SNAPSHOT" + tag: "1.0.0-alpha2" pullPolicy: IfNotPresent # Pull secrets for THIS component only (merged with global.imagePullSecrets). imagePullSecrets: [] diff --git a/kubernetes/helm/platform-api-helm-chart/Chart.yaml b/kubernetes/helm/platform-api-helm-chart/Chart.yaml index dcbddf5c58..2518a0c290 100644 --- a/kubernetes/helm/platform-api-helm-chart/Chart.yaml +++ b/kubernetes/helm/platform-api-helm-chart/Chart.yaml @@ -1,12 +1,12 @@ apiVersion: v2 name: platform-api description: >- - WSO2 API Platform — Platform API control plane (Go backend). Independently - released component chart; consumed by the product umbrella charts + 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.1.0 -appVersion: "0.12.0-SNAPSHOT" +version: 0.12.0 +appVersion: "0.12.0" home: https://github.com/wso2/api-platform sources: - https://github.com/wso2/api-platform diff --git a/kubernetes/helm/platform-api-helm-chart/values.yaml b/kubernetes/helm/platform-api-helm-chart/values.yaml index 8fc1da2841..f70dfa2eba 100644 --- a/kubernetes/helm/platform-api-helm-chart/values.yaml +++ b/kubernetes/helm/platform-api-helm-chart/values.yaml @@ -47,7 +47,7 @@ global: enabled: true image: repository: ghcr.io/wso2/api-platform/platform-api - tag: 0.12.0-SNAPSHOT + tag: 0.12.0 pullPolicy: IfNotPresent # Pull secrets for THIS component only (merged with global.imagePullSecrets). imagePullSecrets: [] From 07e52f84e82e98ebf24d964e87135ec8adb33e8a Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 24 Jul 2026 14:05:59 +0530 Subject: [PATCH 4/9] Update the release workflows to comply with common pattern --- .../ai-workspace-ui-helm-release.yml | 144 +++++++++++------- .../developer-portal-ui-helm-release.yml | 144 +++++++++++------- .../workflows/platform-api-helm-release.yml | 138 ++++++++++------- 3 files changed, 261 insertions(+), 165 deletions(-) diff --git a/.github/workflows/ai-workspace-ui-helm-release.yml b/.github/workflows/ai-workspace-ui-helm-release.yml index 85c4db1add..9776e143a5 100644 --- a/.github/workflows/ai-workspace-ui-helm-release.yml +++ b/.github/workflows/ai-workspace-ui-helm-release.yml @@ -1,126 +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. The AI Workspace UI is stateless (React SPA + Go BFF). +# 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 (semver, e.g. 0.1.0)' + description: 'Chart version (e.g., 0.1.0)' required: true type: string appVersion: - description: 'App version to stamp into Chart.yaml (optional; blank = keep current)' - required: false + description: 'App version (e.g., 1.0.0-alpha2)' + required: true type: string - dryRun: - description: 'Package only — do not push, tag, or release' - required: false - type: boolean - default: false 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_PATH: kubernetes/helm/ai-workspace-ui-helm-chart CHART_VERSION: ${{ inputs.version }} + APP_VERSION: ${{ inputs.appVersion }} jobs: - release: + release-helm-chart: runs-on: ubuntu-latest permissions: contents: write packages: write + steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Validate version format + - 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: Set chart version (and appVersion if provided) + - name: Update Chart version working-directory: ${{ env.CHART_PATH }} run: | sed -i "s/^version:.*/version: ${CHART_VERSION}/" Chart.yaml - if [ -n "${{ inputs.appVersion }}" ]; then - sed -i "s/^appVersion:.*/appVersion: \"${{ inputs.appVersion }}\"/" Chart.yaml - fi - CHART_NAME=$(grep '^name:' Chart.yaml | head -1 | awk '{print $2}') - echo "CHART_NAME=${CHART_NAME}" >> "$GITHUB_ENV" - echo "== Chart.yaml =="; grep -E '^(name|version|appVersion):' Chart.yaml - - - name: Check release tag is free - if: ${{ !inputs.dryRun }} - 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 + sed -i "s/^appVersion:.*/appVersion: \"${APP_VERSION}\"/" Chart.yaml + echo "Updated Chart.yaml:" + cat Chart.yaml - - name: Package chart + - name: Package Helm chart working-directory: ${{ env.CHART_PATH }} run: | helm package . + echo "Packaged chart:" ls -la *.tgz - - name: Login to GHCR - if: ${{ !inputs.dryRun }} + - name: Login to GitHub Container Registry run: | - echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login "${REGISTRY}" --username "${REGISTRY_USERNAME}" --password-stdin + echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login ${{ env.REGISTRY }} --username ${{ env.REGISTRY_USERNAME }} --password-stdin - - name: Push chart - if: ${{ !inputs.dryRun }} + - name: Push Helm chart working-directory: ${{ env.CHART_PATH }} run: | - helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}" + helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}" - - name: Create and push git tag - if: ${{ !inputs.dryRun }} + - 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 tag "helm-${CHART_NAME}-${CHART_VERSION}" - git push origin "refs/tags/helm-${CHART_NAME}-${CHART_VERSION}" + 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 draft GitHub Release - if: ${{ !inputs.dryRun }} + - 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: ${{ env.CHART_NAME }} Helm Chart ${{ inputs.version }} + name: AI Workspace UI Helm Chart ${{ inputs.version }} body: | - ## `${{ env.CHART_NAME }}` Helm Chart ${{ inputs.version }} + ## 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: Summary + + - name: Chart Summary run: | - { - echo "### \`${CHART_NAME}\` Helm chart :package:" - echo "- Version: \`${CHART_VERSION}\`" - echo "- Registry: \`oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME}\`" - [ "${{ inputs.dryRun }}" = "true" ] && echo "- **Dry run** — not pushed." - echo "" - echo "\`\`\`bash" - echo "helm install ${CHART_NAME} oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME} --version ${CHART_VERSION}" - echo "\`\`\`" - } >> "$GITHUB_STEP_SUMMARY" + 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 index eed3605628..3de09174f0 100644 --- a/.github/workflows/developer-portal-ui-helm-release.yml +++ b/.github/workflows/developer-portal-ui-helm-release.yml @@ -1,126 +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. The Developer Portal is a Node.js web app. +# 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 (semver, e.g. 0.1.0)' + description: 'Chart version (e.g., 0.1.0)' required: true type: string appVersion: - description: 'App version to stamp into Chart.yaml (optional; blank = keep current)' - required: false + description: 'App version (e.g., 1.0.0-alpha2)' + required: true type: string - dryRun: - description: 'Package only — do not push, tag, or release' - required: false - type: boolean - default: false 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_PATH: kubernetes/helm/developer-portal-ui-helm-chart CHART_VERSION: ${{ inputs.version }} + APP_VERSION: ${{ inputs.appVersion }} jobs: - release: + release-helm-chart: runs-on: ubuntu-latest permissions: contents: write packages: write + steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Validate version format + - 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: Set chart version (and appVersion if provided) + - name: Update Chart version working-directory: ${{ env.CHART_PATH }} run: | sed -i "s/^version:.*/version: ${CHART_VERSION}/" Chart.yaml - if [ -n "${{ inputs.appVersion }}" ]; then - sed -i "s/^appVersion:.*/appVersion: \"${{ inputs.appVersion }}\"/" Chart.yaml - fi - CHART_NAME=$(grep '^name:' Chart.yaml | head -1 | awk '{print $2}') - echo "CHART_NAME=${CHART_NAME}" >> "$GITHUB_ENV" - echo "== Chart.yaml =="; grep -E '^(name|version|appVersion):' Chart.yaml - - - name: Check release tag is free - if: ${{ !inputs.dryRun }} - 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 + sed -i "s/^appVersion:.*/appVersion: \"${APP_VERSION}\"/" Chart.yaml + echo "Updated Chart.yaml:" + cat Chart.yaml - - name: Package chart + - name: Package Helm chart working-directory: ${{ env.CHART_PATH }} run: | helm package . + echo "Packaged chart:" ls -la *.tgz - - name: Login to GHCR - if: ${{ !inputs.dryRun }} + - name: Login to GitHub Container Registry run: | - echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login "${REGISTRY}" --username "${REGISTRY_USERNAME}" --password-stdin + echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login ${{ env.REGISTRY }} --username ${{ env.REGISTRY_USERNAME }} --password-stdin - - name: Push chart - if: ${{ !inputs.dryRun }} + - name: Push Helm chart working-directory: ${{ env.CHART_PATH }} run: | - helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}" + helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}" - - name: Create and push git tag - if: ${{ !inputs.dryRun }} + - 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 tag "helm-${CHART_NAME}-${CHART_VERSION}" - git push origin "refs/tags/helm-${CHART_NAME}-${CHART_VERSION}" + 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 draft GitHub Release - if: ${{ !inputs.dryRun }} + - 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: ${{ env.CHART_NAME }} Helm Chart ${{ inputs.version }} + name: Developer Portal UI Helm Chart ${{ inputs.version }} body: | - ## `${{ env.CHART_NAME }}` Helm Chart ${{ inputs.version }} + ## 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: Summary + + - name: Chart Summary run: | - { - echo "### \`${CHART_NAME}\` Helm chart :package:" - echo "- Version: \`${CHART_VERSION}\`" - echo "- Registry: \`oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME}\`" - [ "${{ inputs.dryRun }}" = "true" ] && echo "- **Dry run** — not pushed." - echo "" - echo "\`\`\`bash" - echo "helm install ${CHART_NAME} oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME} --version ${CHART_VERSION}" - echo "\`\`\`" - } >> "$GITHUB_STEP_SUMMARY" + 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 index 580649f30e..fa7ae8b511 100644 --- a/.github/workflows/platform-api-helm-release.yml +++ b/.github/workflows/platform-api-helm-release.yml @@ -8,119 +8,151 @@ on: workflow_dispatch: inputs: version: - description: 'Chart version (semver, e.g. 0.1.0)' + description: 'Chart version (e.g., 0.1.0)' required: true type: string appVersion: - description: 'App version to stamp into Chart.yaml (optional; blank = keep current)' - required: false + description: 'App version (e.g., 0.12.0)' + required: true type: string - dryRun: - description: 'Package only — do not push, tag, or release' - required: false - type: boolean - default: false 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_PATH: kubernetes/helm/platform-api-helm-chart CHART_VERSION: ${{ inputs.version }} + APP_VERSION: ${{ inputs.appVersion }} jobs: - release: + release-helm-chart: runs-on: ubuntu-latest permissions: contents: write packages: write + steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Validate version format + - 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: Set chart version (and appVersion if provided) + - name: Update Chart version working-directory: ${{ env.CHART_PATH }} run: | sed -i "s/^version:.*/version: ${CHART_VERSION}/" Chart.yaml - if [ -n "${{ inputs.appVersion }}" ]; then - sed -i "s/^appVersion:.*/appVersion: \"${{ inputs.appVersion }}\"/" Chart.yaml - fi - CHART_NAME=$(grep '^name:' Chart.yaml | head -1 | awk '{print $2}') - echo "CHART_NAME=${CHART_NAME}" >> "$GITHUB_ENV" - echo "== Chart.yaml =="; grep -E '^(name|version|appVersion):' Chart.yaml - - - name: Check release tag is free - if: ${{ !inputs.dryRun }} - 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 + sed -i "s/^appVersion:.*/appVersion: \"${APP_VERSION}\"/" Chart.yaml + echo "Updated Chart.yaml:" + cat Chart.yaml - - name: Package chart + - name: Package Helm chart working-directory: ${{ env.CHART_PATH }} run: | helm package . + echo "Packaged chart:" ls -la *.tgz - - name: Login to GHCR - if: ${{ !inputs.dryRun }} + - name: Login to GitHub Container Registry run: | - echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login "${REGISTRY}" --username "${REGISTRY_USERNAME}" --password-stdin + echo "${{ secrets.API_PLATFORM_BOT_TOKEN }}" | helm registry login ${{ env.REGISTRY }} --username ${{ env.REGISTRY_USERNAME }} --password-stdin - - name: Push chart - if: ${{ !inputs.dryRun }} + - name: Push Helm chart working-directory: ${{ env.CHART_PATH }} run: | - helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}" + helm push "${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${{ env.REGISTRY }}/${{ env.ORGANIZATION }}/${{ env.REGISTRY_PATH }}" - - name: Create and push git tag - if: ${{ !inputs.dryRun }} + - 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 tag "helm-${CHART_NAME}-${CHART_VERSION}" - git push origin "refs/tags/helm-${CHART_NAME}-${CHART_VERSION}" + 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 draft GitHub Release - if: ${{ !inputs.dryRun }} + - 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: ${{ env.CHART_NAME }} Helm Chart ${{ inputs.version }} + name: Platform API Helm Chart ${{ inputs.version }} body: | - ## `${{ env.CHART_NAME }}` Helm Chart ${{ inputs.version }} + ## 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: Summary + + - name: Chart Summary run: | - { - echo "### \`${CHART_NAME}\` Helm chart :package:" - echo "- Version: \`${CHART_VERSION}\`" - echo "- Registry: \`oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME}\`" - [ "${{ inputs.dryRun }}" = "true" ] && echo "- **Dry run** — not pushed." - echo "" - echo "\`\`\`bash" - echo "helm install ${CHART_NAME} oci://${REGISTRY}/${ORGANIZATION}/${REGISTRY_PATH}/${CHART_NAME} --version ${CHART_VERSION}" - echo "\`\`\`" - } >> "$GITHUB_STEP_SUMMARY" + 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 From 9353a8fc2bf71d58ccf331f9ec799643603ff315 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 24 Jul 2026 14:14:10 +0530 Subject: [PATCH 5/9] Add missing configs to conform to current config catalog --- .../templates/_helpers.tpl | 23 +- .../templates/configmap.yaml | 83 ++++-- .../templates/deployment.yaml | 41 +-- .../values-local.yaml | 16 +- .../ai-workspace-ui-helm-chart/values.yaml | 120 +++++---- .../templates/_helpers.tpl | 23 +- .../templates/configmap.yaml | 130 ++++++++- .../templates/deployment.yaml | 116 +++----- .../values-local.yaml | 16 +- .../values.yaml | 132 +++++++--- .../templates/_helpers.tpl | 23 +- .../templates/certificate.yaml | 7 +- .../templates/configmap.yaml | 192 ++++++++++---- .../templates/deployment.yaml | 59 ++++- .../templates/issuer.yaml | 2 +- .../platform-api-helm-chart/values-local.yaml | 14 +- .../helm/platform-api-helm-chart/values.yaml | 249 +++++++++++------- 17 files changed, 810 insertions(+), 436 deletions(-) diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl index 30f5d5cdec..0dbb30bf87 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/_helpers.tpl @@ -2,11 +2,11 @@ {{/* Shared helpers for the api-platform-portals suite. -Cross-cutting configuration (developmentMode, 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. +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 @@ -24,14 +24,17 @@ in-cluster Service name even though it lives in a different subchart. {{- 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 63 | trimSuffix "-" -}} +{{- 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 63 | trimSuffix "-" -}} +{{- 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 63 | trimSuffix "-" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" . | trunc 45 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- define "apip.chart" -}} @@ -137,7 +140,9 @@ subchart's own values. {{- define "apip.platformApi.internalURL" -}} {{- $g := default (dict) .Values.global -}} {{- $pa := default (dict) $g.platformApi -}} -{{- $scheme := ternary "https" "http" (default true $pa.tlsEnabled) -}} +{{- $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 -}} diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml index 7f4327b622..16b6916df2 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml @@ -1,5 +1,8 @@ {{- $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 @@ -12,36 +15,72 @@ metadata: {{- . | 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: | - domain = {{ $cfg.domain | quote }} + [ai_workspace] default_org_region = {{ $cfg.defaultOrgRegion | quote }} - auth_mode = {{ $cfg.authMode | quote }} - {{- with $cfg.controlplaneHost }} - controlplane_host = {{ . | quote }} - {{- end }} - platform_gateway_versions = {{ $cfg.platformGatewayVersions | quote }} - {{- if eq $cfg.authMode "oidc" }} - - # --- OIDC (client secret is injected via environment, never written here) --- - oidc_authority = {{ $cfg.oidc.authority | quote }} - oidc_client_id = {{ $cfg.oidc.clientId | quote }} - {{- with $cfg.oidc.redirectUrl }} - oidc_redirect_url = {{ . | quote }} + + [ai_workspace.server] + domain = {{ $domain | quote }} + 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.oidc.postLogoutRedirectUrl }} - oidc_post_logout_redirect_url = {{ . | quote }} + {{- with $cfg.auth.oidc.postLogoutRedirectUrl }} + post_logout_redirect_url = {{ . | quote }} {{- end }} - {{- with $cfg.oidc.scope }} - oidc_scope = {{ . | quote }} + {{- with $cfg.auth.oidc.scope }} + scope = {{ . | quote }} {{- end }} - oidc_username_claim = {{ $cfg.oidc.usernameClaim | quote }} - oidc_org_id_claim = {{ $cfg.oidc.orgIdClaim | quote }} - oidc_org_name_claim = {{ $cfg.oidc.orgNameClaim | quote }} - oidc_org_handle_claim = {{ $cfg.oidc.orgHandleClaim | quote }} {{- 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 index 52c9e449d8..d32fb989e1 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml @@ -1,11 +1,10 @@ {{- $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") -}} -{{- $platformApiUrl := default (include "apip.platformApi.internalURL" .) $ui.platformApi.url -}} -{{- $skipVerify := and $ui.platformApi.tlsSkipVerify .Values.global.developmentMode -}} {{- $imageTag := default .Chart.AppVersion $ui.image.tag -}} {{- if $deployment.enabled }} apiVersion: apps/v1 @@ -90,41 +89,27 @@ spec: securityContext: {{- toYaml . | nindent 12 }} {{- end }} + {{- if eq $cfg.auth.mode "oidc" }} env: - - name: BFF_ADDR - value: {{ printf ":%d" (int $ui.containerPort) | quote }} - - name: BFF_CONFIG_FILE - value: /etc/ai-workspace/config.toml - - name: PLATFORM_API_URL - value: {{ $platformApiUrl | quote }} - - name: PLATFORM_API_TLS_SKIP_VERIFY - value: {{ $skipVerify | toString | quote }} - {{- with $ui.platformApi.caFile }} - - name: PLATFORM_API_CA_FILE - value: {{ . | quote }} - {{- end }} - - name: SESSION_STORE - value: {{ $ui.sessionStore | quote }} - - name: COOKIE_SECURE - value: {{ $ui.cookie.secure | toString | quote }} - - name: COOKIE_SAMESITE - value: {{ $ui.cookie.sameSite | quote }} - - name: BFF_TLS_SELF_SIGNED - value: {{ $selfSigned | toString | quote }} - - name: VITE_AUTH_MODE - value: {{ $ui.config.authMode | quote }} - {{- if eq $ui.config.authMode "oidc" }} - # OIDC client secret from the external UI Secret (generate-secrets.sh). - # The secretName helper fails the render if ui.secrets.existingSecret is unset. - - name: OIDC_CLIENT_SECRET + # 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 }} diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml index 7e6a6ad964..69aa259dee 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values-local.yaml @@ -1,13 +1,17 @@ # Local development values for the ai-workspace-ui chart. -# Usage (standalone): -# helm install ai-workspace-ui ./ai-workspace-ui-helm-chart -f values-local.yaml +# 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: -# - developmentMode on (self-signed TLS, basic/file-based login) # - locally-built image (latest tag, IfNotPresent pull policy) -global: - developmentMode: true - +# - 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 index 364e394e14..fdc56a7b62 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml @@ -16,7 +16,6 @@ # global — STANDALONE defaults only (an umbrella's global.* overrides these) # ---------------------------------------------------------------------------- global: - developmentMode: true nameOverride: "" fullnameOverride: "" commonLabels: {} @@ -49,63 +48,82 @@ image: imagePullSecrets: [] # ---------------------------------------------------------------------------- -# config — rendered into /etc/ai-workspace/config.toml +# 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: - # Hostname (or host:port) the browser uses to reach the UI. Shown in the address - # bar and used to build redirect URLs. - domain: ai-workspace.localhost # Region assigned to a new organization the first time someone logs in. defaultOrgRegion: us - # How users sign in: - # basic → the Platform API's file-based login (admin/admin in dev) - # oidc → an external OIDC provider (fill in config.oidc below + the secret) - authMode: basic - # Public address of the control plane that GATEWAYS dial to reach the Platform - # API. Shown verbatim in the in-UI "add a gateway" instructions. - controlplaneHost: "" - # Gateway build options offered in the create-gateway dropdown (a JSON string). - platformGatewayVersions: '[{"version":"1.2","latestVersion":"v1.2.0-M1","channel":"STS"}]' - # OIDC settings — used only when authMode=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" - # Map the IDP's token claims to what the UI expects: - usernameClaim: username - orgIdClaim: org_id - orgNameClaim: org_name - orgHandleClaim: org_handle -# Escape hatch: raw TOML appended verbatim to the generated config.toml. -configToml: "" + 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: "" -# ---------------------------------------------------------------------------- -# platformApi — how the BFF reaches the Platform API (server-to-server) -# ---------------------------------------------------------------------------- -platformApi: - # 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. Forced to false when developmentMode is false. - tlsSkipVerify: true - # Instead of skipping verification, mount a CA file (via extraVolumes/Mounts) - # and point to it here to properly verify the Platform API's certificate. - caFile: "" + logging: + level: info # debug | info | warn | error + format: text # text | json + browserDebug: false # verbose console logging in the SPA -# ---------------------------------------------------------------------------- -# Session cookie & store -# ---------------------------------------------------------------------------- -cookie: - secure: true # keep true when served over HTTPS - sameSite: lax # lax | strict | none -# Where server-side sessions live: "memory" works for a single replica; use an -# external store if you scale the UI to multiple replicas. -sessionStore: memory + # 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) diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl b/kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl index 30f5d5cdec..0dbb30bf87 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/_helpers.tpl @@ -2,11 +2,11 @@ {{/* Shared helpers for the api-platform-portals suite. -Cross-cutting configuration (developmentMode, 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. +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 @@ -24,14 +24,17 @@ in-cluster Service name even though it lives in a different subchart. {{- 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 63 | trimSuffix "-" -}} +{{- 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 63 | trimSuffix "-" -}} +{{- 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 63 | trimSuffix "-" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" . | trunc 45 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- define "apip.chart" -}} @@ -137,7 +140,9 @@ subchart's own values. {{- define "apip.platformApi.internalURL" -}} {{- $g := default (dict) .Values.global -}} {{- $pa := default (dict) $g.platformApi -}} -{{- $scheme := ternary "https" "http" (default true $pa.tlsEnabled) -}} +{{- $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 -}} diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml index 8f89d9bb1d..0c9afc1ca9 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yaml @@ -1,5 +1,13 @@ {{- $dp := .Values -}} -{{- if and $dp.deployment.enabled $dp.configToml }} +{{- $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: @@ -11,9 +19,121 @@ metadata: {{- . | nindent 4 }} {{- end }} data: - # Optional raw config.toml. The Developer Portal is configured primarily via - # APIP_DP_* env vars (set on the Deployment); anything here is overridden by them. + # 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: | - {{- $dp.configToml | nindent 4 }} -{{- end }} + [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 index 174107ac73..c2c95bc3b9 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/templates/deployment.yaml @@ -1,6 +1,7 @@ {{- $dp := .Values -}} {{- $deployment := $dp.deployment -}} {{- $cfg := $dp.config -}} +{{- $auth := $cfg.auth -}} {{- $secrets := $dp.secrets -}} {{- $tls := $dp.tls -}} {{- $selfSigned := eq $tls.certificateProvider "selfSigned" -}} @@ -8,10 +9,7 @@ {{- $secretName := include "apip.developerPortal.secretName" . -}} {{- $imageTag := default .Chart.AppVersion $dp.image.tag -}} {{- $certDir := $tls.mountPath -}} -{{- $baseUrl := default (printf "https://localhost:%d" (int $dp.containerPort)) $cfg.server.baseUrl -}} -{{- $platformApiUrl := default (include "apip.platformApi.internalURL" .) $cfg.platformApi.baseUrl -}} -{{- $demo := $cfg.demoEnabled -}} -{{- if kindIs "invalid" $demo }}{{- $demo = .Values.global.developmentMode -}}{{- end }} +{{- $jwtKeyDir := regexReplaceAll "/[^/]+$" $auth.publicKeyPath "" -}} {{- if $deployment.enabled }} apiVersion: apps/v1 kind: Deployment @@ -94,106 +92,40 @@ spec: {{- toYaml . | nindent 12 }} {{- end }} env: - # ── Server / TLS ────────────────────────────────────────────── - - name: APIP_DP_SERVER_PORT - value: {{ $dp.containerPort | toString | quote }} - - name: APIP_DP_SERVER_BASEURL - value: {{ $baseUrl | quote }} - - name: APIP_DP_TLS_ENABLED - value: "true" - - name: APIP_DP_TLS_CERTFILE - value: {{ printf "%s/server.crt" $certDir | quote }} - - name: APIP_DP_TLS_KEYFILE - value: {{ printf "%s/server.key" $certDir | quote }} - - name: APIP_DP_TLS_CAFILE - value: {{ printf "%s/server.crt" $certDir | quote }} - # ── Database ────────────────────────────────────────────────── - - name: APIP_DP_DATABASE_TYPE - value: {{ $cfg.database.type | quote }} - {{- if eq $cfg.database.type "postgres" }} - - name: APIP_DP_DATABASE_HOST - value: {{ $cfg.database.host | quote }} - - name: APIP_DP_DATABASE_PORT - value: {{ $cfg.database.port | toString | quote }} - - name: APIP_DP_DATABASE_NAME - value: {{ $cfg.database.database | quote }} - - name: APIP_DP_DATABASE_USERNAME - value: {{ $cfg.database.user | quote }} - - name: APIP_DP_DATABASE_PASSWORD + # 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.databasePassword | default "APIP_DP_DATABASE_PASSWORD" }} - {{- else }} - - name: APIP_DP_DATABASE_FILE - value: {{ $cfg.database.file | quote }} - {{- end }} - # ── Behaviour ───────────────────────────────────────────────── - - name: APIP_DP_DEMO_ENABLED - value: {{ $demo | toString | quote }} - - name: APIP_DP_LOGGING_CONSOLEONLY - value: {{ $cfg.logging.consoleOnly | toString | quote }} - - name: APIP_DP_SECURITY_ROLEVALIDATION - value: {{ $cfg.security.roleValidation | toString | quote }} - - name: APIP_DP_SECURITY_SERVICEAPIKEY_ENABLED - value: {{ $cfg.security.serviceApiKey.enabled | toString | quote }} - - name: APIP_DP_SECURITY_SERVICEAPIKEY_HEADERNAME - value: {{ $cfg.security.serviceApiKey.headerName | quote }} - - name: APIP_DP_ORGANIZATION_DEFAULTNAME - value: {{ $cfg.organization.defaultName | quote }} - # ── Platform API connectivity ──────────────────────────────── - - name: APIP_DP_PLATFORMAPI_BASEURL - value: {{ $platformApiUrl | quote }} - - name: APIP_DP_PLATFORMAPI_INSECURE - value: {{ $cfg.platformApi.insecure | toString | quote }} - # ── Secrets (from the external Secret) ──────────────────────── - - name: APIP_DP_SECURITY_ENCRYPTIONKEY + key: {{ $secrets.keys.encryptionKey }} + - name: APIP_DP_SECURITY_SESSION_SECRET valueFrom: secretKeyRef: name: {{ $secretName }} - key: {{ $secrets.keys.encryptionKey }} - - name: APIP_DP_PLATFORMAPI_JWTSECRET + key: {{ $secrets.keys.sessionSecret }} + {{- if eq $cfg.database.type "postgres" }} + - name: APIP_DP_DATABASE_PASSWORD valueFrom: secretKeyRef: name: {{ $secretName }} - key: {{ $secrets.keys.platformApiJwtSecret }} + key: {{ $secrets.keys.databasePassword }} + {{- end }} {{- if $secrets.hasServiceApiKeyValue }} - - name: APIP_DP_SECURITY_SERVICEAPIKEY_VALUE + - name: APIP_DP_SECURITY_SERVICE_API_KEY_VALUE valueFrom: secretKeyRef: name: {{ $secretName }} key: {{ $secrets.keys.serviceApiKeyValue }} {{- end }} - # ── OIDC IDP (active when clientId is set) ──────────────────── - {{- if $cfg.idp.clientId }} - - name: APIP_DP_IDP_NAME - value: {{ $cfg.idp.name | quote }} - - name: APIP_DP_IDP_ISSUER - value: {{ $cfg.idp.issuer | quote }} - - name: APIP_DP_IDP_AUTHORIZATIONURL - value: {{ $cfg.idp.authorizationUrl | quote }} - - name: APIP_DP_IDP_TOKENURL - value: {{ $cfg.idp.tokenUrl | quote }} - - name: APIP_DP_IDP_USERINFOURL - value: {{ $cfg.idp.userInfoUrl | quote }} - - name: APIP_DP_IDP_JWKSURL - value: {{ $cfg.idp.jwksUrl | quote }} - - name: APIP_DP_IDP_CLIENTID - value: {{ $cfg.idp.clientId | quote }} - - name: APIP_DP_IDP_CALLBACKURL - value: {{ $cfg.idp.callbackUrl | quote }} - - name: APIP_DP_IDP_LOGOUTURL - value: {{ $cfg.idp.logoutUrl | quote }} - - name: APIP_DP_IDP_SCOPE - value: {{ $cfg.idp.scope | quote }} - {{- if $secrets.hasIdpClientSecret }} - - name: APIP_DP_IDP_CLIENTSECRET + {{- if and (ne $auth.idp.clientId "") $secrets.hasIdpClientSecret }} + - name: APIP_DP_AUTH_IDP_CLIENT_SECRET valueFrom: secretKeyRef: name: {{ $secretName }} key: {{ $secrets.keys.idpClientSecret }} {{- end }} - {{- end }} {{- range $deployment.extraEnv }} - {{- toYaml . | nindent 14 }} {{- end }} @@ -223,10 +155,13 @@ spec: {{- if $tlsFromSecret }} readOnly: true {{- end }} - {{- if $dp.configToml }} - 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 }} @@ -256,13 +191,22 @@ spec: # selfSigned: writable dir where the entrypoint generates server.crt/server.key. emptyDir: {} {{- end }} - {{- if $dp.configToml }} - 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 }} diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml index 6c978f2b00..f38660a981 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml @@ -1,13 +1,17 @@ # Local development values for the developer-portal-ui chart. -# Usage (standalone): -# helm install developer-portal-ui ./developer-portal-ui-helm-chart -f values-local.yaml +# 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: -# - developmentMode on (self-signed TLS, sample-API seeding, file-based login) # - locally-built image (latest tag, IfNotPresent pull policy) -global: - developmentMode: true - +# - 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 diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml index 2c7536fb46..30c564137f 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml @@ -17,7 +17,6 @@ # global — STANDALONE defaults only (an umbrella's global.* overrides these) # ---------------------------------------------------------------------------- global: - developmentMode: true nameOverride: "" fullnameOverride: "" commonLabels: {} @@ -50,7 +49,9 @@ image: imagePullSecrets: [] # ---------------------------------------------------------------------------- -# config — becomes APIP_DP_* env vars on the container +# 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: @@ -58,15 +59,14 @@ config: # 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: "" - # Demo features (sample-API seeding, onboarding prompts). null → follow - # global.developmentMode; set true/false to force. - demoEnabled: null 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. Good for demos. + # 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. @@ -76,9 +76,16 @@ config: # for sqlite. host: "" port: 5432 - database: devportal # database (schema) name (gateway-convention key) + 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: @@ -95,28 +102,75 @@ config: # 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 (needed while the - # Platform API uses a self-signed cert). Forced false when developmentMode - # is false. - insecure: true + # 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 - # --- OIDC login (optional) --- - # Used when idp.clientId is set. 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" + # --- 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 + 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: "" @@ -126,21 +180,25 @@ configToml: "" # ---------------------------------------------------------------------------- # 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: -# APIP_DP_SECURITY_ENCRYPTIONKEY and APIP_DP_PLATFORMAPI_JWTSECRET (the latter = -# the Platform API's AUTH_JWT_SECRET_KEY, so the portal can verify its tokens). -# The optional keys (OIDC client secret, service API key, DB password) are only -# wired when the matching has* flag below is true. +# 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: APIP_DP_SECURITY_ENCRYPTIONKEY - platformApiJwtSecret: APIP_DP_PLATFORMAPI_JWTSECRET - idpClientSecret: APIP_DP_IDP_CLIENTSECRET - serviceApiKeyValue: APIP_DP_SECURITY_SERVICEAPIKEY_VALUE - databasePassword: APIP_DP_DATABASE_PASSWORD # used only when database.type=postgres + 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 as an env var. generate-secrets.sh sets these for - # you in the values-secrets.yaml it writes. + # 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 diff --git a/kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl b/kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl index 30f5d5cdec..0dbb30bf87 100644 --- a/kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl +++ b/kubernetes/helm/platform-api-helm-chart/templates/_helpers.tpl @@ -2,11 +2,11 @@ {{/* Shared helpers for the api-platform-portals suite. -Cross-cutting configuration (developmentMode, 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. +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 @@ -24,14 +24,17 @@ in-cluster Service name even though it lives in a different subchart. {{- 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 63 | trimSuffix "-" -}} +{{- 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 63 | trimSuffix "-" -}} +{{- 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 63 | trimSuffix "-" -}} +{{- printf "%s-developer-portal" (include "apip.fullname" . | trunc 45 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- define "apip.chart" -}} @@ -137,7 +140,9 @@ subchart's own values. {{- define "apip.platformApi.internalURL" -}} {{- $g := default (dict) .Values.global -}} {{- $pa := default (dict) $g.platformApi -}} -{{- $scheme := ternary "https" "http" (default true $pa.tlsEnabled) -}} +{{- $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 -}} diff --git a/kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml b/kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml index aaa54f0eb8..ed3fd6e78a 100644 --- a/kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml +++ b/kubernetes/helm/platform-api-helm-chart/templates/certificate.yaml @@ -1,6 +1,11 @@ {{- $pa := .Values -}} {{- $tls := $pa.tls -}} -{{- if and $pa.deployment.enabled $pa.config.tls.enabled (eq $tls.certificateProvider "cert-manager") $tls.certManager.create }} +{{- 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: diff --git a/kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml b/kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml index 1636ef1802..5e704fafe8 100644 --- a/kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml +++ b/kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml @@ -2,6 +2,8 @@ {{- $cfg := $pa.config -}} {{- $db := $cfg.database -}} {{- $auth := $cfg.auth -}} +{{- $srv := $cfg.server -}} +{{- $https := $srv.https -}} {{- if $pa.deployment.enabled }} apiVersion: v1 kind: ConfigMap @@ -14,86 +16,160 @@ metadata: {{- . | 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: | - log_level = {{ $cfg.logLevel | quote }} - log_format = {{ $cfg.logFormat | quote }} - port = {{ $pa.containerPort | toString | quote }} - enable_scope_validation = {{ $cfg.enableScopeValidation }} + [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 }} - [database] + [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 "postgres" }} - host = {{ $db.postgres.host | quote }} - port = {{ $db.postgres.port }} - name = {{ $db.postgres.database | quote }} - user = {{ $db.postgres.user | quote }} - ssl_mode = {{ $db.postgres.sslmode | quote }} - max_open_conns = {{ $db.postgres.max_open_conns }} - max_idle_conns = {{ $db.postgres.max_idle_conns }} - conn_max_lifetime = {{ $db.postgres.conn_max_lifetime }} - {{- else }} + {{- 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 }} - [auth.jwt] - enabled = {{ $auth.jwt.enabled }} + [platform_api.auth.jwt] issuer = {{ $auth.jwt.issuer | quote }} - skip_validation = {{ if .Values.global.developmentMode }}{{ $auth.jwt.skipValidation }}{{ else }}false{{ end }} + public_key_file = {{ $auth.jwt.publicKeyFile | quote }} + private_key_file = {{ $auth.jwt.privateKeyFile | quote }} + token_ttl = {{ $auth.jwt.tokenTtl | quote }} - [auth.idp] - enabled = {{ $auth.idp.enabled }} - {{- if $auth.idp.enabled }} - {{- with $auth.idp.name }} - name = {{ . | quote }} - {{- end }} - jwks_url = {{ $auth.idp.jwksUrl | 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 }} - - [auth.idp.claim_mappings] - organization_claim_name = {{ $auth.idp.claimMappings.organizationClaimName | quote }} - org_name_claim_name = {{ $auth.idp.claimMappings.orgNameClaimName | quote }} - org_handle_claim_name = {{ $auth.idp.claimMappings.orgHandleClaimName | quote }} - user_id_claim_name = {{ $auth.idp.claimMappings.userIdClaimName | quote }} - username_claim_name = {{ $auth.idp.claimMappings.usernameClaimName | quote }} - email_claim_name = {{ $auth.idp.claimMappings.emailClaimName | quote }} - scope_claim_name = {{ $auth.idp.claimMappings.scopeClaimName | quote }} - roles_claim_path = {{ $auth.idp.claimMappings.rolesClaimPath | quote }} + role_mappings = {{ $auth.idp.roleMappings | quote }} {{- end }} - [auth.file_based] - enabled = {{ $auth.fileBased.enabled }} - {{- if $auth.fileBased.enabled }} + {{- if eq $auth.mode "file" }} - [auth.file_based.organization] - id = {{ $auth.fileBased.organization.id | quote }} - display_name = {{ $auth.fileBased.organization.displayName | quote }} - region = {{ $auth.fileBased.organization.region | quote }} - {{- range $auth.fileBased.users }} + [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 }} - [[auth.file_based.users]] - username = {{ .username | quote }} - password_hash = {{ .passwordHash | quote }} - scopes = {{ .scopes | quote }} - {{- end }} + [[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 }} - [tls] - enabled = {{ $cfg.tls.enabled }} - cert_dir = {{ $cfg.tls.certDir | quote }} + [platform_api.server.http] + enabled = {{ $srv.http.enabled }} + port = {{ $srv.http.port | int }} - {{- if $cfg.cors.allowedOrigins }} + [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 }} - [cors] - allowed_origins = {{ toJson $cfg.cors.allowedOrigins }} - {{- end }} + [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 }} - [default_devportal] - enabled = {{ $cfg.defaultDevportal.enabled }} + [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 index df94adece6..a28018a997 100644 --- a/kubernetes/helm/platform-api-helm-chart/templates/deployment.yaml +++ b/kubernetes/helm/platform-api-helm-chart/templates/deployment.yaml @@ -2,7 +2,13 @@ {{- $deployment := $pa.deployment -}} {{- $secrets := $pa.secrets -}} {{- $tls := $pa.tls -}} -{{- $tlsMounted := and $pa.config.tls.enabled (or (eq $tls.certificateProvider "cert-manager") (eq $tls.certificateProvider "secret")) -}} +{{- $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 @@ -91,27 +97,40 @@ spec: - "-config" - "/etc/platform-api/config-platform-api.toml" env: - - name: APIP_DEMO_MODE - value: {{ .Values.global.developmentMode | toString | quote }} - # Secrets sourced from the external Secret (created by generate-secrets.sh). - - name: ENCRYPTION_KEY + # 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 $pa.config.auth.jwt.enabled }} - - name: AUTH_JWT_SECRET_KEY + {{- if ne $pa.config.database.driver "sqlite3" }} + - name: APIP_CP_DATABASE_PASSWORD valueFrom: secretKeyRef: name: {{ $secretName }} - key: {{ $secrets.keys.authJwtSecretKey }} + key: {{ $secrets.keys.databasePassword }} {{- end }} - {{- if eq $pa.config.database.driver "postgres" }} - - name: DATABASE_PASSWORD + {{- if $pa.config.webhook.enabled }} + - name: APIP_CP_WEBHOOK_SECRET valueFrom: secretKeyRef: name: {{ $secretName }} - key: {{ $secrets.keys.databasePassword }} + 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 }} @@ -140,9 +159,12 @@ spec: - 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: {{ $pa.config.tls.certDir }} + mountPath: {{ $https.certDir }} readOnly: true {{- end }} {{- with $deployment.extraVolumeMounts }} @@ -162,6 +184,19 @@ spec: 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: diff --git a/kubernetes/helm/platform-api-helm-chart/templates/issuer.yaml b/kubernetes/helm/platform-api-helm-chart/templates/issuer.yaml index 1ea2ad3349..1e729edca8 100644 --- a/kubernetes/helm/platform-api-helm-chart/templates/issuer.yaml +++ b/kubernetes/helm/platform-api-helm-chart/templates/issuer.yaml @@ -1,6 +1,6 @@ {{- $pa := .Values -}} {{- $tls := $pa.tls -}} -{{- if and $pa.deployment.enabled $pa.config.tls.enabled (eq $tls.certificateProvider "cert-manager") }} +{{- 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 diff --git a/kubernetes/helm/platform-api-helm-chart/values-local.yaml b/kubernetes/helm/platform-api-helm-chart/values-local.yaml index bd0e8429a4..6a03644516 100644 --- a/kubernetes/helm/platform-api-helm-chart/values-local.yaml +++ b/kubernetes/helm/platform-api-helm-chart/values-local.yaml @@ -1,13 +1,15 @@ # Local development values for the platform-api chart. -# Usage (standalone): -# helm install platform-api ./platform-api-helm-chart -f values-local.yaml +# 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: -# - developmentMode on (self-signed TLS, relaxed auth/CORS, file-based login) # - locally-built image (latest tag, IfNotPresent pull policy) -global: - developmentMode: true - +# - 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 index f70dfa2eba..9c69aef75e 100644 --- a/kubernetes/helm/platform-api-helm-chart/values.yaml +++ b/kubernetes/helm/platform-api-helm-chart/values.yaml @@ -18,7 +18,6 @@ # global — STANDALONE defaults only (an umbrella's global.* overrides these) # ---------------------------------------------------------------------------- global: - developmentMode: true nameOverride: "" fullnameOverride: "" commonLabels: {} @@ -53,25 +52,38 @@ image: imagePullSecrets: [] # ---------------------------------------------------------------------------- -# config — rendered into /etc/platform-api/config-platform-api.toml +# 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, JWT -# signing key, DB password, OIDC client secret) are NOT here — they're injected -# as env vars from a Kubernetes Secret; see `secrets:` further down. +# 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: - # Logging. - logLevel: INFO # DEBUG | INFO | WARN | ERROR - logFormat: json # json | text - # Reject requests whose token lacks the required OAuth2 scope. - enableScopeValidation: true + # 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: - # Which backend to use: - # sqlite3 — no external DB; the file lives on the PVC (see persistence). - # Single replica only → the HPA cannot be used. Great for demos. - # postgres — external PostgreSQL (configured below). Enables multiple - # replicas and the HPA. + # 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. @@ -79,86 +91,135 @@ config: # PostgreSQL connection (driver=postgres only). The password is NOT set here — # it comes from the Secret key secrets.keys.databasePassword. postgres: - # Host reachable FROM THE CLUSTER: an in-cluster Service - # (e.g. my-postgresql.db.svc.cluster.local) or an external endpoint. - host: "" + host: "" # in-cluster Service or external endpoint port: 5432 - # Database (schema) name to connect to. (Key is `database` to match the - # gateway chart's DB-config convention.) - database: platformapi - # Login role; its password comes from the Secret (see above). - user: postgres - sslmode: disable # disable | require | verify-ca | verify-full - # Connection-pool tuning: - max_open_conns: 25 # max connections open at once - max_idle_conns: 10 # idle connections kept ready in the pool - conn_max_lifetime: 300 # recycle a connection after this many seconds - - # --- TLS on the Platform API's own listener --- - tls: - # true → the app serves HTTPS using certs from certDir (provisioned via the - # top-level `tls:` block below). - # false → serves plain HTTP; put TLS termination in front (ingress / mesh). - enabled: true - certDir: /app/data/certs - # --- CORS --- - # Browser origins allowed to call the API with credentials. MUST be set to - # explicit origins when developmentMode is false. When empty AND developmentMode - # is true, a wildcard origin is allowed (dev convenience only). - cors: - allowedOrigins: [] - # - https://workspace.example.com + 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 --- - # Pick exactly ONE real mode (jwt / idp / file_based). Enabling more than one is - # rejected at startup. auth: - # jwt — local HMAC-signed tokens issued after file-based login. The usual - # choice for self-hosted setups. Its signing key comes from - # secrets.keys.authJwtSecretKey (required when developmentMode is false). + # 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: - enabled: true issuer: platform-api - # Skip signature verification. Forced to false when developmentMode is false. - skipValidation: true - # file_based — local username/password login. Rejected when developmentMode - # is false (use idp in production). - fileBased: - enabled: true + 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 - users: - - username: admin - # bcrypt hash of the password "admin". Regenerate with: - # htpasswd -bnBC 12 "" | tr -d ':\n' - passwordHash: "$2y$10$U2yKMwGamGwDoMu0hRPT7u8nCuP8z/qxHFOKV6dhIxkJN9NJ0eVQ." - 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 — validate tokens from an external OIDC provider (Asgardeo, Keycloak, - # Auth0, …) against its JWKS. Use this instead of jwt/file_based in production. + # 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: - enabled: false name: "" jwksUrl: "" issuer: [] # accepted token issuers - audience: [] # accepted audiences; empty = don't check audience - validationMode: scope # scope → check scope claim; role → check roles claim - # Map your IDP's token claim names to what the Platform API expects. - claimMappings: - organizationClaimName: organization - orgNameClaimName: org_name - orgHandleClaimName: org_handle - userIdClaimName: sub - usernameClaimName: username - emailClaimName: email - scopeClaimName: scope - rolesClaimPath: "" + 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 - # Seed a default Developer Portal on first start. Leave off for a standalone - # AI Workspace; the Developer Portal package turns this on itself. - defaultDevportal: + # --- 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. @@ -169,14 +230,20 @@ configToml: "" # ---------------------------------------------------------------------------- # 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 AUTH_JWT_SECRET_KEY (plus DATABASE_PASSWORD when -# driver=postgres). `keys` maps each logical name to the key inside that 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 - authJwtSecretKey: AUTH_JWT_SECRET_KEY - databasePassword: DATABASE_PASSWORD + 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 @@ -200,20 +267,22 @@ service: containerPort: 9243 # ---------------------------------------------------------------------------- -# tls — where the listener's certificate comes from (used when config.tls.enabled) +# 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. - # selfSigned → the app generates a self-signed pair at startup (demo only; - # requires developmentMode=true). - certificateProvider: selfSigned + 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 # ignored when createIssuer=true - kind: Issuer + name: selfsigned-issuer # used when createIssuer=false + kind: Issuer # Issuer | ClusterIssuer # group: cert-manager.io commonName: platform-api.localhost dnsNames: From cd34d2d6272d0d248855d1678a53eb8ffa6b3eb8 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 24 Jul 2026 14:14:26 +0530 Subject: [PATCH 6/9] Refactor .gitignore to use generic patterns for chart directories and lockfiles --- kubernetes/helm/.gitignore | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/kubernetes/helm/.gitignore b/kubernetes/helm/.gitignore index d60835427e..8320388e0b 100644 --- a/kubernetes/helm/.gitignore +++ b/kubernetes/helm/.gitignore @@ -1,9 +1,8 @@ # Vendored subchart archives + lockfiles fetched by `helm dependency update`. # Build artifacts (the umbrellas pull their components in) — never commit them. -ai-workspace-helm-chart/charts/ -ai-workspace-helm-chart/Chart.lock -developer-portal-helm-chart/charts/ -developer-portal-helm-chart/Chart.lock +# 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). From ca8d3dfe6c568855f5e611a3ac9c32a61fcff36d Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 24 Jul 2026 15:02:44 +0530 Subject: [PATCH 7/9] Change ai-workspace port and fixed cert paths --- kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml | 4 ++-- .../templates/configmap.yaml | 9 +++++++-- .../templates/deployment.yaml | 6 ++++-- .../helm/ai-workspace-ui-helm-chart/values.yaml | 11 ++++++++--- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml index 502cf87b45..b40389ffde 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/Chart.yaml @@ -5,8 +5,8 @@ description: >- released component chart; consumed by the ai-workspace product package (and pluggable into others) and installable standalone. type: application -version: 1.0.0-alpha2 -appVersion: "1.0.0-alpha2" +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/ai-workspace-ui-helm-chart/templates/configmap.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml index 16b6916df2..4bb42cc9ff 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/configmap.yaml @@ -22,9 +22,14 @@ data: config.toml: | [ai_workspace] default_org_region = {{ $cfg.defaultOrgRegion | quote }} - - [ai_workspace.server] 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" diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml index d32fb989e1..5a30a88932 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/templates/deployment.yaml @@ -156,10 +156,12 @@ spec: 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: tls.crt + path: cert.pem - key: {{ if eq $tls.certificateProvider "cert-manager" }}tls.key{{ else }}{{ $tls.secret.keyKey }}{{ end }} - path: tls.key + path: key.pem {{- end }} {{- with $deployment.extraVolumes }} {{- toYaml . | nindent 8 }} diff --git a/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml index fdc56a7b62..b493e613a4 100644 --- a/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml +++ b/kubernetes/helm/ai-workspace-ui-helm-chart/values.yaml @@ -61,6 +61,11 @@ config: # 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 @@ -143,7 +148,7 @@ service: type: LoadBalancer # LoadBalancer | NodePort | ClusterIP annotations: {} labels: {} - port: 5380 + port: 9643 clusterIP: "" externalTrafficPolicy: "" loadBalancerClass: "" @@ -152,8 +157,8 @@ service: ipFamilies: [] nodePort: "" -# Port the BFF process listens on. -containerPort: 5380 +# Port the BFF process listens on (the BFF's native default). +containerPort: 9643 # ---------------------------------------------------------------------------- # tls — certificate for the BFF's HTTPS listener From 26d8afcf52a6cc8a6f984cf41bf859a794fab69d Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 24 Jul 2026 15:03:38 +0530 Subject: [PATCH 8/9] Update to the latest version of developer portal and platform api --- kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml | 4 ++-- kubernetes/helm/platform-api-helm-chart/Chart.yaml | 4 ++-- kubernetes/helm/platform-api-helm-chart/values.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml index a0031f7727..068c34dccd 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/Chart.yaml @@ -5,8 +5,8 @@ description: >- component chart; consumed by the developer-portal product package (and pluggable into others) and installable standalone. type: application -version: 1.0.0-alpha2 -appVersion: "1.0.0-alpha2" +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/platform-api-helm-chart/Chart.yaml b/kubernetes/helm/platform-api-helm-chart/Chart.yaml index 2518a0c290..34cb9e6aa3 100644 --- a/kubernetes/helm/platform-api-helm-chart/Chart.yaml +++ b/kubernetes/helm/platform-api-helm-chart/Chart.yaml @@ -5,8 +5,8 @@ description: >- released component chart; consumed by the product charts (ai-workspace, developer-portal) and installable standalone. type: application -version: 0.12.0 -appVersion: "0.12.0" +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/values.yaml b/kubernetes/helm/platform-api-helm-chart/values.yaml index 9c69aef75e..bafe47212d 100644 --- a/kubernetes/helm/platform-api-helm-chart/values.yaml +++ b/kubernetes/helm/platform-api-helm-chart/values.yaml @@ -46,7 +46,7 @@ global: enabled: true image: repository: ghcr.io/wso2/api-platform/platform-api - tag: 0.12.0 + tag: 0.13.0 pullPolicy: IfNotPresent # Pull secrets for THIS component only (merged with global.imagePullSecrets). imagePullSecrets: [] From 8ca12e555d7b43efd648c20aa86c10ac0b6e5270 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 24 Jul 2026 15:25:29 +0530 Subject: [PATCH 9/9] Update security role validation settings for local development --- .../helm/developer-portal-ui-helm-chart/values-local.yaml | 3 +++ kubernetes/helm/developer-portal-ui-helm-chart/values.yaml | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml b/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml index f38660a981..77a9409fa5 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values-local.yaml @@ -15,3 +15,6 @@ config: 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 index 30c564137f..dc24b24fed 100644 --- a/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml +++ b/kubernetes/helm/developer-portal-ui-helm-chart/values.yaml @@ -89,7 +89,10 @@ config: # --- Security --- security: - roleValidation: false # enforce role checks on incoming tokens + # 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