From 5a649d03fbf6901ce6a94b6d0ac940f9bff59b60 Mon Sep 17 00:00:00 2001 From: Julia A Date: Mon, 11 May 2026 18:26:23 +0400 Subject: [PATCH 1/7] fix(014-galust-umbrella-chart): Galust Imbrella Chart --- charts/galust-ai-layer/.helmignore | 18 ++ charts/galust-ai-layer/Chart.lock | 15 + charts/galust-ai-layer/Chart.yaml | 28 ++ charts/galust-ai-layer/README.md | 273 +++++++++++++++++ charts/galust-ai-layer/charts/base-0.3.2.tgz | Bin 0 -> 16565 bytes charts/galust-ai-layer/templates/NOTES.txt | 20 ++ charts/galust-ai-layer/templates/_helpers.tpl | 40 +++ .../templates/image-pull-secret.yaml | 20 ++ charts/galust-ai-layer/values copy.yaml | 265 +++++++++++++++++ charts/galust-ai-layer/values.test.yaml | 25 ++ charts/galust-ai-layer/values.yaml | 275 ++++++++++++++++++ specs/014-galust-umbrella-chart/plan.md | 54 ++++ specs/014-galust-umbrella-chart/spec.md | 63 ++++ specs/014-galust-umbrella-chart/tasks.md | 21 ++ 14 files changed, 1117 insertions(+) create mode 100644 charts/galust-ai-layer/.helmignore create mode 100644 charts/galust-ai-layer/Chart.lock create mode 100644 charts/galust-ai-layer/Chart.yaml create mode 100644 charts/galust-ai-layer/README.md create mode 100644 charts/galust-ai-layer/charts/base-0.3.2.tgz create mode 100644 charts/galust-ai-layer/templates/NOTES.txt create mode 100644 charts/galust-ai-layer/templates/_helpers.tpl create mode 100644 charts/galust-ai-layer/templates/image-pull-secret.yaml create mode 100644 charts/galust-ai-layer/values copy.yaml create mode 100644 charts/galust-ai-layer/values.test.yaml create mode 100644 charts/galust-ai-layer/values.yaml create mode 100644 specs/014-galust-umbrella-chart/plan.md create mode 100644 specs/014-galust-umbrella-chart/spec.md create mode 100644 specs/014-galust-umbrella-chart/tasks.md diff --git a/charts/galust-ai-layer/.helmignore b/charts/galust-ai-layer/.helmignore new file mode 100644 index 0000000..414bb6e --- /dev/null +++ b/charts/galust-ai-layer/.helmignore @@ -0,0 +1,18 @@ +# Patterns to ignore when building packages. +.DS_Store +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +*.swp +*.bak +*.tmp +*.orig +*~ +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/galust-ai-layer/Chart.lock b/charts/galust-ai-layer/Chart.lock new file mode 100644 index 0000000..859864e --- /dev/null +++ b/charts/galust-ai-layer/Chart.lock @@ -0,0 +1,15 @@ +dependencies: +- name: base + repository: https://dasmeta.github.io/helm + version: 0.3.2 +- name: base + repository: https://dasmeta.github.io/helm + version: 0.3.2 +- name: base + repository: https://dasmeta.github.io/helm + version: 0.3.2 +- name: base + repository: https://dasmeta.github.io/helm + version: 0.3.2 +digest: sha256:34d0dda0c6459ceaaa2b2e45c9f334038b1763aa7b0683e5e241af5a96276961 +generated: "2026-05-11T14:46:40.654468+04:00" diff --git a/charts/galust-ai-layer/Chart.yaml b/charts/galust-ai-layer/Chart.yaml new file mode 100644 index 0000000..c661d0a --- /dev/null +++ b/charts/galust-ai-layer/Chart.yaml @@ -0,0 +1,28 @@ +apiVersion: v2 +name: galust-ai-layer +description: Galust AI layer umbrella chart for Kubernetes clusters +type: application +version: 0.1.0 +appVersion: "0.1.0" + +dependencies: + - name: base + alias: backend + version: 0.3.2 + repository: https://dasmeta.github.io/helm + condition: backend.enabled + - name: base + alias: mcp + version: 0.3.2 + repository: https://dasmeta.github.io/helm + condition: mcp.enabled + - name: base + alias: mcpUseCase + version: 0.3.2 + repository: https://dasmeta.github.io/helm + condition: mcpUseCase.enabled + - name: base + alias: orchestrator + version: 0.3.2 + repository: https://dasmeta.github.io/helm + condition: orchestrator.enabled diff --git a/charts/galust-ai-layer/README.md b/charts/galust-ai-layer/README.md new file mode 100644 index 0000000..7f619d9 --- /dev/null +++ b/charts/galust-ai-layer/README.md @@ -0,0 +1,273 @@ +# Galust AI Layer + +Umbrella Helm chart for deploying the Galust AI layer stack into a Kubernetes or EKS cluster. + +## What This Chart Deploys + +This chart is an umbrella chart for the Galust AI layer services. It wraps the published `dasmeta/base` chart once per component and deploys: + +- Strapi backend +- MCP +- MCP use-case service +- Orchestrator + +The chart manages Kubernetes workload configuration for these services. It does not provision cloud infrastructure, databases, DNS records, TLS issuers, IAM roles, ECR policies, or external secrets. + +## Components + +The chart wraps the published `dasmeta/base` chart with one alias per deployable component: + +| Component | Values key | Default | +| --- | --- | --- | +| Strapi backend | `backend.enabled` | `true` | +| MCP | `mcp.enabled` | `true` | +| MCP use-case service | `mcpUseCase.enabled` | `true` | +| Orchestrator | `orchestrator.enabled` | `true` | + +Each component can be disabled independently: + +```bash +helm upgrade --install galust-ai-layer ./charts/galust-ai-layer \ + -n ai-layer \ + --create-namespace \ + --set mcpUseCase.enabled=false +``` + +## Prerequisites + +Before deploying, confirm the target cluster has: + +- Kubernetes access through a working `kubectl` context. +- Helm 3 installed locally. +- Access to the `dasmeta` Helm repository, because this chart depends on `dasmeta/base`. +- Namespace access for `ai-layer`, or permission to create it. +- Image pull access for the private ECR images. +- Required application secrets already created in the namespace. +- Database connectivity for the backend. +- A PVC or storage class suitable for backend uploads. +- Ingress controller installed if ingress is enabled. +- DNS records pointing the public hosts to the ingress/load balancer. +- TLS/cert-manager setup if the default TLS annotations and secrets are used. + +Required default Kubernetes objects: + +| Object | Default name | Used by | +| --- | --- | --- | +| Docker registry pull secret | `ecr-secret` | all components | +| Backend app secret | `ai-layer-strapi` | backend | +| Backend DB host secret | `cnpg-main-urls`, key `host` | backend | +| Backend DB password secret | `cnpg-main-user`, key `password` | backend | +| Backend uploads PVC | `ai-layer-strapi-uploads` | backend | +| MCP secret | `ai-layer-mcp` | MCP | +| MCP use-case secret | `ai-layer-mcp-use-case` | MCP use-case | +| Orchestrator secret | `ai-layer-orchestrator` | orchestrator | + +External dependencies such as Redis, Qdrant, Langfuse, OpenAI credentials, database provisioning, External Secrets, IAM trust, and DNS are handled outside this chart. + +## Orchestrator Secret + +The orchestrator reads sensitive values from a Kubernetes Secret through `envFrom.secret`. + +Default values: + +```yaml +orchestrator: + envFrom: + secret: ai-layer-orchestrator +``` + +This means every key in the `ai-layer-orchestrator` Secret is exposed to the orchestrator container as an environment variable. The default secret should contain: + +| Secret key | Purpose | +| --- | --- | +| `OPENAI_API_KEY` | OpenAI API access | +| `LANGFUSE_PUBLIC_KEY` | Langfuse public key | +| `LANGFUSE_SECRET_KEY` | Langfuse secret key | +| `CLOUDBROWSER_API_TOKEN` | Cloud browser access | +| `FIRECRAWL_API_KEY` | Firecrawl API access | +| `SENTRY_DSN` | Sentry project DSN | +| `AI_LAYER_BACKEND_API_TOKEN` | Backend API token | +| `MCP_USE_CASE_AUTH_TOKEN` | MCP use-case auth token | +| `SUPPORT_CHAT_CORE_PRODUCT_UID` | Support chat core product UID | + +Create or update the secret before deploying: + +```bash +kubectl create secret generic ai-layer-orchestrator \ + -n ai-layer \ + --from-literal=OPENAI_API_KEY='' \ + --from-literal=LANGFUSE_PUBLIC_KEY='' \ + --from-literal=LANGFUSE_SECRET_KEY='' \ + --from-literal=CLOUDBROWSER_API_TOKEN='' \ + --from-literal=FIRECRAWL_API_KEY='' \ + --from-literal=SENTRY_DSN='' \ + --from-literal=AI_LAYER_BACKEND_API_TOKEN='' \ + --from-literal=MCP_USE_CASE_AUTH_TOKEN='' \ + --from-literal=SUPPORT_CHAT_CORE_PRODUCT_UID='' \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Use a different secret name if needed: + +```bash +helm upgrade --install galust-ai-layer charts/galust-ai-layer \ + -n ai-layer \ + --create-namespace \ + --set orchestrator.envFrom.secret=my-orchestrator-secret +``` + +## Configure Public URLs + +Public URLs are defined near the top of `values.yaml`: + +```yaml +global: + API_URL: &apiUrl https://api.galust.ai + API_HOST: &apiHost api.galust.ai + APP_URL: &appUrl https://app.galust.ai + MCP_URL: &mcpUrl https://mcp.galust.ai + MCP_HOST: &mcpHost mcp.galust.ai + ADMIN_URL: &adminUrl https://api.galust.ai/admin + OPENAPI_BASE_URL: &openapiBaseUrl https://api.galust.ai/api + OPENAPI_SPEC_URL: &openapiSpecUrl https://api.galust.ai/documentation/openapi.json + ORCHESTRATOR_ENDPOINT: &orchestratorEndpoint https://api.galust.ai/orchestrator/api/galust/ask +``` + +The rest of `values.yaml` reuses these values with YAML anchors, for example: + +```yaml +backend: + config: + ADMIN_URL: *adminUrl + BACKEND_URL: *apiUrl + ingress: + hosts: + - host: *apiHost +``` + +Important: YAML anchors are resolved before Helm merges extra values files. If you deploy with `-f values.test.yaml` and only override `global`, Helm will not recalculate aliases already resolved in `values.yaml`. To change domains for another environment, either edit the full values file or provide a values file that also overrides the component `config` and `ingress` fields. + +## Image Pull Access + +The default values expect an existing Kubernetes docker registry secret named `ecr-secret`: + +```yaml +imagePullSecrets: + - name: ecr-secret +``` + +The chart can also render the secret when `imagePullSecret.create=true`: + +```bash +helm upgrade --install galust-ai-layer ./charts/galust-ai-layer \ + -n ai-layer \ + --create-namespace \ + --set imagePullSecret.create=true \ + --set-file imagePullSecret.dockerConfigJson=./dockerconfig.json +``` + +AWS IAM trust, ECR repository policies, role assumption, and External Secrets setup are intentionally outside this chart. Provide the resulting Kubernetes pull secret name through `imagePullSecrets` and each component's `imagePullSecrets` override. + +Example secret creation: + +```bash +kubectl create namespace ai-layer + +kubectl create secret docker-registry ecr-secret \ + -n ai-layer \ + --docker-server=565580475168.dkr.ecr.eu-central-1.amazonaws.com \ + --docker-username=AWS \ + --docker-password='' +``` + +## Backend Notes + +The backend values use the existing Galust Strapi image and runtime defaults from `ai-layer/backend/helm/strapi.yaml`, but this umbrella chart does not provision AWS IAM or a managed database. By default the backend expects: + +- an `ai-layer-strapi` secret for Strapi application secrets +- `cnpg-main-urls` with key `host` +- `cnpg-main-user` with key `password` +- a PVC named `ai-layer-strapi-uploads` for `/opt/app/public/uploads` + +Override those names for environment-specific infrastructure. + +## Deploy + +Run commands from the `helm` repository directory: + +```bash +cd /Users/juliaaghamyan/Desktop/dasmeta/helm +helm dependency update charts/galust-ai-layer +``` + +Deploy with default `values.yaml`: + +```bash +helm upgrade --install galust-ai-layer charts/galust-ai-layer \ + -n ai-layer \ + --create-namespace +``` + +Deploy with the test values overlay: + +```bash +helm upgrade --install galust-ai-layer charts/galust-ai-layer \ + -n ai-layer \ + --create-namespace \ + -f charts/galust-ai-layer/values.test.yaml +``` + +Disable a component: + +```bash +helm upgrade --install galust-ai-layer charts/galust-ai-layer \ + -n ai-layer \ + --create-namespace \ + --set mcpUseCase.enabled=false +``` + +## Post-Deploy Checks + +```bash +kubectl get pods -n ai-layer +kubectl get svc -n ai-layer +kubectl get ingress -n ai-layer +kubectl describe pod -n ai-layer -l app=ai-layer-orchestrator +kubectl logs -n ai-layer deploy/ai-layer-orchestrator +``` + +Expected default service names: + +- `ai-layer-strapi` +- `ai-layer-mcp` +- `ai-layer-mcp-use-case` +- `ai-layer-orchestrator` + +Expected public hosts when ingress is enabled: + +- `api.galust.ai` +- `mcp.galust.ai` + +## Local Validation + +```bash +helm dependency update charts/galust-ai-layer +helm lint charts/galust-ai-layer +helm template galust-ai-layer charts/galust-ai-layer -n ai-layer +helm template galust-ai-layer charts/galust-ai-layer -n ai-layer --set backend.enabled=false +helm template galust-ai-layer charts/galust-ai-layer -n ai-layer -f charts/galust-ai-layer/values.test.yaml +``` + +## Troubleshooting + +If pods are stuck in `ImagePullBackOff`, check the `ecr-secret` secret and ECR access. + +If the backend fails to start, check the `ai-layer-strapi`, `cnpg-main-urls`, and `cnpg-main-user` secrets, plus database reachability from the namespace. + +If ingress does not work, confirm the ingress controller, DNS records, TLS secret or cert-manager issuer, and rendered ingress hosts. + +If URL overrides do not appear in rendered manifests, remember that YAML anchors are not dynamic Helm templates. Render locally with: + +```bash +helm template galust-ai-layer charts/galust-ai-layer -n ai-layer -f charts/galust-ai-layer/values.test.yaml +``` diff --git a/charts/galust-ai-layer/charts/base-0.3.2.tgz b/charts/galust-ai-layer/charts/base-0.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..7ec96b7847d2e518e3cd203858afc56aeec3d11f GIT binary patch literal 16565 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 literal 0 HcmV?d00001 diff --git a/charts/galust-ai-layer/templates/NOTES.txt b/charts/galust-ai-layer/templates/NOTES.txt new file mode 100644 index 0000000..c7b6135 --- /dev/null +++ b/charts/galust-ai-layer/templates/NOTES.txt @@ -0,0 +1,20 @@ +Galust AI layer chart rendered for namespace {{ .Release.Namespace }}. + +Enabled components: +{{- if .Values.backend.enabled }} +- backend: {{ .Values.backend.fullnameOverride | default "backend" }} +{{- end }} +{{- if .Values.mcp.enabled }} +- mcp: {{ .Values.mcp.fullnameOverride | default "mcp" }} +{{- end }} +{{- if .Values.mcpUseCase.enabled }} +- mcp-use-case: {{ .Values.mcpUseCase.fullnameOverride | default "mcp-use-case" }} +{{- end }} +{{- if .Values.orchestrator.enabled }} +- orchestrator: {{ .Values.orchestrator.fullnameOverride | default "orchestrator" }} +{{- end }} + +Image pull access: +- By default the chart references an existing imagePullSecret named {{ include "galust-ai-layer.imagePullSecretName" . }}. +- Set imagePullSecret.create=true with dockerConfigJson or dockerConfigJsonBase64 to render that Secret from this chart. +- AWS IAM trust, ECR repository policy, and external secret provisioning are outside this chart. diff --git a/charts/galust-ai-layer/templates/_helpers.tpl b/charts/galust-ai-layer/templates/_helpers.tpl new file mode 100644 index 0000000..45e93fd --- /dev/null +++ b/charts/galust-ai-layer/templates/_helpers.tpl @@ -0,0 +1,40 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "galust-ai-layer.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "galust-ai-layer.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := include "galust-ai-layer.name" . }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Common labels. +*/}} +{{- define "galust-ai-layer.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +app.kubernetes.io/name: {{ include "galust-ai-layer.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Image pull secret name used by the optional generated dockerconfigjson Secret. +*/}} +{{- define "galust-ai-layer.imagePullSecretName" -}} +{{- default "ecr-secret" .Values.imagePullSecret.name | trunc 63 | trimSuffix "-" }} +{{- end }} diff --git a/charts/galust-ai-layer/templates/image-pull-secret.yaml b/charts/galust-ai-layer/templates/image-pull-secret.yaml new file mode 100644 index 0000000..3f4aa42 --- /dev/null +++ b/charts/galust-ai-layer/templates/image-pull-secret.yaml @@ -0,0 +1,20 @@ +{{- if .Values.imagePullSecret.create -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "galust-ai-layer.imagePullSecretName" . }} + labels: + {{- include "galust-ai-layer.labels" . | nindent 4 }} + {{- with .Values.imagePullSecret.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +type: kubernetes.io/dockerconfigjson +{{- if .Values.imagePullSecret.dockerConfigJson }} +stringData: + .dockerconfigjson: {{ .Values.imagePullSecret.dockerConfigJson | quote }} +{{- else }} +data: + .dockerconfigjson: {{ required "imagePullSecret.dockerConfigJson or imagePullSecret.dockerConfigJsonBase64 is required when imagePullSecret.create=true" .Values.imagePullSecret.dockerConfigJsonBase64 | quote }} +{{- end }} +{{- end }} diff --git a/charts/galust-ai-layer/values copy.yaml b/charts/galust-ai-layer/values copy.yaml new file mode 100644 index 0000000..3dd6070 --- /dev/null +++ b/charts/galust-ai-layer/values copy.yaml @@ -0,0 +1,265 @@ +imagePullSecret: + create: false + name: ecr-secret + annotations: {} + dockerConfigJson: "" + dockerConfigJsonBase64: "" + +imagePullSecrets: &galustImagePullSecrets + - name: ecr-secret + +gatewayApi: + enabled: false + +zeroTrustMesh: + enabled: false + +backend: + enabled: true + gatewayApi: + enabled: false + zeroTrustMesh: + enabled: false + fullnameOverride: ai-layer-strapi + version: 0.1.0 + appVersion: 0.1.0 + image: + repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-backend + tag: latest + pullPolicy: Always + imagePullSecrets: *galustImagePullSecrets + replicaCount: 1 + containerPort: 1337 + service: + type: ClusterIP + port: 1337 + persistence: + uploads: + enabled: true + claimName: ai-layer-strapi-uploads + size: 20Gi + accessModes: + - ReadWriteOnce + storageClassName: "" + keepPvc: true + storage: + - persistentVolumeClaimName: ai-layer-strapi-uploads + accessModes: + - ReadWriteOnce + requestedSize: 20Gi + keepPvc: true + volumes: + - name: strapi-uploads + persistentVolumeClaim: + claimName: ai-layer-strapi-uploads + mountPath: /opt/app/public/uploads + resources: + limits: + cpu: 1500m + memory: 1500Mi + requests: + cpu: 1000m + memory: 1000Mi + securityContext: + readOnlyRootFilesystem: false + runAsNonRoot: false + runAsUser: 0 + runAsGroup: 0 + envFrom: + secret: ai-layer-strapi + config: + PORT: "1337" + DATABASE_CLIENT: postgres + DATABASE_PORT: "5432" + DATABASE_NAME: strapi + DATABASE_USERNAME: strapi + STRAPI_DISABLE_UPDATE_NOTIFICATION: "true" + FAST_REFRESH: "false" + EXTRA_ARGS: "" + ADMIN_URL: https://api.galust.ai/admin + APP_HOST: 0.0.0.0 + APP_URL: https://api.galust.ai/admin + BACKEND_URL: https://api.galust.ai + ENV: production + HOST: 0.0.0.0 + NODE_ENV: production + PUBLIC_URL: https://api.galust.ai + STRAPI_ADMIN_BACKEND_URL: https://api.galust.ai + EVENT_MANAGER_BACKEND_HOST: http://event-manager.em.svc.cluster.local + extraEnv: + DATABASE_HOST: + secretKeyRef: + name: cnpg-main-urls + key: host + DATABASE_PASSWORD: + secretKeyRef: + name: cnpg-main-user + key: password + ingress: + enabled: false + class: nginx + annotations: + kubernetes.io/tls-acme: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: api.galust.ai + paths: + - path: / + pathType: Prefix + backend: + servicePort: 1337 + tls: + - secretName: com-galust-api-tls2 + hosts: + - api.galust.ai + +mcp: + enabled: true + gatewayApi: + enabled: false + zeroTrustMesh: + enabled: false + fullnameOverride: ai-layer-mcp + version: 0.0.1 + appVersion: 0.0.1 + image: + repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-mcp + tag: latest + pullPolicy: Always + imagePullSecrets: *galustImagePullSecrets + replicaCount: 1 + labels: + version: + name: app-version + value: v0.0.1 + app: + name: app + value: ai-layer-mcp + service: + type: ClusterIP + port: 4002 + containerPort: 4002 + envFrom: + secret: ai-layer-mcp + config: + DEBUG_MCP_HEADER_FLOW: "true" + OPENAPI_BASE_URL: http://ai-layer-strapi.ai-layer.svc.cluster.local:1337/api + OPENAPI_SPEC_URL: http://ai-layer-strapi.ai-layer.svc.cluster.local:1337/documentation/openapi.json + ingress: + enabled: false + class: nginx + annotations: + kubernetes.io/tls-acme: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: mcp.galust.ai + paths: + - path: / + pathType: Prefix + backend: + servicePort: 4002 + tls: + - secretName: com-galust-mcp-tls2 + hosts: + - mcp.galust.ai + +mcpUseCase: + enabled: true + gatewayApi: + enabled: false + zeroTrustMesh: + enabled: false + fullnameOverride: ai-layer-mcp-use-case + version: 0.0.1 + appVersion: 0.0.1 + image: + repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-mcp-use-case + tag: latest + pullPolicy: Always + imagePullSecrets: *galustImagePullSecrets + replicaCount: 1 + labels: + version: + name: app-version + value: v0.0.1 + app: + name: app + value: ai-layer-mcp-use-case + service: + type: ClusterIP + port: 4002 + containerPort: 4002 + envFrom: + secret: ai-layer-mcp-use-case + config: + DEBUG_MCP_HEADER_FLOW: "true" + TOOLS_CATALOG_URL: https://api.galust.ai + ORCHESTRATOR_ENDPOINT: http://ai-layer-orchestrator:3000/api/galust/ask + +orchestrator: + enabled: true + gatewayApi: + enabled: false + zeroTrustMesh: + enabled: false + fullnameOverride: ai-layer-orchestrator + version: 0.0.1 + appVersion: 0.0.1 + image: + repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-orchestrator + tag: latest + pullPolicy: Always + imagePullSecrets: *galustImagePullSecrets + replicaCount: 1 + labels: + version: + name: app-version + value: v0.0.1 + app: + name: app + value: ai-layer-orchestrator + service: + type: ClusterIP + port: 3000 + containerPort: 3000 + resources: + requests: + cpu: 500m + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 5 + envFrom: + secret: ai-layer-orchestrator + config: + NODE_ENV: production + OPENAI_MODEL: gpt-4o-mini + QDRANT_URL: http://qdrant.dasmeta-customer-portal.svc.cluster.local:6333 + REDIS_URL: redis://redis:6379/0 + LANGFUSE_HOST: https://cloud.langfuse.com + GLOBAL_PREFIX: orchestrator + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://localhost:4318/v1/traces + AI_LAYER_BACKEND_URL: http://ai-layer-strapi:1337 + ingress: + enabled: false + class: nginx + annotations: + kubernetes.io/tls-acme: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: https://app.galust.ai + nginx.ingress.kubernetes.io/cors-allow-credentials: "true" + nginx.ingress.kubernetes.io/cors-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS + nginx.ingress.kubernetes.io/cors-allow-headers: Authorization, Content-Type, Origin, Accept, X-Requested-With, MCP-Protocol-Version, Mcp-Session-Id + hosts: + - host: api.galust.ai + paths: + - path: /orchestrator + pathType: Prefix + backend: + servicePort: 3000 + tls: + - clusterIssuer: letsencrypt-prod + secretName: com-galust-api-tls2 + hosts: + - api.galust.ai diff --git a/charts/galust-ai-layer/values.test.yaml b/charts/galust-ai-layer/values.test.yaml new file mode 100644 index 0000000..4717467 --- /dev/null +++ b/charts/galust-ai-layer/values.test.yaml @@ -0,0 +1,25 @@ +# Minimal test overlay for externally reachable Galust AI layer URLs. +imagePullSecret: + create: false + name: ecr-secret + annotations: {} + dockerConfigJson: "" + dockerConfigJsonBase64: "" + +imagePullSecrets: &galustImagePullSecrets + - name: ecr-secret + +global: + API_URL: &apiUrl https://api.galust.ai + API_HOST: &apiHost api.galust.ai + APP_URL: &appUrl https://app.galust.ai + MCP_URL: &mcpUrl https://mcp.galust.ai + MCP_HOST: &mcpHost mcp.galust.ai + ADMIN_URL: &adminUrl https://api.galust.ai/admin + OPENAPI_BASE_URL: &openapiBaseUrl https://api.galust.ai/api + OPENAPI_SPEC_URL: &openapiSpecUrl https://api.galust.ai/documentation/openapi.json + ORCHESTRATOR_ENDPOINT: &orchestratorEndpoint https://api.galust.ai/orchestrator/api/galust/ask + +orchestrator: + envFrom: + secret: ai-layer-orchestrator \ No newline at end of file diff --git a/charts/galust-ai-layer/values.yaml b/charts/galust-ai-layer/values.yaml new file mode 100644 index 0000000..9e1d073 --- /dev/null +++ b/charts/galust-ai-layer/values.yaml @@ -0,0 +1,275 @@ +imagePullSecret: + create: false + name: ecr-secret + annotations: {} + dockerConfigJson: "" + dockerConfigJsonBase64: "" + +imagePullSecrets: &galustImagePullSecrets + - name: ecr-secret + +global: + API_URL: &apiUrl https://api.galust.ai + API_HOST: &apiHost api.galust.ai + APP_URL: &appUrl https://app.galust.ai + MCP_URL: &mcpUrl https://mcp.galust.ai + MCP_HOST: &mcpHost mcp.galust.ai + ADMIN_URL: &adminUrl https://api.galust.ai/admin + OPENAPI_BASE_URL: &openapiBaseUrl https://api.galust.ai/api + OPENAPI_SPEC_URL: &openapiSpecUrl https://api.galust.ai/documentation/openapi.json + ORCHESTRATOR_ENDPOINT: &orchestratorEndpoint https://api.galust.ai/orchestrator/api/galust/ask + +gatewayApi: + enabled: false + +zeroTrustMesh: + enabled: false + +backend: + enabled: true + gatewayApi: + enabled: false + zeroTrustMesh: + enabled: false + fullnameOverride: ai-layer-strapi + version: 0.1.0 + appVersion: 0.1.0 + image: + repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-backend + tag: latest + pullPolicy: Always + imagePullSecrets: *galustImagePullSecrets + replicaCount: 1 + containerPort: 1337 + service: + type: ClusterIP + port: 1337 + persistence: + uploads: + enabled: true + claimName: ai-layer-strapi-uploads + size: 20Gi + accessModes: + - ReadWriteOnce + storageClassName: "" + keepPvc: true + storage: + - persistentVolumeClaimName: ai-layer-strapi-uploads + accessModes: + - ReadWriteOnce + requestedSize: 20Gi + keepPvc: true + volumes: + - name: strapi-uploads + persistentVolumeClaim: + claimName: ai-layer-strapi-uploads + mountPath: /opt/app/public/uploads + resources: + limits: + cpu: 1500m + memory: 1500Mi + requests: + cpu: 1000m + memory: 1000Mi + securityContext: + readOnlyRootFilesystem: false + runAsNonRoot: false + runAsUser: 0 + runAsGroup: 0 + envFrom: + secret: ai-layer-strapi + config: + PORT: "1337" + DATABASE_CLIENT: postgres + DATABASE_PORT: "5432" + DATABASE_NAME: strapi + DATABASE_USERNAME: strapi + STRAPI_DISABLE_UPDATE_NOTIFICATION: "true" + FAST_REFRESH: "false" + EXTRA_ARGS: "" + ADMIN_URL: *adminUrl + APP_HOST: 0.0.0.0 + APP_URL: *adminUrl + BACKEND_URL: *apiUrl + ENV: production + HOST: 0.0.0.0 + NODE_ENV: production + PUBLIC_URL: *apiUrl + STRAPI_ADMIN_BACKEND_URL: *apiUrl + extraEnv: + DATABASE_HOST: + secretKeyRef: + name: cnpg-main-urls + key: host + DATABASE_PASSWORD: + secretKeyRef: + name: cnpg-main-user + key: password + ingress: + enabled: true + class: nginx + annotations: + kubernetes.io/tls-acme: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: *apiHost + paths: + - path: / + pathType: Prefix + backend: + servicePort: 1337 + tls: + - secretName: com-galust-api-tls2 + hosts: + - *apiHost + +mcp: + enabled: true + gatewayApi: + enabled: false + zeroTrustMesh: + enabled: false + fullnameOverride: ai-layer-mcp + version: 0.0.1 + appVersion: 0.0.1 + image: + repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-mcp + tag: latest + pullPolicy: Always + imagePullSecrets: *galustImagePullSecrets + replicaCount: 1 + labels: + version: + name: app-version + value: v0.0.1 + app: + name: app + value: ai-layer-mcp + service: + type: ClusterIP + port: 4002 + containerPort: 4002 + envFrom: + secret: ai-layer-mcp + config: + DEBUG_MCP_HEADER_FLOW: "true" + OPENAPI_BASE_URL: *openapiBaseUrl + OPENAPI_SPEC_URL: *openapiSpecUrl + ingress: + enabled: true + class: nginx + annotations: + kubernetes.io/tls-acme: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: *mcpHost + paths: + - path: / + pathType: Prefix + backend: + servicePort: 4002 + tls: + - secretName: com-galust-mcp-tls2 + hosts: + - *mcpHost + +mcpUseCase: + enabled: true + gatewayApi: + enabled: false + zeroTrustMesh: + enabled: false + fullnameOverride: ai-layer-mcp-use-case + version: 0.0.1 + appVersion: 0.0.1 + image: + repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-mcp-use-case + tag: latest + pullPolicy: Always + imagePullSecrets: *galustImagePullSecrets + replicaCount: 1 + labels: + version: + name: app-version + value: v0.0.1 + app: + name: app + value: ai-layer-mcp-use-case + service: + type: ClusterIP + port: 4002 + containerPort: 4002 + envFrom: + secret: ai-layer-mcp-use-case + config: + DEBUG_MCP_HEADER_FLOW: "true" + TOOLS_CATALOG_URL: *apiUrl + ORCHESTRATOR_ENDPOINT: *orchestratorEndpoint + +orchestrator: + enabled: true + gatewayApi: + enabled: false + zeroTrustMesh: + enabled: false + fullnameOverride: ai-layer-orchestrator + version: 0.0.1 + appVersion: 0.0.1 + image: + repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-orchestrator + tag: latest + pullPolicy: Always + imagePullSecrets: *galustImagePullSecrets + replicaCount: 1 + labels: + version: + name: app-version + value: v0.0.1 + app: + name: app + value: ai-layer-orchestrator + service: + type: ClusterIP + port: 3000 + containerPort: 3000 + resources: + requests: + cpu: 500m + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 5 + envFrom: + # Existing Kubernetes Secret loaded as environment variables by the orchestrator. + # Override this name when secrets are managed outside this chart under a different name. + secret: ai-layer-orchestrator + config: + NODE_ENV: production + OPENAI_MODEL: gpt-4o-mini + LANGFUSE_HOST: https://cloud.langfuse.com + GLOBAL_PREFIX: orchestrator + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://localhost:4318/v1/traces + AI_LAYER_BACKEND_URL: *apiUrl + ingress: + enabled: true + class: nginx + annotations: + kubernetes.io/tls-acme: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: *appUrl + nginx.ingress.kubernetes.io/cors-allow-credentials: "true" + nginx.ingress.kubernetes.io/cors-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS + nginx.ingress.kubernetes.io/cors-allow-headers: Authorization, Content-Type, Origin, Accept, X-Requested-With, MCP-Protocol-Version, Mcp-Session-Id + hosts: + - host: *apiHost + paths: + - path: /orchestrator + pathType: Prefix + backend: + servicePort: 3000 + tls: + - clusterIssuer: letsencrypt-prod + secretName: com-galust-api-tls2 + hosts: + - *apiHost diff --git a/specs/014-galust-umbrella-chart/plan.md b/specs/014-galust-umbrella-chart/plan.md new file mode 100644 index 0000000..8001ea4 --- /dev/null +++ b/specs/014-galust-umbrella-chart/plan.md @@ -0,0 +1,54 @@ +# Implementation Plan: Galust AI Layer Umbrella Chart + +**Branch**: `014-galust-umbrella-chart` | **Date**: 2026-05-11 | **Spec**: `specs/014-galust-umbrella-chart/spec.md` +**Input**: Feature specification from `specs/014-galust-umbrella-chart/spec.md` + +## Summary + +Add `charts/galust-ai-layer` as an umbrella chart that deploys the Galust AI layer components to Kubernetes or EKS. Use one `dasmeta/base` dependency alias per component, expose component enable flags, and carry the existing `ai-layer` deployment defaults into chart values. + +## Technical Context + +**Language/Version**: Helm 3 chart YAML and Go templates +**Primary Dependencies**: Published `dasmeta/base` chart version `0.3.2`, matching the `ai-layer` deployment workflows +**Storage**: Backend uploads PVC, default `ai-layer-strapi-uploads` +**Testing**: `helm dependency update`, `helm lint`, `helm template` +**Target Platform**: Kubernetes / EKS +**Project Type**: Helm chart repository +**Constraints**: Do not provision AWS IAM trust, ECR repository policy, or database infrastructure in this chart +**Scale/Scope**: Four long-running Galust workloads + +## Constitution Check + +The change is chart-scoped and keeps shared chart logic in `charts/base`. It does not copy base templates into the new chart. + +## Project Structure + +### Documentation + +```text +specs/014-galust-umbrella-chart/ +├── spec.md +├── plan.md +└── tasks.md +``` + +### Source Code + +```text +charts/galust-ai-layer/ +├── Chart.yaml +├── README.md +├── values.yaml +└── templates/ + ├── NOTES.txt + ├── _helpers.tpl + └── image-pull-secret.yaml +``` + +## Implementation Notes + +- Use dependency `condition` fields so component toggles are native Helm behavior. +- Set `fullnameOverride` defaults to preserve existing Galust service names. +- Keep ingress defaults disabled to avoid installing Galust production hostnames into clusters by accident. +- Provide default image pull secret name `ecr-secret`, with optional chart-rendered docker config secret. diff --git a/specs/014-galust-umbrella-chart/spec.md b/specs/014-galust-umbrella-chart/spec.md new file mode 100644 index 0000000..ca8743f --- /dev/null +++ b/specs/014-galust-umbrella-chart/spec.md @@ -0,0 +1,63 @@ +# Feature Specification: Galust AI Layer Umbrella Chart + +**Feature Branch**: `014-galust-umbrella-chart` +**Created**: 2026-05-11 +**Status**: Draft +**Input**: Jira `DMVP-10005` and repo evidence from `ai-layer` + +## User Scenarios & Testing + +### User Story 1 - Install All Galust Components (Priority: P1) + +Platform operators can install one Helm chart to deploy the Galust backend, MCP, MCP use-case service, and orchestrator into their own EKS cluster. + +**Why this priority**: This is the core onboarding deliverable. + +**Independent Test**: Render the chart with default values and confirm all four component releases are present. + +**Acceptance Scenarios**: + +1. **Given** default chart values, **When** the chart is rendered, **Then** backend, mcp, mcp-use-case, and orchestrator resources are included. +2. **Given** an existing ECR pull secret name, **When** the chart is installed, **Then** every component references that pull secret. + +### User Story 2 - Selectively Disable Components (Priority: P1) + +Platform operators can deploy only the components they need for a given environment. + +**Why this priority**: The ticket explicitly asks for deploy options per component. + +**Independent Test**: Render the chart with each component disabled and confirm that component is omitted while the others remain. + +**Acceptance Scenarios**: + +1. **Given** `backend.enabled=false`, **When** the chart is rendered, **Then** backend resources are omitted. +2. **Given** `mcp.enabled=false`, **When** the chart is rendered, **Then** mcp resources are omitted. +3. **Given** `mcpUseCase.enabled=false`, **When** the chart is rendered, **Then** mcp-use-case resources are omitted. +4. **Given** `orchestrator.enabled=false`, **When** the chart is rendered, **Then** orchestrator resources are omitted. + +### User Story 3 - Configure ECR Pull Access (Priority: P2) + +Platform operators can point the deployment at a Kubernetes docker registry secret or have the chart render one from provided docker config JSON. + +**Why this priority**: Pull access is necessary for private images but AWS IAM trust remains outside this chart. + +**Independent Test**: Render the chart with `imagePullSecret.create=true` and confirm a `kubernetes.io/dockerconfigjson` Secret is produced. + +## Requirements + +### Functional Requirements + +- **FR-001**: The chart MUST expose independent enable flags for `backend`, `mcp`, `mcpUseCase`, and `orchestrator`. +- **FR-002**: The chart MUST use `dasmeta/base` subchart aliases for the four long-running workloads. +- **FR-003**: The chart MUST carry defaults derived from the current `ai-layer` deployment files. +- **FR-004**: The chart MUST allow image repository, tag, pull policy, and pull secrets to be overridden per component. +- **FR-005**: The chart MUST document ECR/IAM provisioning as out of scope. + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: `helm lint charts/galust-ai-layer` succeeds after dependencies are built. +- **SC-002**: `helm template` succeeds with default values. +- **SC-003**: `helm template` succeeds when any one component is disabled. +- **SC-004**: The README contains install and pull-secret examples. diff --git a/specs/014-galust-umbrella-chart/tasks.md b/specs/014-galust-umbrella-chart/tasks.md new file mode 100644 index 0000000..fbaca8c --- /dev/null +++ b/specs/014-galust-umbrella-chart/tasks.md @@ -0,0 +1,21 @@ +# Tasks: Galust AI Layer Umbrella Chart + +**Input**: `specs/014-galust-umbrella-chart/spec.md`, `specs/014-galust-umbrella-chart/plan.md` + +## Phase 1: Setup + +- [x] T001 Create Speckit package `specs/014-galust-umbrella-chart` +- [x] T002 Confirm source deployment values from `ai-layer` + +## Phase 2: Chart Implementation + +- [x] T003 Add `charts/galust-ai-layer/Chart.yaml` with base subchart aliases and component conditions +- [x] T004 Add default values for backend, mcp, mcp-use-case, and orchestrator +- [x] T005 Add optional docker registry pull-secret template +- [x] T006 Add chart README and notes + +## Phase 3: Validation + +- [x] T007 Run `helm dependency update charts/galust-ai-layer` +- [x] T008 Run `helm lint charts/galust-ai-layer` +- [x] T009 Render all components and disabled-component cases with `helm template` From a20b86dfedc47cb26b72c668a8bd06ae3e36e5f7 Mon Sep 17 00:00:00 2001 From: Julia A Date: Mon, 11 May 2026 18:27:09 +0400 Subject: [PATCH 2/7] fix(014-galust-umbrella-chart): Galust Imbrella Chart --- charts/galust-ai-layer/values copy.yaml | 265 ------------------------ 1 file changed, 265 deletions(-) delete mode 100644 charts/galust-ai-layer/values copy.yaml diff --git a/charts/galust-ai-layer/values copy.yaml b/charts/galust-ai-layer/values copy.yaml deleted file mode 100644 index 3dd6070..0000000 --- a/charts/galust-ai-layer/values copy.yaml +++ /dev/null @@ -1,265 +0,0 @@ -imagePullSecret: - create: false - name: ecr-secret - annotations: {} - dockerConfigJson: "" - dockerConfigJsonBase64: "" - -imagePullSecrets: &galustImagePullSecrets - - name: ecr-secret - -gatewayApi: - enabled: false - -zeroTrustMesh: - enabled: false - -backend: - enabled: true - gatewayApi: - enabled: false - zeroTrustMesh: - enabled: false - fullnameOverride: ai-layer-strapi - version: 0.1.0 - appVersion: 0.1.0 - image: - repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-backend - tag: latest - pullPolicy: Always - imagePullSecrets: *galustImagePullSecrets - replicaCount: 1 - containerPort: 1337 - service: - type: ClusterIP - port: 1337 - persistence: - uploads: - enabled: true - claimName: ai-layer-strapi-uploads - size: 20Gi - accessModes: - - ReadWriteOnce - storageClassName: "" - keepPvc: true - storage: - - persistentVolumeClaimName: ai-layer-strapi-uploads - accessModes: - - ReadWriteOnce - requestedSize: 20Gi - keepPvc: true - volumes: - - name: strapi-uploads - persistentVolumeClaim: - claimName: ai-layer-strapi-uploads - mountPath: /opt/app/public/uploads - resources: - limits: - cpu: 1500m - memory: 1500Mi - requests: - cpu: 1000m - memory: 1000Mi - securityContext: - readOnlyRootFilesystem: false - runAsNonRoot: false - runAsUser: 0 - runAsGroup: 0 - envFrom: - secret: ai-layer-strapi - config: - PORT: "1337" - DATABASE_CLIENT: postgres - DATABASE_PORT: "5432" - DATABASE_NAME: strapi - DATABASE_USERNAME: strapi - STRAPI_DISABLE_UPDATE_NOTIFICATION: "true" - FAST_REFRESH: "false" - EXTRA_ARGS: "" - ADMIN_URL: https://api.galust.ai/admin - APP_HOST: 0.0.0.0 - APP_URL: https://api.galust.ai/admin - BACKEND_URL: https://api.galust.ai - ENV: production - HOST: 0.0.0.0 - NODE_ENV: production - PUBLIC_URL: https://api.galust.ai - STRAPI_ADMIN_BACKEND_URL: https://api.galust.ai - EVENT_MANAGER_BACKEND_HOST: http://event-manager.em.svc.cluster.local - extraEnv: - DATABASE_HOST: - secretKeyRef: - name: cnpg-main-urls - key: host - DATABASE_PASSWORD: - secretKeyRef: - name: cnpg-main-user - key: password - ingress: - enabled: false - class: nginx - annotations: - kubernetes.io/tls-acme: "true" - cert-manager.io/cluster-issuer: letsencrypt-prod - hosts: - - host: api.galust.ai - paths: - - path: / - pathType: Prefix - backend: - servicePort: 1337 - tls: - - secretName: com-galust-api-tls2 - hosts: - - api.galust.ai - -mcp: - enabled: true - gatewayApi: - enabled: false - zeroTrustMesh: - enabled: false - fullnameOverride: ai-layer-mcp - version: 0.0.1 - appVersion: 0.0.1 - image: - repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-mcp - tag: latest - pullPolicy: Always - imagePullSecrets: *galustImagePullSecrets - replicaCount: 1 - labels: - version: - name: app-version - value: v0.0.1 - app: - name: app - value: ai-layer-mcp - service: - type: ClusterIP - port: 4002 - containerPort: 4002 - envFrom: - secret: ai-layer-mcp - config: - DEBUG_MCP_HEADER_FLOW: "true" - OPENAPI_BASE_URL: http://ai-layer-strapi.ai-layer.svc.cluster.local:1337/api - OPENAPI_SPEC_URL: http://ai-layer-strapi.ai-layer.svc.cluster.local:1337/documentation/openapi.json - ingress: - enabled: false - class: nginx - annotations: - kubernetes.io/tls-acme: "true" - cert-manager.io/cluster-issuer: letsencrypt-prod - hosts: - - host: mcp.galust.ai - paths: - - path: / - pathType: Prefix - backend: - servicePort: 4002 - tls: - - secretName: com-galust-mcp-tls2 - hosts: - - mcp.galust.ai - -mcpUseCase: - enabled: true - gatewayApi: - enabled: false - zeroTrustMesh: - enabled: false - fullnameOverride: ai-layer-mcp-use-case - version: 0.0.1 - appVersion: 0.0.1 - image: - repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-mcp-use-case - tag: latest - pullPolicy: Always - imagePullSecrets: *galustImagePullSecrets - replicaCount: 1 - labels: - version: - name: app-version - value: v0.0.1 - app: - name: app - value: ai-layer-mcp-use-case - service: - type: ClusterIP - port: 4002 - containerPort: 4002 - envFrom: - secret: ai-layer-mcp-use-case - config: - DEBUG_MCP_HEADER_FLOW: "true" - TOOLS_CATALOG_URL: https://api.galust.ai - ORCHESTRATOR_ENDPOINT: http://ai-layer-orchestrator:3000/api/galust/ask - -orchestrator: - enabled: true - gatewayApi: - enabled: false - zeroTrustMesh: - enabled: false - fullnameOverride: ai-layer-orchestrator - version: 0.0.1 - appVersion: 0.0.1 - image: - repository: 565580475168.dkr.ecr.eu-central-1.amazonaws.com/ai-layer-orchestrator - tag: latest - pullPolicy: Always - imagePullSecrets: *galustImagePullSecrets - replicaCount: 1 - labels: - version: - name: app-version - value: v0.0.1 - app: - name: app - value: ai-layer-orchestrator - service: - type: ClusterIP - port: 3000 - containerPort: 3000 - resources: - requests: - cpu: 500m - autoscaling: - enabled: true - minReplicas: 2 - maxReplicas: 5 - envFrom: - secret: ai-layer-orchestrator - config: - NODE_ENV: production - OPENAI_MODEL: gpt-4o-mini - QDRANT_URL: http://qdrant.dasmeta-customer-portal.svc.cluster.local:6333 - REDIS_URL: redis://redis:6379/0 - LANGFUSE_HOST: https://cloud.langfuse.com - GLOBAL_PREFIX: orchestrator - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://localhost:4318/v1/traces - AI_LAYER_BACKEND_URL: http://ai-layer-strapi:1337 - ingress: - enabled: false - class: nginx - annotations: - kubernetes.io/tls-acme: "true" - cert-manager.io/cluster-issuer: letsencrypt-prod - nginx.ingress.kubernetes.io/enable-cors: "true" - nginx.ingress.kubernetes.io/cors-allow-origin: https://app.galust.ai - nginx.ingress.kubernetes.io/cors-allow-credentials: "true" - nginx.ingress.kubernetes.io/cors-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS - nginx.ingress.kubernetes.io/cors-allow-headers: Authorization, Content-Type, Origin, Accept, X-Requested-With, MCP-Protocol-Version, Mcp-Session-Id - hosts: - - host: api.galust.ai - paths: - - path: /orchestrator - pathType: Prefix - backend: - servicePort: 3000 - tls: - - clusterIssuer: letsencrypt-prod - secretName: com-galust-api-tls2 - hosts: - - api.galust.ai From 38509c05a4e9f303685efc05000744ffa1000455 Mon Sep 17 00:00:00 2001 From: Julia A Date: Mon, 11 May 2026 18:27:57 +0400 Subject: [PATCH 3/7] fix(014-galust-umbrella-chart): Galust Imbrella Chart --- charts/galust-ai-layer/values.test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/galust-ai-layer/values.test.yaml b/charts/galust-ai-layer/values.test.yaml index 4717467..2f7892e 100644 --- a/charts/galust-ai-layer/values.test.yaml +++ b/charts/galust-ai-layer/values.test.yaml @@ -22,4 +22,4 @@ global: orchestrator: envFrom: - secret: ai-layer-orchestrator \ No newline at end of file + secret: ai-layer-orchestrator From 30f598aa1118e1c4f9ef5bb889d83260b2f5efd5 Mon Sep 17 00:00:00 2001 From: Julia A Date: Tue, 12 May 2026 15:02:53 +0400 Subject: [PATCH 4/7] fix(014-galust-umbrella-chart): Galust Umbrella Chart --- charts/galust-ai-layer/Chart.lock | 12 ++-- charts/galust-ai-layer/Chart.yaml | 8 +-- charts/galust-ai-layer/README.md | 57 +++++++++++++++--- charts/galust-ai-layer/charts/base-0.3.2.tgz | Bin 16565 -> 0 bytes charts/galust-ai-layer/charts/base-0.3.29.tgz | Bin 0 -> 44949 bytes charts/galust-ai-layer/values.yaml | 7 --- .../galust-ai-layer/values.test.yaml | 12 +--- 7 files changed, 59 insertions(+), 37 deletions(-) delete mode 100644 charts/galust-ai-layer/charts/base-0.3.2.tgz create mode 100644 charts/galust-ai-layer/charts/base-0.3.29.tgz rename {charts => examples}/galust-ai-layer/values.test.yaml (68%) diff --git a/charts/galust-ai-layer/Chart.lock b/charts/galust-ai-layer/Chart.lock index 859864e..84099ba 100644 --- a/charts/galust-ai-layer/Chart.lock +++ b/charts/galust-ai-layer/Chart.lock @@ -1,15 +1,15 @@ dependencies: - name: base repository: https://dasmeta.github.io/helm - version: 0.3.2 + version: 0.3.29 - name: base repository: https://dasmeta.github.io/helm - version: 0.3.2 + version: 0.3.29 - name: base repository: https://dasmeta.github.io/helm - version: 0.3.2 + version: 0.3.29 - name: base repository: https://dasmeta.github.io/helm - version: 0.3.2 -digest: sha256:34d0dda0c6459ceaaa2b2e45c9f334038b1763aa7b0683e5e241af5a96276961 -generated: "2026-05-11T14:46:40.654468+04:00" + version: 0.3.29 +digest: sha256:0f058458a7e18d9bb5c9b227c8a7d29712f572efa84f5fad9fa19e314d1d6ab5 +generated: "2026-05-12T09:59:51.72808+04:00" diff --git a/charts/galust-ai-layer/Chart.yaml b/charts/galust-ai-layer/Chart.yaml index c661d0a..2ec1f71 100644 --- a/charts/galust-ai-layer/Chart.yaml +++ b/charts/galust-ai-layer/Chart.yaml @@ -8,21 +8,21 @@ appVersion: "0.1.0" dependencies: - name: base alias: backend - version: 0.3.2 + version: 0.3.29 repository: https://dasmeta.github.io/helm condition: backend.enabled - name: base alias: mcp - version: 0.3.2 + version: 0.3.29 repository: https://dasmeta.github.io/helm condition: mcp.enabled - name: base alias: mcpUseCase - version: 0.3.2 + version: 0.3.29 repository: https://dasmeta.github.io/helm condition: mcpUseCase.enabled - name: base alias: orchestrator - version: 0.3.2 + version: 0.3.29 repository: https://dasmeta.github.io/helm condition: orchestrator.enabled diff --git a/charts/galust-ai-layer/README.md b/charts/galust-ai-layer/README.md index 7f619d9..b27e13d 100644 --- a/charts/galust-ai-layer/README.md +++ b/charts/galust-ai-layer/README.md @@ -56,6 +56,7 @@ Required default Kubernetes objects: | Docker registry pull secret | `ecr-secret` | all components | | Backend app secret | `ai-layer-strapi` | backend | | Backend DB host secret | `cnpg-main-urls`, key `host` | backend | +| Backend DB port | `backend.config.DATABASE_PORT`, default `5432` | backend | | Backend DB password secret | `cnpg-main-user`, key `password` | backend | | Backend uploads PVC | `ai-layer-strapi-uploads` | backend | | MCP secret | `ai-layer-mcp` | MCP | @@ -145,15 +146,18 @@ backend: - host: *apiHost ``` -Important: YAML anchors are resolved before Helm merges extra values files. If you deploy with `-f values.test.yaml` and only override `global`, Helm will not recalculate aliases already resolved in `values.yaml`. To change domains for another environment, either edit the full values file or provide a values file that also overrides the component `config` and `ingress` fields. +Important: YAML anchors are resolved before Helm merges extra values files. If you deploy with `-f examples/galust-ai-layer/values.test.yaml` and only override `global`, Helm will not recalculate aliases already resolved in `values.yaml`. To change domains for another environment, either edit the full values file or provide a values file that also overrides the component `config` and `ingress` fields. ## Image Pull Access -The default values expect an existing Kubernetes docker registry secret named `ecr-secret`: +The default values expect an existing Kubernetes docker registry secret named `ecr-secret`. The secret name is anchored once and reused by every component: ```yaml +imagePullSecret: + name: &imagePullSecretName ecr-secret + imagePullSecrets: - - name: ecr-secret + - name: *imagePullSecretName ``` The chart can also render the secret when `imagePullSecret.create=true`: @@ -166,7 +170,9 @@ helm upgrade --install galust-ai-layer ./charts/galust-ai-layer \ --set-file imagePullSecret.dockerConfigJson=./dockerconfig.json ``` -AWS IAM trust, ECR repository policies, role assumption, and External Secrets setup are intentionally outside this chart. Provide the resulting Kubernetes pull secret name through `imagePullSecrets` and each component's `imagePullSecrets` override. +AWS IAM trust, ECR repository policies, role assumption, and External Secrets setup are intentionally outside this chart. Provide the resulting Kubernetes pull secret name through `imagePullSecret.name` and each component's `imagePullSecrets` override. + +ECR authorization tokens expire. For long-running environments, prefer a cluster-managed renewal mechanism such as External Secrets, a registry credential controller, or a platform-owned refresh job. This chart can reference an existing pull secret or render one from provided docker config JSON, but it does not create AWS IAM credentials or install a token renewal controller. Example secret creation: @@ -182,15 +188,48 @@ kubectl create secret docker-registry ecr-secret \ ## Backend Notes -The backend values use the existing Galust Strapi image and runtime defaults from `ai-layer/backend/helm/strapi.yaml`, but this umbrella chart does not provision AWS IAM or a managed database. By default the backend expects: +The backend values use the existing Galust Strapi image and runtime defaults from `ai-layer/backend/helm/strapi.yaml`, but this umbrella chart does not provision AWS IAM or a managed database. Database secrets are created outside this chart. By default the backend expects: - an `ai-layer-strapi` secret for Strapi application secrets - `cnpg-main-urls` with key `host` +- `backend.config.DATABASE_PORT`, defaulting to `5432` - `cnpg-main-user` with key `password` - a PVC named `ai-layer-strapi-uploads` for `/opt/app/public/uploads` Override those names for environment-specific infrastructure. +If the database host and password are stored in one Kubernetes Secret, override the `extraEnv` references: + +```yaml +backend: + extraEnv: + DATABASE_HOST: + secretKeyRef: + name: my-database-secret + key: host + DATABASE_PASSWORD: + secretKeyRef: + name: my-database-secret + key: password +``` + +If the database port is also stored in the same CNPG URL secret, override `DATABASE_PORT` from `config` with an `extraEnv` secret reference: + +```yaml +backend: + config: + DATABASE_PORT: null + extraEnv: + DATABASE_HOST: + secretKeyRef: + name: cnpg-main-urls + key: host + DATABASE_PORT: + secretKeyRef: + name: cnpg-main-urls + key: port +``` + ## Deploy Run commands from the `helm` repository directory: @@ -208,13 +247,13 @@ helm upgrade --install galust-ai-layer charts/galust-ai-layer \ --create-namespace ``` -Deploy with the test values overlay: +Deploy with the test values example: ```bash helm upgrade --install galust-ai-layer charts/galust-ai-layer \ -n ai-layer \ --create-namespace \ - -f charts/galust-ai-layer/values.test.yaml + -f examples/galust-ai-layer/values.test.yaml ``` Disable a component: @@ -255,7 +294,7 @@ helm dependency update charts/galust-ai-layer helm lint charts/galust-ai-layer helm template galust-ai-layer charts/galust-ai-layer -n ai-layer helm template galust-ai-layer charts/galust-ai-layer -n ai-layer --set backend.enabled=false -helm template galust-ai-layer charts/galust-ai-layer -n ai-layer -f charts/galust-ai-layer/values.test.yaml +helm template galust-ai-layer charts/galust-ai-layer -n ai-layer -f examples/galust-ai-layer/values.test.yaml ``` ## Troubleshooting @@ -269,5 +308,5 @@ If ingress does not work, confirm the ingress controller, DNS records, TLS secre If URL overrides do not appear in rendered manifests, remember that YAML anchors are not dynamic Helm templates. Render locally with: ```bash -helm template galust-ai-layer charts/galust-ai-layer -n ai-layer -f charts/galust-ai-layer/values.test.yaml +helm template galust-ai-layer charts/galust-ai-layer -n ai-layer -f examples/galust-ai-layer/values.test.yaml ``` diff --git a/charts/galust-ai-layer/charts/base-0.3.2.tgz b/charts/galust-ai-layer/charts/base-0.3.2.tgz deleted file mode 100644 index 7ec96b7847d2e518e3cd203858afc56aeec3d11f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16565 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 diff --git a/charts/galust-ai-layer/charts/base-0.3.29.tgz b/charts/galust-ai-layer/charts/base-0.3.29.tgz new file mode 100644 index 0000000000000000000000000000000000000000..e96111f3b590151445ab494b268a99cfd1194ec5 GIT binary patch literal 44949 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 literal 0 HcmV?d00001 diff --git a/charts/galust-ai-layer/values.yaml b/charts/galust-ai-layer/values.yaml index 9e1d073..73bf892 100644 --- a/charts/galust-ai-layer/values.yaml +++ b/charts/galust-ai-layer/values.yaml @@ -1,10 +1,3 @@ -imagePullSecret: - create: false - name: ecr-secret - annotations: {} - dockerConfigJson: "" - dockerConfigJsonBase64: "" - imagePullSecrets: &galustImagePullSecrets - name: ecr-secret diff --git a/charts/galust-ai-layer/values.test.yaml b/examples/galust-ai-layer/values.test.yaml similarity index 68% rename from charts/galust-ai-layer/values.test.yaml rename to examples/galust-ai-layer/values.test.yaml index 2f7892e..fd41322 100644 --- a/charts/galust-ai-layer/values.test.yaml +++ b/examples/galust-ai-layer/values.test.yaml @@ -1,14 +1,4 @@ -# Minimal test overlay for externally reachable Galust AI layer URLs. -imagePullSecret: - create: false - name: ecr-secret - annotations: {} - dockerConfigJson: "" - dockerConfigJsonBase64: "" - -imagePullSecrets: &galustImagePullSecrets - - name: ecr-secret - +# Minimal example overlay for externally reachable Galust AI layer URLs. global: API_URL: &apiUrl https://api.galust.ai API_HOST: &apiHost api.galust.ai From 1a079f1a7e1a3000db6fc32b0a388fea1fbf5297 Mon Sep 17 00:00:00 2001 From: Julia A Date: Tue, 12 May 2026 15:03:36 +0400 Subject: [PATCH 5/7] fix(014-galust-umbrella-chart): Galust Umbrella Chart --- specs/014-galust-umbrella-chart/plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/014-galust-umbrella-chart/plan.md b/specs/014-galust-umbrella-chart/plan.md index 8001ea4..e82204e 100644 --- a/specs/014-galust-umbrella-chart/plan.md +++ b/specs/014-galust-umbrella-chart/plan.md @@ -10,7 +10,7 @@ Add `charts/galust-ai-layer` as an umbrella chart that deploys the Galust AI lay ## Technical Context **Language/Version**: Helm 3 chart YAML and Go templates -**Primary Dependencies**: Published `dasmeta/base` chart version `0.3.2`, matching the `ai-layer` deployment workflows +**Primary Dependencies**: Published `dasmeta/base` chart version `0.3.29`, matching the current shared chart release **Storage**: Backend uploads PVC, default `ai-layer-strapi-uploads` **Testing**: `helm dependency update`, `helm lint`, `helm template` **Target Platform**: Kubernetes / EKS From 22f81010f4b9ffb246423eff6a19379beec397eb Mon Sep 17 00:00:00 2001 From: Julia A Date: Tue, 12 May 2026 15:11:51 +0400 Subject: [PATCH 6/7] fix(014-galust-umbrella-chart): Galust Umbrella Chart --- charts/galust-ai-layer/README.md | 16 ++++++++-------- charts/galust-ai-layer/values.yaml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/charts/galust-ai-layer/README.md b/charts/galust-ai-layer/README.md index b27e13d..7780c8f 100644 --- a/charts/galust-ai-layer/README.md +++ b/charts/galust-ai-layer/README.md @@ -55,9 +55,9 @@ Required default Kubernetes objects: | --- | --- | --- | | Docker registry pull secret | `ecr-secret` | all components | | Backend app secret | `ai-layer-strapi` | backend | -| Backend DB host secret | `cnpg-main-urls`, key `host` | backend | +| Backend DB host secret | `db-ai-layer-strapi`, key `host` | backend | | Backend DB port | `backend.config.DATABASE_PORT`, default `5432` | backend | -| Backend DB password secret | `cnpg-main-user`, key `password` | backend | +| Backend DB password secret | `db-ai-layer-strapi`, key `password` | backend | | Backend uploads PVC | `ai-layer-strapi-uploads` | backend | | MCP secret | `ai-layer-mcp` | MCP | | MCP use-case secret | `ai-layer-mcp-use-case` | MCP use-case | @@ -191,9 +191,9 @@ kubectl create secret docker-registry ecr-secret \ The backend values use the existing Galust Strapi image and runtime defaults from `ai-layer/backend/helm/strapi.yaml`, but this umbrella chart does not provision AWS IAM or a managed database. Database secrets are created outside this chart. By default the backend expects: - an `ai-layer-strapi` secret for Strapi application secrets -- `cnpg-main-urls` with key `host` +- `db-ai-layer-strapi` with key `host` - `backend.config.DATABASE_PORT`, defaulting to `5432` -- `cnpg-main-user` with key `password` +- `db-ai-layer-strapi` with key `password` - a PVC named `ai-layer-strapi-uploads` for `/opt/app/public/uploads` Override those names for environment-specific infrastructure. @@ -213,7 +213,7 @@ backend: key: password ``` -If the database port is also stored in the same CNPG URL secret, override `DATABASE_PORT` from `config` with an `extraEnv` secret reference: +If the database port is also stored in the same database secret, override `DATABASE_PORT` from `config` with an `extraEnv` secret reference: ```yaml backend: @@ -222,11 +222,11 @@ backend: extraEnv: DATABASE_HOST: secretKeyRef: - name: cnpg-main-urls + name: db-ai-layer-strapi key: host DATABASE_PORT: secretKeyRef: - name: cnpg-main-urls + name: db-ai-layer-strapi key: port ``` @@ -301,7 +301,7 @@ helm template galust-ai-layer charts/galust-ai-layer -n ai-layer -f examples/gal If pods are stuck in `ImagePullBackOff`, check the `ecr-secret` secret and ECR access. -If the backend fails to start, check the `ai-layer-strapi`, `cnpg-main-urls`, and `cnpg-main-user` secrets, plus database reachability from the namespace. +If the backend fails to start, check the `ai-layer-strapi` and `db-ai-layer-strapi` secrets, plus database reachability from the namespace. If ingress does not work, confirm the ingress controller, DNS records, TLS secret or cert-manager issuer, and rendered ingress hosts. diff --git a/charts/galust-ai-layer/values.yaml b/charts/galust-ai-layer/values.yaml index 73bf892..a11b1d0 100644 --- a/charts/galust-ai-layer/values.yaml +++ b/charts/galust-ai-layer/values.yaml @@ -92,11 +92,11 @@ backend: extraEnv: DATABASE_HOST: secretKeyRef: - name: cnpg-main-urls + name: db-ai-layer-strapi key: host DATABASE_PASSWORD: secretKeyRef: - name: cnpg-main-user + name: db-ai-layer-strapi key: password ingress: enabled: true From 8ac82afa06173665a0b23ccaf1155f471f95b5f8 Mon Sep 17 00:00:00 2001 From: Julia A Date: Tue, 12 May 2026 17:28:44 +0400 Subject: [PATCH 7/7] fix(014-galust-umbrella-chart): Galus AI layer --- charts/galust-ai-layer/README.md | 67 +++++++- charts/galust-ai-layer/templates/NOTES.txt | 1 + charts/galust-ai-layer/templates/_helpers.tpl | 14 ++ .../templates/ecr-credentials-refresh.yaml | 143 ++++++++++++++++++ charts/galust-ai-layer/values.yaml | 34 ++++- examples/galust-ai-layer/values.test.yaml | 14 +- 6 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 charts/galust-ai-layer/templates/ecr-credentials-refresh.yaml diff --git a/charts/galust-ai-layer/README.md b/charts/galust-ai-layer/README.md index 7780c8f..68b0071 100644 --- a/charts/galust-ai-layer/README.md +++ b/charts/galust-ai-layer/README.md @@ -40,8 +40,10 @@ Before deploying, confirm the target cluster has: - Kubernetes access through a working `kubectl` context. - Helm 3 installed locally. - Access to the `dasmeta` Helm repository, because this chart depends on `dasmeta/base`. +- AWS access to the target account, usually through an AWS SSO permission set and account assignment managed outside this chart. - Namespace access for `ai-layer`, or permission to create it. - Image pull access for the private ECR images. +- ECR read access for the private repositories used by the backend, MCP, MCP use-case, and orchestrator images. - Required application secrets already created in the namespace. - Database connectivity for the backend. - A PVC or storage class suitable for backend uploads. @@ -49,6 +51,13 @@ Before deploying, confirm the target cluster has: - DNS records pointing the public hosts to the ingress/load balancer. - TLS/cert-manager setup if the default TLS annotations and secrets are used. +If AWS access is managed through the Terraform SSO/RBAC modules, create or assign an AWS SSO permission set before deploying this chart. The permission set should allow the operator or automation role to: + +- Read private ECR repositories and get ECR authorization tokens. +- Access the target EKS cluster and update Kubernetes resources in the `ai-layer` namespace. +- Create or update Kubernetes Secrets used by the chart, including `ecr-secret`, `ai-layer-strapi`, `db-ai-layer-strapi`, `ai-layer-mcp`, `ai-layer-mcp-use-case`, and `ai-layer-orchestrator`. +- If `ecrCredentialsRefresh.enabled=true`, provide an AWS identity for the refresh job with `ecr:GetAuthorizationToken`. + Required default Kubernetes objects: | Object | Default name | Used by | @@ -172,7 +181,7 @@ helm upgrade --install galust-ai-layer ./charts/galust-ai-layer \ AWS IAM trust, ECR repository policies, role assumption, and External Secrets setup are intentionally outside this chart. Provide the resulting Kubernetes pull secret name through `imagePullSecret.name` and each component's `imagePullSecrets` override. -ECR authorization tokens expire. For long-running environments, prefer a cluster-managed renewal mechanism such as External Secrets, a registry credential controller, or a platform-owned refresh job. This chart can reference an existing pull secret or render one from provided docker config JSON, but it does not create AWS IAM credentials or install a token renewal controller. +ECR authorization tokens expire. For long-running environments, enable the optional refresh CronJob or use another cluster-managed renewal mechanism such as External Secrets, a registry credential controller, or a platform-owned refresh job. This chart can reference an existing pull secret, render one from provided docker config JSON, or refresh the secret through the optional CronJob. Example secret creation: @@ -186,6 +195,62 @@ kubectl create secret docker-registry ecr-secret \ --docker-password='' ``` +## ECR Credentials Refresh + +The chart can render an optional CronJob that refreshes the ECR docker registry secret before the token expires: + +```yaml +ecrCredentialsRefresh: + enabled: true + schedule: "0 */6 * * *" + registry: 565580475168.dkr.ecr.eu-central-1.amazonaws.com + region: eu-central-1 + secretName: ecr-secret +``` + +The refresh job updates the same `kubernetes.io/dockerconfigjson` Secret referenced by component `imagePullSecrets`. It creates a namespaced `Role`, `RoleBinding`, `ServiceAccount`, and `CronJob`. + +Use an existing Kubernetes Secret with AWS credentials: + +```bash +kubectl create secret generic ecr-refresh-aws-credentials \ + -n ai-layer \ + --from-literal=AWS_ACCESS_KEY_ID='' \ + --from-literal=AWS_SECRET_ACCESS_KEY='' \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Then enable the refresher: + +```bash +helm upgrade --install galust-ai-layer charts/galust-ai-layer \ + -n ai-layer \ + --create-namespace \ + --set ecrCredentialsRefresh.enabled=true \ + --set ecrCredentialsRefresh.awsCredentialsSecret.name=ecr-refresh-aws-credentials +``` + +If the cluster uses IRSA or EKS Pod Identity, leave `awsCredentialsSecret.name` empty and annotate the refresh service account: + +```yaml +ecrCredentialsRefresh: + enabled: true + serviceAccount: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam:::role/ +``` + +The AWS identity used by the refresh job needs permission to call `ecr:GetAuthorizationToken`. The refresh container uses `alpine` and installs `aws-cli`, `curl`, `ca-certificates`, and `coreutils` at runtime, so the namespace must allow outbound package download access or you must override `ecrCredentialsRefresh.image` with an internal image that already contains the required tools. + +After installing, trigger the first refresh before private-image workloads need to pull: + +```bash +kubectl create job \ + -n ai-layer \ + --from=cronjob/galust-ai-layer-ecr-refresh \ + galust-ai-layer-ecr-refresh-manual +``` + ## Backend Notes The backend values use the existing Galust Strapi image and runtime defaults from `ai-layer/backend/helm/strapi.yaml`, but this umbrella chart does not provision AWS IAM or a managed database. Database secrets are created outside this chart. By default the backend expects: diff --git a/charts/galust-ai-layer/templates/NOTES.txt b/charts/galust-ai-layer/templates/NOTES.txt index c7b6135..f7b3194 100644 --- a/charts/galust-ai-layer/templates/NOTES.txt +++ b/charts/galust-ai-layer/templates/NOTES.txt @@ -17,4 +17,5 @@ Enabled components: Image pull access: - By default the chart references an existing imagePullSecret named {{ include "galust-ai-layer.imagePullSecretName" . }}. - Set imagePullSecret.create=true with dockerConfigJson or dockerConfigJsonBase64 to render that Secret from this chart. +- Set ecrCredentialsRefresh.enabled=true to render a CronJob that refreshes the ECR docker registry secret. - AWS IAM trust, ECR repository policy, and external secret provisioning are outside this chart. diff --git a/charts/galust-ai-layer/templates/_helpers.tpl b/charts/galust-ai-layer/templates/_helpers.tpl index 45e93fd..e4d07bc 100644 --- a/charts/galust-ai-layer/templates/_helpers.tpl +++ b/charts/galust-ai-layer/templates/_helpers.tpl @@ -38,3 +38,17 @@ Image pull secret name used by the optional generated dockerconfigjson Secret. {{- define "galust-ai-layer.imagePullSecretName" -}} {{- default "ecr-secret" .Values.imagePullSecret.name | trunc 63 | trimSuffix "-" }} {{- end }} + +{{/* +Name used for the optional ECR credentials refresh resources. +*/}} +{{- define "galust-ai-layer.ecrCredentialsRefreshName" -}} +{{- printf "%s-ecr-refresh" (include "galust-ai-layer.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Service account name used by the optional ECR credentials refresh job. +*/}} +{{- define "galust-ai-layer.ecrCredentialsRefreshServiceAccountName" -}} +{{- default (include "galust-ai-layer.ecrCredentialsRefreshName" .) .Values.ecrCredentialsRefresh.serviceAccount.name | trunc 63 | trimSuffix "-" }} +{{- end }} diff --git a/charts/galust-ai-layer/templates/ecr-credentials-refresh.yaml b/charts/galust-ai-layer/templates/ecr-credentials-refresh.yaml new file mode 100644 index 0000000..1774674 --- /dev/null +++ b/charts/galust-ai-layer/templates/ecr-credentials-refresh.yaml @@ -0,0 +1,143 @@ +{{- if .Values.ecrCredentialsRefresh.enabled -}} +{{- $name := include "galust-ai-layer.ecrCredentialsRefreshName" . -}} +{{- $serviceAccountName := include "galust-ai-layer.ecrCredentialsRefreshServiceAccountName" . -}} +{{- $secretName := required "ecrCredentialsRefresh.secretName is required when ecrCredentialsRefresh.enabled=true" .Values.ecrCredentialsRefresh.secretName -}} +{{- $registry := required "ecrCredentialsRefresh.registry is required when ecrCredentialsRefresh.enabled=true" .Values.ecrCredentialsRefresh.registry -}} +{{- $region := required "ecrCredentialsRefresh.region is required when ecrCredentialsRefresh.enabled=true" .Values.ecrCredentialsRefresh.region -}} +{{- if .Values.ecrCredentialsRefresh.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ $serviceAccountName }} + labels: + {{- include "galust-ai-layer.labels" . | nindent 4 }} + {{- with .Values.ecrCredentialsRefresh.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +--- +{{- end }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ $name }} + labels: + {{- include "galust-ai-layer.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - create + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ $name }} + labels: + {{- include "galust-ai-layer.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: {{ $serviceAccountName }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ $name }} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ $name }} + labels: + {{- include "galust-ai-layer.labels" . | nindent 4 }} +spec: + schedule: {{ .Values.ecrCredentialsRefresh.schedule | quote }} + concurrencyPolicy: {{ .Values.ecrCredentialsRefresh.concurrencyPolicy }} + successfulJobsHistoryLimit: {{ .Values.ecrCredentialsRefresh.successfulJobsHistoryLimit }} + failedJobsHistoryLimit: {{ .Values.ecrCredentialsRefresh.failedJobsHistoryLimit }} + jobTemplate: + spec: + template: + metadata: + labels: + {{- include "galust-ai-layer.labels" . | nindent 12 }} + spec: + serviceAccountName: {{ $serviceAccountName }} + restartPolicy: OnFailure + containers: + - name: refresh + image: "{{ .Values.ecrCredentialsRefresh.image.repository }}:{{ .Values.ecrCredentialsRefresh.image.tag }}" + imagePullPolicy: {{ .Values.ecrCredentialsRefresh.image.pullPolicy }} + env: + - name: AWS_REGION + value: {{ $region | quote }} + - name: ECR_REGISTRY + value: {{ $registry | quote }} + - name: DOCKER_SECRET_NAME + value: {{ $secretName | quote }} + - name: TARGET_NAMESPACE + value: {{ .Release.Namespace | quote }} + {{- with .Values.ecrCredentialsRefresh.awsCredentialsSecret.name }} + envFrom: + - secretRef: + name: {{ . }} + {{- end }} + command: + - /bin/sh + - -ec + args: + - | + apk add --no-cache aws-cli curl ca-certificates coreutils + + ECR_PASSWORD="$(aws ecr get-login-password --region "${AWS_REGION}")" + AUTH="$(printf 'AWS:%s' "${ECR_PASSWORD}" | base64 | tr -d '\n')" + DOCKER_CONFIG_JSON="$(printf '{"auths":{"%s":{"username":"AWS","password":"%s","auth":"%s"}}}' "${ECR_REGISTRY}" "${ECR_PASSWORD}" "${AUTH}")" + DOCKER_CONFIG_B64="$(printf '%s' "${DOCKER_CONFIG_JSON}" | base64 | tr -d '\n')" + + cat >/tmp/secret-create.json </tmp/secret-patch.json <