From f528f30e175c24559828a1184d6a8379f1788758 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Fri, 17 Oct 2025 16:30:54 -0700 Subject: [PATCH 01/17] working helm chart, de-netifly ui --- .dockerignore | 5 + .gitignore | 2 + Dockerfile_ui | 47 +++ build-all-images.sh | 131 ++++++ helm-charts/README.md | 382 +++++++++++++----- helm-charts/digger-backend/Chart.yaml | 2 +- .../templates/backend-deployment.yaml | 86 ++-- .../templates/digger-secret.yaml | 20 - .../templates/postgres-secret.yaml | 13 - .../templates/postgres-service.yaml | 12 - .../templates/postgres-statefulset.yaml | 35 -- helm-charts/digger-backend/values.yaml | 127 ++---- helm-charts/digger-drift/.helmignore | 27 ++ helm-charts/digger-drift/Chart.yaml | 8 + .../digger-drift/templates/_helpers.tpl | 59 +++ .../digger-drift/templates/deployment.yaml | 208 ++++++++++ .../digger-drift/templates/ingress.yaml | 34 ++ .../digger-drift/templates/service.yaml | 16 + helm-charts/digger-drift/values.yaml | 121 ++++++ helm-charts/opentaco/.helmignore | 24 ++ helm-charts/opentaco/Chart.lock | 18 + helm-charts/opentaco/Chart.yaml | 67 +++ .../opentaco/charts/digger-backend-0.1.12.tgz | Bin 0 -> 2589 bytes helm-charts/opentaco/charts/drift-0.1.0.tgz | Bin 0 -> 3563 bytes .../opentaco/charts/postgresql-15.5.38.tgz | Bin 0 -> 75781 bytes .../opentaco/charts/statesman-0.1.0.tgz | Bin 0 -> 2696 bytes helm-charts/opentaco/charts/ui-0.1.0.tgz | Bin 0 -> 3067 bytes helm-charts/opentaco/templates/NOTES.txt | 80 ++++ helm-charts/opentaco/templates/_helpers.tpl | 76 ++++ helm-charts/opentaco/values-production.yaml | 133 ++++++ helm-charts/opentaco/values-test.yaml | 78 ++++ helm-charts/opentaco/values.yaml | 227 +++++++++++ .../secrets-example/digger-backend.env | 41 ++ helm-charts/secrets-example/drift.env | 19 + helm-charts/secrets-example/statesman.env | 34 ++ helm-charts/secrets-example/ui.env | 26 ++ helm-charts/taco-statesman/Chart.yaml | 4 +- helm-charts/taco-statesman/README.md | 36 -- .../taco-statesman/templates/deployment.yaml | 38 ++ helm-charts/taco-statesman/values.yaml | 6 +- helm-charts/taco-ui/.helmignore | 27 ++ helm-charts/taco-ui/Chart.yaml | 8 + helm-charts/taco-ui/templates/_helpers.tpl | 59 +++ helm-charts/taco-ui/templates/deployment.yaml | 112 +++++ helm-charts/taco-ui/templates/ingress.yaml | 42 ++ helm-charts/taco-ui/templates/service.yaml | 16 + .../taco-ui/tests/deployment_test.yaml | 44 ++ helm-charts/taco-ui/values.yaml | 81 ++++ ui/package.json | 5 +- ui/server-start.js | 102 +++++ ui/src/routeTree.gen.ts | 38 +- .../dashboard/projects.$projectid.tsx | 6 +- .../_dashboard/dashboard/projects.index.tsx | 2 +- ui/vite.config.ts | 4 +- 54 files changed, 2398 insertions(+), 390 deletions(-) create mode 100644 Dockerfile_ui create mode 100755 build-all-images.sh delete mode 100644 helm-charts/digger-backend/templates/digger-secret.yaml delete mode 100644 helm-charts/digger-backend/templates/postgres-secret.yaml delete mode 100644 helm-charts/digger-backend/templates/postgres-service.yaml delete mode 100644 helm-charts/digger-backend/templates/postgres-statefulset.yaml create mode 100644 helm-charts/digger-drift/.helmignore create mode 100644 helm-charts/digger-drift/Chart.yaml create mode 100644 helm-charts/digger-drift/templates/_helpers.tpl create mode 100644 helm-charts/digger-drift/templates/deployment.yaml create mode 100644 helm-charts/digger-drift/templates/ingress.yaml create mode 100644 helm-charts/digger-drift/templates/service.yaml create mode 100644 helm-charts/digger-drift/values.yaml create mode 100644 helm-charts/opentaco/.helmignore create mode 100644 helm-charts/opentaco/Chart.lock create mode 100644 helm-charts/opentaco/Chart.yaml create mode 100644 helm-charts/opentaco/charts/digger-backend-0.1.12.tgz create mode 100644 helm-charts/opentaco/charts/drift-0.1.0.tgz create mode 100644 helm-charts/opentaco/charts/postgresql-15.5.38.tgz create mode 100644 helm-charts/opentaco/charts/statesman-0.1.0.tgz create mode 100644 helm-charts/opentaco/charts/ui-0.1.0.tgz create mode 100644 helm-charts/opentaco/templates/NOTES.txt create mode 100644 helm-charts/opentaco/templates/_helpers.tpl create mode 100644 helm-charts/opentaco/values-production.yaml create mode 100644 helm-charts/opentaco/values-test.yaml create mode 100644 helm-charts/opentaco/values.yaml create mode 100644 helm-charts/secrets-example/digger-backend.env create mode 100644 helm-charts/secrets-example/drift.env create mode 100644 helm-charts/secrets-example/statesman.env create mode 100644 helm-charts/secrets-example/ui.env delete mode 100644 helm-charts/taco-statesman/README.md create mode 100644 helm-charts/taco-ui/.helmignore create mode 100644 helm-charts/taco-ui/Chart.yaml create mode 100644 helm-charts/taco-ui/templates/_helpers.tpl create mode 100644 helm-charts/taco-ui/templates/deployment.yaml create mode 100644 helm-charts/taco-ui/templates/ingress.yaml create mode 100644 helm-charts/taco-ui/templates/service.yaml create mode 100644 helm-charts/taco-ui/tests/deployment_test.yaml create mode 100644 helm-charts/taco-ui/values.yaml create mode 100644 ui/server-start.js diff --git a/.dockerignore b/.dockerignore index e872a73e5..d6c4cf642 100644 --- a/.dockerignore +++ b/.dockerignore @@ -61,3 +61,8 @@ libs/digger_config/**/.idea # flyctl launch added from libs/orchestrator/.gitignore libs/orchestrator/**/.idea + +# Don't copy node_modules - we install fresh in the container +ui/node_modules +ui/dist +ui/.next diff --git a/.gitignore b/.gitignore index bde0c9c51..38384ad25 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ data/ taco/data/ +.registry-config +.secrets/ \ No newline at end of file diff --git a/Dockerfile_ui b/Dockerfile_ui new file mode 100644 index 000000000..e27bc9953 --- /dev/null +++ b/Dockerfile_ui @@ -0,0 +1,47 @@ +# Multi-stage build for Taco UI +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY ui/package.json ./ + +# Install dependencies first +# Explicitly tell npm to install for linux/amd64 platform (for cloud deployment) +# This ensures correct optional dependencies like @rollup/rollup-linux-x64-musl +RUN npm install --cpu=x64 --os=linux --libc=musl && \ + npm cache clean --force + +# Copy source code (node_modules excluded via .dockerignore) +COPY ui/ ./ + +# Build the application for linux/amd64 (with node adapter) +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package file +COPY ui/package.json ./ + +# Install production dependencies only for linux/amd64 +RUN npm install --omit=dev --cpu=x64 --os=linux --libc=musl && \ + npm cache clean --force + +# Copy built application and server entry from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/server-start.js ./ + +# Expose port +EXPOSE 3030 + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3030 +ENV HOST=0.0.0.0 + +# Start the Node.js production server +CMD ["node", "server-start.js"] + diff --git a/build-all-images.sh b/build-all-images.sh new file mode 100755 index 000000000..2536e3ff8 --- /dev/null +++ b/build-all-images.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# Build all Docker images for Digger/Taco services +# +# IMPORTANT: Images are built locally and NOT pushed automatically. +# You control where/if they get pushed. +# +# Usage: +# ./build-all-images.sh [REGISTRY] [VERSION] +# +# Examples: +# ./build-all-images.sh us-central1-docker.pkg.dev/my-project/digger v0.1.0 # GCP Artifact Registry (private) +# ./build-all-images.sh ghcr.io/my-org v0.1.0 # GitHub (can be private/public) +# ./build-all-images.sh # Just tag locally, don't specify registry +# +set -e + +# Configuration - YOU MUST SET YOUR REGISTRY! +if [ -z "$1" ]; then + echo "ERROR: Registry not specified!" + echo "" + echo "Usage: $0 REGISTRY [VERSION]" + echo "" + echo "For GCP (private by default):" + echo " $0 REGION-docker.pkg.dev/PROJECT/REPO v0.1.0" + echo "" + echo "For GitHub Container Registry (set to private in settings):" + echo " $0 ghcr.io/YOUR_ORG v0.1.0" + echo "" + exit 1 +fi + +REGISTRY="${1}" +VERSION="${2:-v0.1.0}" +COMMIT_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown") + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Building Digger/Taco Docker Images ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" +echo "" +echo -e "${YELLOW}Registry:${NC} $REGISTRY" +echo -e "${YELLOW}Version:${NC} $VERSION" +echo -e "${YELLOW}Commit:${NC} $COMMIT_SHA" +echo -e "${YELLOW}Platform:${NC} linux/amd64 (for GKE/GCP)" +echo "" +echo -e "${YELLOW}Note:${NC} Building on ARM64 Mac for AMD64 deployment" +echo -e " This ensures images run correctly in GCP/GKE" +echo "" + +# Build drift +echo -e "${YELLOW}[1/3] Building drift service...${NC}" +docker build \ + --platform linux/amd64 \ + -t ${REGISTRY}/drift:${VERSION} \ + -t ${REGISTRY}/drift:latest \ + --build-arg COMMIT_SHA=${COMMIT_SHA} \ + -f Dockerfile_drift \ + . +echo -e "${GREEN}✓ Drift built${NC}" +echo "" + +# Build taco-statesman +echo -e "${YELLOW}[2/3] Building taco-statesman...${NC}" +cd taco +docker build \ + --platform linux/amd64 \ + -t ${REGISTRY}/taco-statesman:${VERSION} \ + -t ${REGISTRY}/taco-statesman:latest \ + --build-arg COMMIT_SHA=${COMMIT_SHA} \ + -f Dockerfile_statesman \ + . +cd .. +echo -e "${GREEN}✓ Taco-statesman built${NC}" +echo "" + +# Build taco-ui (standalone Node.js SSR app - no Netlify!) +echo -e "${YELLOW}[3/3] Building taco-ui (Node.js + TanStack Start)...${NC}" +docker build \ + --platform linux/amd64 \ + -t ${REGISTRY}/taco-ui:${VERSION} \ + -t ${REGISTRY}/taco-ui:latest \ + --build-arg COMMIT_SHA=${COMMIT_SHA} \ + -f Dockerfile_ui \ + . +echo -e "${GREEN}✓ Taco-ui built (standalone Node.js, no Netlify dependencies)${NC}" +echo "" + +echo -e "${GREEN}═══════════════════════════════════════${NC}" +echo -e "${GREEN}All images built successfully!${NC}" +echo -e "${GREEN}═══════════════════════════════════════${NC}" +echo "" +echo -e "${YELLOW}Built images:${NC}" +echo " • ${REGISTRY}/drift:${VERSION}" +echo " • ${REGISTRY}/taco-statesman:${VERSION}" +echo " • ${REGISTRY}/taco-ui:${VERSION} (standalone Node.js + SSR)" +echo "" +echo -e "${YELLOW}Image Details:${NC}" +echo " • drift: Go-based drift detection service" +echo " • taco-statesman: Go-based IaC orchestration service" +echo " • taco-ui: React SSR app (TanStack Start, no Netlify)" +echo "" +echo -e "${YELLOW}IMPORTANT: Images are built locally only.${NC}" +echo -e "${YELLOW}They are NOT automatically pushed to any registry.${NC}" +echo "" +echo -e "${YELLOW}To push to your PRIVATE registry:${NC}" +echo "" +echo " # First, authenticate (if not already done):" +echo " gcloud auth configure-docker ${REGISTRY%%/*} # For GCP" +echo " # OR" +echo " docker login ${REGISTRY%%/*} # For other registries" +echo "" +echo " # Then push:" +echo " docker push ${REGISTRY}/drift:${VERSION}" +echo " docker push ${REGISTRY}/drift:latest" +echo " docker push ${REGISTRY}/taco-statesman:${VERSION}" +echo " docker push ${REGISTRY}/taco-statesman:latest" +echo " docker push ${REGISTRY}/taco-ui:${VERSION}" +echo " docker push ${REGISTRY}/taco-ui:latest" +echo "" +echo -e "${YELLOW}Or push all at once:${NC}" +cat <" - bearerAuthToken: "" - hostname: "digger.example.com" - - # GitHub App credentials (filled after setup) - githubOrg: "" - githubAppID: "" # Note: uppercase ID - githubAppClientID: "" - githubAppClientSecret: "" - githubAppKeyFile: "" # base64 encoded private key - githubWebhookSecret: "" - - # PostgreSQL configuration - postgres: - user: "postgres" - database: "digger" - host: "your-postgres-host" - password: "" - - # Resource limits (optional) - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi -``` - -### Database Options - -#### Option 1: External PostgreSQL (Recommended for production) -```yaml -digger: - postgres: - user: "digger" - database: "digger" - host: "postgresql.example.com" - password: "your-secure-password" - sslmode: "require" # or "disable" for non-SSL connections +### 4. Update Secrets + +```bash +# Delete old secret +kubectl delete secret statesman-secrets -n opentaco + +# Recreate with new values +kubectl create secret generic statesman-secrets \ + --from-env-file=.secrets/statesman.env -n opentaco + +# Restart pods to pick up changes +kubectl delete pods -l app.kubernetes.io/name=statesman -n opentaco ``` -#### Option 2: Built-in PostgreSQL (Testing only) +## Cloud SQL Setup + +Statesman uses Google Cloud SQL for database. Backend and Drift can use external databases (Supabase, etc.) or Cloud SQL. + +### 1. Create Cloud SQL Instance + +```bash +gcloud sql instances create taco-postgres \ + --database-version=POSTGRES_15 \ + --tier=db-f1-micro \ + --region=us-central1 \ + --database-flags=max_connections=100 +``` + +### 2. Create Database + +```bash +gcloud sql databases create taco \ + --instance=taco-postgres +``` + +### 3. Create Service Account + +```bash +# Create service account for Cloud SQL proxy +gcloud iam service-accounts create cloudsql-sa \ + --display-name="Cloud SQL Proxy Service Account" + +# Grant Cloud SQL Client role +gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \ + --member="serviceAccount:cloudsql-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/cloudsql.client" + +# Create and download key +gcloud iam service-accounts keys create cloudsql-key.json \ + --iam-account=cloudsql-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com +``` + +### 4. Create Kubernetes Secret for Cloud SQL + +```bash +kubectl create secret generic cloudsql-credentials \ + --from-file=credentials.json=cloudsql-key.json \ + -n opentaco +``` + +### 5. Configure in values-test.yaml + ```yaml -postgres: +statesman: enabled: true - secret: - postgresPassword: "" + taco: + cloudSql: + enabled: true + instanceConnectionName: "PROJECT_ID:REGION:INSTANCE_NAME" # e.g., "dev-XXXXXXX:us-west2:taco-postgres" + credentialsSecret: "cloudsql-credentials" ``` -### Using Existing Secrets +### 6. Set Database Connection in statesman.env -Instead of putting secrets in values.yaml, reference an existing Kubernetes secret: +```bash +# Cloud SQL uses localhost via proxy sidecar +OPENTACO_POSTGRES_HOST=localhost +OPENTACO_POSTGRES_PORT=5432 +OPENTACO_POSTGRES_USER=postgres +OPENTACO_POSTGRES_PASSWORD=YOUR_DB_PASSWORD +OPENTACO_POSTGRES_DBNAME=taco +OPENTACO_QUERY_BACKEND=postgres +``` -```yaml -digger: - secret: - useExistingSecret: true - existingSecretName: "digger-secrets" +The Cloud SQL proxy runs as a sidecar container, connecting to your Cloud SQL instance and exposing it on localhost:5432. + +## Deployment + +### Test Environment + +```bash +cd opentaco +helm install opentaco . -f values-test.yaml -n opentaco +``` + +### Production Environment + +```bash +# Review and customize production values +vim opentaco/values-production.yaml + +# Deploy +helm install opentaco . -f values-production.yaml -n opentaco +``` + +### Verify Deployment + +```bash +# Check pods +kubectl get pods -n opentaco + +# Check logs +kubectl logs -f deployment/opentaco-statesman -n opentaco -c statesman + +# Access UI locally +kubectl port-forward svc/opentaco-ui 3030:3030 -n opentaco +open http://localhost:3030 +``` + +## Service Communication + +Services communicate via Kubernetes DNS: + +```bash +# From within the cluster: +http://opentaco-digger-backend-web:3000 +http://opentaco-drift:3004 +http://opentaco-statesman:8080 +http://opentaco-ui:3030 +``` + +These URLs are configured in `ui.env`: +```bash +ORCHESTRATOR_BACKEND_URL="http://opentaco-digger-backend-web:3000" +DRIFT_REPORTING_BACKEND_URL="http://opentaco-drift:3004" +STATESMAN_BACKEND_URL="http://opentaco-statesman:8080" +``` + +## Upgrading + +```bash +# Update dependencies +cd opentaco +helm dependency update + +# Upgrade deployment +helm upgrade opentaco . -f values-test.yaml -n opentaco + +# Force pod recreation if needed +kubectl delete pods --all -n opentaco ``` -## Upgrade After GitHub App Setup +## Troubleshooting + +### Pods not starting + +```bash +# Check pod status +kubectl get pods -n opentaco + +# Check events +kubectl describe pod POD_NAME -n opentaco + +# Check logs +kubectl logs POD_NAME -n opentaco +``` -After configuring the GitHub App at `/github/setup`, update your values with the app credentials and upgrade: +### Secret issues ```bash -helm upgrade digger-backend oci://ghcr.io/diggerhq/helm-charts/digger-backend \ - --namespace digger \ - --values values.yaml +# List secrets +kubectl get secrets -n opentaco + +# Verify secret contents +kubectl get secret backend-secrets -n opentaco -o jsonpath='{.data}' | jq 'keys' +``` + +### Cloud SQL connection issues + +```bash +# Check Cloud SQL proxy sidecar logs +kubectl logs POD_NAME -n opentaco -c cloud-sql-proxy + +# Verify instance connection name +gcloud sql instances describe INSTANCE_NAME --format="value(connectionName)" ``` + +## Chart Structure + +``` +helm-charts/ +├── opentaco/ # Umbrella chart +│ ├── Chart.yaml +│ ├── values.yaml # Default values +│ ├── values-test.yaml # Test environment +│ └── values-production.yaml +├── digger-backend/ # Terraform orchestration +├── digger-drift/ # Drift detection +├── taco-statesman/ # State management +├── taco-ui/ # Web frontend +└── secrets-example/ # Example secret files +``` + +## Required External Services + +- **GitHub App** - Repository access and webhooks +- **WorkOS** - UI authentication (or configure alternative) +- **Auth0** - Statesman authentication (or configure alternative) +- **S3-compatible storage** - State and artifact storage +- **Cloud SQL or PostgreSQL** - Database + +## Configuration Files + +| File | Purpose | +|------|---------| +| `values.yaml` | Default configuration for all services | +| `values-test.yaml` | Minimal config for testing | +| `values-production.yaml` | Production-ready settings | +| `.secrets/*.env` | Environment-specific secrets (not committed) | + +## Security Notes + +- Never commit `.secrets/` directory to version control +- Use strong random secrets (32-64 characters) +- Rotate secrets regularly +- Review service account permissions +- Enable network policies for production + +## Support + +For issues and documentation: +- GitHub: https://github.com/diggerhq/digger +- Docs: https://docs.digger.dev diff --git a/helm-charts/digger-backend/Chart.yaml b/helm-charts/digger-backend/Chart.yaml index 91113e436..9a2836038 100644 --- a/helm-charts/digger-backend/Chart.yaml +++ b/helm-charts/digger-backend/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: digger-backend -description: A Helm chart for Kubernetes +description: Digger Backend - Terraform orchestration and IaC management service # A chart can be either an 'application' or a 'library' chart. # diff --git a/helm-charts/digger-backend/templates/backend-deployment.yaml b/helm-charts/digger-backend/templates/backend-deployment.yaml index 1b2c76a35..988e81198 100644 --- a/helm-charts/digger-backend/templates/backend-deployment.yaml +++ b/helm-charts/digger-backend/templates/backend-deployment.yaml @@ -2,73 +2,63 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "digger-backend.fullname" . }}-web + labels: + {{- include "digger-backend.labels" . | nindent 4 }} spec: - replicas: 1 + replicas: {{ .Values.digger.replicaCount }} selector: matchLabels: - app: {{ include "digger-backend.name" . }}-web + app: digger-backend-web template: metadata: labels: - app: {{ include "digger-backend.name" . }}-web + app: digger-backend-web + {{- include "digger-backend.selectorLabels" . | nindent 8 }} spec: - {{- if $.Values.digger.nodeSelector }} - nodeSelector: - {{- toYaml $.Values.digger.nodeSelector | nindent 8 }} + {{- if .Values.global }} + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} {{- end }} - {{- if $.Values.digger.tolerations }} - tolerations: - {{- toYaml $.Values.digger.tolerations | nindent 8 }} {{- end }} containers: - name: web image: "{{ .Values.digger.image.repository }}:{{ .Values.digger.image.tag }}" + imagePullPolicy: {{ .Values.digger.image.pullPolicy | default "IfNotPresent" }} ports: - - containerPort: 3000 - livenessProbe: - {{ .Values.digger.livenessProbe | toYaml | indent 10 | trim }} - startupProbe: - {{ .Values.digger.startupProbe | toYaml | indent 10 | trim }} + - name: http + containerPort: 3000 + protocol: TCP + {{- if .Values.digger.secret.useExistingSecret }} envFrom: - secretRef: - {{- if not .Values.digger.secret.useExistingSecret }} - name: {{ include "digger-backend.fullname" . }}-secret - {{- else }} name: {{ .Values.digger.secret.existingSecretName }} + {{- else }} + envFrom: + - secretRef: + name: {{ include "digger-backend.fullname" . }}-secret + {{- end }} + {{- if .Values.digger.livenessProbe }} + livenessProbe: + {{- toYaml .Values.digger.livenessProbe | nindent 10 }} + {{- end }} + {{- if .Values.digger.startupProbe }} + startupProbe: + {{- toYaml .Values.digger.startupProbe | nindent 10 }} {{- end }} {{- with .Values.digger.resources }} resources: {{- toYaml . | nindent 10 }} {{- end }} - env: - - name: POSTGRES_PASSWORD - {{- if and .Values.postgres.enabled .Values.postgres.secret.useExistingSecret }} - valueFrom: - secretKeyRef: - name: {{ .Values.postgres.secret.existingSecretName }} - key: postgres-password - {{- else if .Values.digger.postgres.existingSecretName }} - valueFrom: - secretKeyRef: - name: {{ .Values.digger.postgres.existingSecretName }} - key: {{ .Values.digger.postgres.existingSecretKey }} - {{- else }} - valueFrom: - secretKeyRef: - name: {{ include "digger-backend.fullname" . }}-postgres-secret - key: postgres-password - {{- end }} - - name: DATABASE_URL - {{- if .Values.postgres.enabled }} - value: "postgres://postgres:$(POSTGRES_PASSWORD)@{{ include "digger-backend.fullname" . }}-postgres:5432/postgres?sslmode={{ .Values.postgres.sslmode }}" - {{- else }} - {{- $pg := .Values.digger.postgres }} - value: "postgres://{{ $pg.user }}:$(POSTGRES_PASSWORD)@{{ $pg.host }}:{{ $pg.port }}/{{ $pg.database }}?sslmode={{ $pg.sslmode }}" - {{- end }} - - name: ALLOW_DIRTY - value: "{{ .Values.digger.postgres.allow_dirty }}" - - name: DIGGER_LOG_LEVEL - value: {{ .Values.digger.logLevel | quote }} - {{- with .Values.digger.customEnv }} + {{- with .Values.digger.nodeSelector }} + nodeSelector: {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} + {{- with .Values.digger.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.digger.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm-charts/digger-backend/templates/digger-secret.yaml b/helm-charts/digger-backend/templates/digger-secret.yaml deleted file mode 100644 index 1b7c5549d..000000000 --- a/helm-charts/digger-backend/templates/digger-secret.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if not .Values.digger.secret.useExistingSecret }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "digger-backend.fullname" . }}-secret -type: Opaque -data: - HTTP_BASIC_AUTH_USERNAME: {{ .Values.digger.secret.httpBasicAuthUsername | b64enc | quote }} - HTTP_BASIC_AUTH_PASSWORD: {{ .Values.digger.secret.httpBasicAuthPassword | b64enc | quote }} - BEARER_AUTH_TOKEN: {{ .Values.digger.secret.bearerAuthToken | b64enc | quote }} - HOSTNAME: {{ .Values.digger.secret.hostname | b64enc | quote }} - GITHUB_ORG: {{ .Values.digger.secret.githubOrg | b64enc | quote}} - GITHUB_APP_ID: {{ .Values.digger.secret.githubAppID | b64enc | quote }} - GITHUB_APP_CLIENT_ID: {{ .Values.digger.secret.githubAppClientID | b64enc | quote }} - GITHUB_APP_CLIENT_SECRET: {{ .Values.digger.secret.githubAppClientSecret | b64enc | quote }} - GITHUB_APP_PRIVATE_KEY: {{ .Values.digger.secret.githubAppKeyFile | quote }} - # Note we keeping the one without _BASE64 suffix for backward compatibility - GITHUB_APP_PRIVATE_KEY_BASE64: {{ .Values.digger.secret.githubAppKeyFile | b64enc | quote }} - GITHUB_WEBHOOK_SECRET: {{ .Values.digger.secret.githubWebhookSecret | b64enc | quote }} -{{- end }} diff --git a/helm-charts/digger-backend/templates/postgres-secret.yaml b/helm-charts/digger-backend/templates/postgres-secret.yaml deleted file mode 100644 index eef730519..000000000 --- a/helm-charts/digger-backend/templates/postgres-secret.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if or .Values.postgres.enabled .Values.digger.postgres.password }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "digger-backend.fullname" . }}-postgres-secret -type: Opaque -data: - {{- if and .Values.postgres.secret.password (not .Values.postgres.secret.useExistingSecret) }} - postgres-password: {{ .Values.postgres.secret.password | b64enc | quote }} - {{- else }} - postgres-password: {{ .Values.digger.postgres.password | b64enc | quote }} - {{- end }} -{{- end }} diff --git a/helm-charts/digger-backend/templates/postgres-service.yaml b/helm-charts/digger-backend/templates/postgres-service.yaml deleted file mode 100644 index 95c32b0c2..000000000 --- a/helm-charts/digger-backend/templates/postgres-service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.postgres.enabled }} -apiVersion: v1 -kind: Service -metadata: - name: {{ include "digger-backend.fullname" . }}-postgres -spec: - ports: - - port: 5432 - targetPort: 5432 - selector: - app: {{ include "digger-backend.name" . }}-postgres -{{- end }} diff --git a/helm-charts/digger-backend/templates/postgres-statefulset.yaml b/helm-charts/digger-backend/templates/postgres-statefulset.yaml deleted file mode 100644 index b46cdc460..000000000 --- a/helm-charts/digger-backend/templates/postgres-statefulset.yaml +++ /dev/null @@ -1,35 +0,0 @@ -{{- if .Values.postgres.enabled }} -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: {{ include "digger-backend.fullname" . }}-postgres -spec: - serviceName: "{{ include "digger-backend.fullname" . }}-postgres" - replicas: 1 - selector: - matchLabels: - app: {{ include "digger-backend.name" . }}-postgres - template: - metadata: - labels: - app: {{ include "digger-backend.name" . }}-postgres - spec: - containers: - - name: postgres - image: {{ printf "%s:%s" .Values.postgres.image .Values.postgres.tag }} - ports: - - containerPort: 5432 - {{- if .Values.resources }} - resources: {{- toYaml .Values.postgres.resources | nindent 10 }} - {{- end }} - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - key: postgres-password - {{- if .Values.postgres.secret.useExistingSecret }} - name: {{ .Values.postgres.secret.existingSecretName }} - {{- else }} - name: {{ include "digger-backend.fullname" . }}-postgres-secret - {{- end }} -{{- end }} diff --git a/helm-charts/digger-backend/values.yaml b/helm-charts/digger-backend/values.yaml index f3fc30131..00ecdc2aa 100644 --- a/helm-charts/digger-backend/values.yaml +++ b/helm-charts/digger-backend/values.yaml @@ -1,49 +1,42 @@ -# values.yaml +# Digger Backend - Terraform Orchestration Service +# This chart deploys the Digger backend service digger: - # image values - # repository: digger backend image repository - # tag: digger backend image tag + # Replica count + replicaCount: 1 + + # Image configuration image: - repository: registry.digger.dev/diggerhq/digger_backend - tag: "v0.6.101" + repository: us-central1-docker.pkg.dev/prod-415611/opentaco/digger-backend-ee + tag: "latest" + pullPolicy: IfNotPresent - # Custom environment variables to be added to the backend deployment + # Custom environment variables # Format: # customEnv: # - name: MY_CUSTOM_ENV # value: "my-value" - # - name: ANOTHER_ENV - # value: "another-value" customEnv: [] - # Set the log level for the backend - # DEBUG will enable the debug logs, any other value will set it to INFO + # Log level: DEBUG or INFO logLevel: "INFO" - # Resource limits and requests for the pods + # Resource limits and requests resources: {} # requests: # cpu: 100m - # memory: 140Mi + # memory: 256Mi # limits: # cpu: 500m - # memory: 200Mi - - # livenessProbe and startupProbe settings - # https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + # memory: 512Mi - # livenessProbe: configure probing of /health endpoint - # periodSeconds: how often to perform the probe (20 seconds) + # Health check probes livenessProbe: httpGet: path: /health port: 3000 periodSeconds: 20 - # startupProbe: configure probing of /health endpoint for startup - # failureThreshold: how many times the probe can fail before the container is considered failed (30 times) - # periodSeconds: how often to perform the probe (10 seconds) startupProbe: httpGet: path: /health @@ -51,91 +44,33 @@ digger: failureThreshold: 30 periodSeconds: 10 - # service values - # type: service type (ClusterIP, LoadBalancer, etc) - # port: port number to expose + # Service configuration service: type: ClusterIP - port: 3000 # default port for digger backend + port: 3000 - # ingress values - # enabled: enable ingress resource or not (default: true) - # host: hostname to use (default: "") - # path: path to expose (default: "/") - # tls: tls settings if using https - # secretName: name of k8s secret for tls certs (default: "digger-backend-tls") + # Ingress configuration ingress: - enabled: true + enabled: false className: "" - annotations: - {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: 'true' + annotations: {} host: "" path: / tls: secretName: "digger-backend-tls" - # digger requires a secret with the following key-value pairs: - # HTTP_BASIC_AUTH_USERNAME: - # HTTP_BASIC_AUTH_PASSWORD: - # BEARER_AUTH_TOKEN: - # HOSTNAME: - # GITHUB_ORG: - # GITHUB_APP_ID: - # GITHUB_APP_CLIENT_ID: - # GITHUB_APP_CLIENT_SECRET: - # GITHUB_APP_PRIVATE_KEY: - # GITHUB_WEBHOOK_SECRET: - # POSTGRES_PASSWORD: - # pass the content in clear or specify the name of the existing secret + # Secret configuration + # REQUIRED: Always use an existing secret for production + # Create secret with: kubectl create secret generic backend-secrets --from-env-file=backend.env secret: - useExistingSecret: false - existingSecretName: "" - - httpBasicAuthUsername: "admin" - httpBasicAuthPassword: "admin" - bearerAuthToken: "" # You should generate - hostname: "" - githubOrg: "" - githubAppID: "" - githubAppClientID: "" - githubAppClientSecret: "" - githubAppKeyFile: "" #base64 encoded file - githubWebhookSecret: "" - - # configure this section if you want to use an external postgres database - - postgres: - # specify the secret name and key to pull the existing postgres database password from - existingSecretName: "" - existingSecretKey: "postgres-password" - - # to define connection details in chart: - sslmode: "disable" - user: "postgres" - database: "digger" - host: "pg-postgresql.db" - password: "password" - port: "5432" - allow_dirty: false # set to true if the database has already a schema + useExistingSecret: true + existingSecretName: "backend-secrets" -# configure this section if you want to deploy a postgres db -# WARNING: use only for test purposes, no persistency has been configured -postgres: - sslmode: "disable" - enabled: false - image: postgres - tag: "14" - resources: - limits: {} - requests: {} - # postgres requires a secret with the following key-value pairs: - # postgres-password: + # Node selector + nodeSelector: {} - # pass the content in clear or specify the name of the existing secret - secret: - useExistingSecret: true - existingSecretName: "new-pg-creds" + # Tolerations + tolerations: [] - password: "password" + # Affinity + affinity: {} diff --git a/helm-charts/digger-drift/.helmignore b/helm-charts/digger-drift/.helmignore new file mode 100644 index 000000000..c479619bb --- /dev/null +++ b/helm-charts/digger-drift/.helmignore @@ -0,0 +1,27 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# Test files +tests/ +*.test.yaml + diff --git a/helm-charts/digger-drift/Chart.yaml b/helm-charts/digger-drift/Chart.yaml new file mode 100644 index 000000000..c25a33dd3 --- /dev/null +++ b/helm-charts/digger-drift/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +name: drift +description: Digger Drift - Automated infrastructure drift detection and reporting service +type: application +version: 0.1.0 +appVersion: "v0.1.0" +icon: https://raw.githubusercontent.com/diggerhq/digger/main/docs/logo/digger-logo.png + diff --git a/helm-charts/digger-drift/templates/_helpers.tpl b/helm-charts/digger-drift/templates/_helpers.tpl new file mode 100644 index 000000000..de3d6e83a --- /dev/null +++ b/helm-charts/digger-drift/templates/_helpers.tpl @@ -0,0 +1,59 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "digger-drift.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "digger-drift.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "digger-drift.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "digger-drift.labels" -}} +helm.sh/chart: {{ include "digger-drift.chart" . }} +{{ include "digger-drift.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "digger-drift.selectorLabels" -}} +app.kubernetes.io/name: {{ include "digger-drift.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "digger-drift.serviceAccountName" -}} +{{- default "default" .Values.drift.serviceAccount.name }} +{{- end }} + diff --git a/helm-charts/digger-drift/templates/deployment.yaml b/helm-charts/digger-drift/templates/deployment.yaml new file mode 100644 index 000000000..30833df55 --- /dev/null +++ b/helm-charts/digger-drift/templates/deployment.yaml @@ -0,0 +1,208 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "digger-drift.fullname" . }} + labels: + {{- include "digger-drift.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.drift.replicaCount }} + selector: + matchLabels: + {{- include "digger-drift.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "digger-drift.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.drift.image.repository }}:{{ .Values.drift.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.drift.image.pullPolicy | default "IfNotPresent" }} + ports: + - name: http + containerPort: {{ .Values.drift.service.port }} + protocol: TCP + {{- if .Values.drift.existingSecretName }} + envFrom: + - secretRef: + name: {{ .Values.drift.existingSecretName }} + {{- end }} + env: + - name: DIGGER_PORT + value: "{{ .Values.drift.service.port }}" + - name: DIGGER_LOG_LEVEL + value: "{{ .Values.drift.logLevel }}" + {{- if .Values.drift.sentry.dsn }} + - name: SENTRY_DSN + value: "{{ .Values.drift.sentry.dsn }}" + {{- end }} + {{- if not .Values.drift.existingSecretName }} + # PostgreSQL configuration + - name: POSTGRES_HOST + value: "{{ .Values.drift.postgres.host }}" + - name: POSTGRES_PORT + value: "{{ .Values.drift.postgres.port }}" + - name: POSTGRES_USER + value: "{{ .Values.drift.postgres.user }}" + - name: POSTGRES_DB + value: "{{ .Values.drift.postgres.database }}" + - name: POSTGRES_SSLMODE + value: "{{ .Values.drift.postgres.sslmode }}" + {{- if .Values.drift.postgres.existingSecretName }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.drift.postgres.existingSecretName }} + key: {{ .Values.drift.postgres.existingSecretKey }} + {{- else }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: POSTGRES_PASSWORD + {{- end }} + # Configuration from secret + {{- if .Values.drift.github.existingSecretName }} + - name: DIGGER_HOSTNAME + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: DIGGER_HOSTNAME + - name: DIGGER_WEBHOOK_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: DIGGER_WEBHOOK_SECRET + - name: DIGGER_APP_URL + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: DIGGER_APP_URL + optional: true + - name: DIGGER_DRIFT_REPORTER_HOSTNAME + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: DIGGER_DRIFT_REPORTER_HOSTNAME + optional: true + - name: GITHUB_ORG + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: GITHUB_ORG + - name: GITHUB_APP_ID + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: GITHUB_APP_ID + - name: GITHUB_APP_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: GITHUB_APP_CLIENT_ID + - name: GITHUB_APP_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: GITHUB_APP_CLIENT_SECRET + - name: GITHUB_APP_PRIVATE_KEY_BASE64 + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: GITHUB_APP_PRIVATE_KEY_BASE64 + - name: GITHUB_WEBHOOK_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.drift.github.existingSecretName }} + key: GITHUB_WEBHOOK_SECRET + {{- else }} + - name: DIGGER_HOSTNAME + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: DIGGER_HOSTNAME + - name: DIGGER_WEBHOOK_SECRET + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: DIGGER_WEBHOOK_SECRET + {{- if .Values.drift.config.appUrl }} + - name: DIGGER_APP_URL + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: DIGGER_APP_URL + {{- end }} + {{- if .Values.drift.config.driftReporterHostname }} + - name: DIGGER_DRIFT_REPORTER_HOSTNAME + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: DIGGER_DRIFT_REPORTER_HOSTNAME + {{- end }} + - name: GITHUB_ORG + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: GITHUB_ORG + - name: GITHUB_APP_ID + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: GITHUB_APP_ID + - name: GITHUB_APP_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: GITHUB_APP_CLIENT_ID + - name: GITHUB_APP_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: GITHUB_APP_CLIENT_SECRET + - name: GITHUB_APP_PRIVATE_KEY_BASE64 + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: GITHUB_APP_PRIVATE_KEY_BASE64 + - name: GITHUB_WEBHOOK_SECRET + valueFrom: + secretKeyRef: + name: {{ include "digger-drift.fullname" . }} + key: GITHUB_WEBHOOK_SECRET + {{- end }} + {{- end }} + # Custom environment variables + {{- range .Values.drift.customEnv }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- with .Values.drift.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.drift.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.drift.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.drift.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.drift.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.drift.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + diff --git a/helm-charts/digger-drift/templates/ingress.yaml b/helm-charts/digger-drift/templates/ingress.yaml new file mode 100644 index 000000000..3075ab15a --- /dev/null +++ b/helm-charts/digger-drift/templates/ingress.yaml @@ -0,0 +1,34 @@ +{{- if .Values.drift.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "digger-drift.fullname" . }} + labels: + {{- include "digger-drift.labels" . | nindent 4 }} + {{- with .Values.drift.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.drift.ingress.className }} + ingressClassName: {{ .Values.drift.ingress.className }} + {{- end }} + {{- if .Values.drift.ingress.tls.secretName }} + tls: + - hosts: + - {{ .Values.drift.ingress.host }} + secretName: {{ .Values.drift.ingress.tls.secretName }} + {{- end }} + rules: + - host: {{ .Values.drift.ingress.host }} + http: + paths: + - path: {{ .Values.drift.ingress.path }} + pathType: Prefix + backend: + service: + name: {{ include "digger-drift.fullname" . }} + port: + name: http +{{- end }} + diff --git a/helm-charts/digger-drift/templates/service.yaml b/helm-charts/digger-drift/templates/service.yaml new file mode 100644 index 000000000..e1b5ad508 --- /dev/null +++ b/helm-charts/digger-drift/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "digger-drift.fullname" . }} + labels: + {{- include "digger-drift.labels" . | nindent 4 }} +spec: + type: {{ .Values.drift.service.type }} + ports: + - port: {{ .Values.drift.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "digger-drift.selectorLabels" . | nindent 4 }} + diff --git a/helm-charts/digger-drift/values.yaml b/helm-charts/digger-drift/values.yaml new file mode 100644 index 000000000..860ef9827 --- /dev/null +++ b/helm-charts/digger-drift/values.yaml @@ -0,0 +1,121 @@ +# values.yaml + +drift: + # Image configuration + # NOTE: This image needs to be built first! See helm-charts/BUILD_IMAGES.md + # Build with: docker build -t YOUR_REGISTRY/drift:v0.1.0 -f Dockerfile_drift . + image: + repository: us-central1-docker.pkg.dev/prod-415611/opentaco/drift + tag: "latest" + pullPolicy: "IfNotPresent" + + # Number of replicas + replicaCount: 1 + + # Custom environment variables + customEnv: [] + # - name: MY_CUSTOM_ENV + # value: "my-value" + + # Set the log level for the drift service + # DEBUG will enable the debug logs, any other value will set it to INFO + logLevel: "INFO" + + # Service configuration + service: + type: ClusterIP + port: 3000 + + # Ingress configuration + ingress: + enabled: false + className: "nginx" + annotations: {} + # cert-manager.io/cluster-issuer: letsencrypt-prod + host: "drift.example.com" + path: / + tls: + secretName: "digger-drift-tls" + + # Liveness and startup probe settings + livenessProbe: + httpGet: + path: /health + port: 3004 + periodSeconds: 20 + + startupProbe: + httpGet: + path: /health + port: 3004 + failureThreshold: 30 + periodSeconds: 10 + + readinessProbe: + httpGet: + path: /health + port: 3004 + periodSeconds: 10 + + # Resource limits + resources: {} + # requests: + # cpu: 100m + # memory: 256Mi + # limits: + # cpu: 500m + # memory: 512Mi + + # Node selector + nodeSelector: {} + + # Tolerations + tolerations: [] + + # Affinity + affinity: {} + + # Drift service configuration + # hostname: The public hostname for this drift service + # webhookSecret: Secret for internal webhook authentication + # appUrl: URL for the Digger app UI (for notifications) + # driftReporterHostname: Hostname for the drift reporter (optional) + config: + hostname: "" + webhookSecret: "" + appUrl: "" + driftReporterHostname: "" + + # Sentry configuration (optional) + sentry: + dsn: "" + + # GitHub App configuration + # These are required for drift detection to work with GitHub + github: + # Use existingSecret to reference an existing secret + # or fill in the values below + existingSecretName: "" + + org: "" + appID: "" + appClientID: "" + appClientSecret: "" + appPrivateKey: "" # base64 encoded private key + webhookSecret: "" + + # PostgreSQL configuration + # The drift service uses the same database as the digger backend + postgres: + # Use existingSecret to pull password from an existing secret + existingSecretName: "" + existingSecretKey: "postgres-password" + + # Database connection details + sslmode: "disable" + user: "postgres" + database: "digger" + host: "postgresql.default.svc.cluster.local" + password: "" + port: "5432" + diff --git a/helm-charts/opentaco/.helmignore b/helm-charts/opentaco/.helmignore new file mode 100644 index 000000000..898df4886 --- /dev/null +++ b/helm-charts/opentaco/.helmignore @@ -0,0 +1,24 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ + diff --git a/helm-charts/opentaco/Chart.lock b/helm-charts/opentaco/Chart.lock new file mode 100644 index 000000000..45cf8c680 --- /dev/null +++ b/helm-charts/opentaco/Chart.lock @@ -0,0 +1,18 @@ +dependencies: +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 15.5.38 +- name: digger-backend + repository: file://../digger-backend + version: 0.1.12 +- name: statesman + repository: file://../taco-statesman + version: 0.1.0 +- name: drift + repository: file://../digger-drift + version: 0.1.0 +- name: ui + repository: file://../taco-ui + version: 0.1.0 +digest: sha256:cc82ef456f670919d3100e6336abc4a200917cf5ea37bfdbd3285a6d001bf130 +generated: "2025-10-17T13:52:03.661873-07:00" diff --git a/helm-charts/opentaco/Chart.yaml b/helm-charts/opentaco/Chart.yaml new file mode 100644 index 000000000..34ec5c611 --- /dev/null +++ b/helm-charts/opentaco/Chart.yaml @@ -0,0 +1,67 @@ +apiVersion: v2 +name: opentaco +description: OpenTaco - Complete Infrastructure-as-Code platform deployment +type: application +version: 0.1.0 +appVersion: "0.1.0" + +# Umbrella chart that deploys all OpenTaco components +# This chart orchestrates: +# - PostgreSQL database (optional - can use Cloud SQL instead) +# - Digger Backend (terraform orchestration backend) +# - Taco Statesman (IaC state management) +# - Drift Detection service +# - Taco UI (React frontend) + +dependencies: + # Optional PostgreSQL - disable if using Cloud SQL + - name: postgresql + version: "15.x.x" + repository: https://charts.bitnami.com/bitnami + condition: postgresql.enabled + tags: + - database + + # Digger Backend - terraform orchestration + - name: digger-backend + version: "0.1.12" + repository: "file://../digger-backend" + condition: digger-backend.enabled + tags: + - backend + + # Taco Statesman - IaC state management + - name: statesman + version: "0.1.0" + repository: "file://../taco-statesman" + condition: taco-statesman.enabled + tags: + - backend + + # Drift Detection + - name: drift + version: "0.1.0" + repository: "file://../digger-drift" + condition: drift.enabled + tags: + - backend + + # Taco UI - React frontend + - name: ui + version: "0.1.0" + repository: "file://../taco-ui" + condition: taco-ui.enabled + tags: + - frontend + +maintainers: + - name: OpenTaco Team + email: team@opentaco.dev + +keywords: + - infrastructure + - terraform + - iac + - opentaco + - digger + diff --git a/helm-charts/opentaco/charts/digger-backend-0.1.12.tgz b/helm-charts/opentaco/charts/digger-backend-0.1.12.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3ad304cf9e469e9533dfaf44f1be5133f4ad42c4 GIT binary patch literal 2589 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 literal 0 HcmV?d00001 diff --git a/helm-charts/opentaco/charts/postgresql-15.5.38.tgz b/helm-charts/opentaco/charts/postgresql-15.5.38.tgz new file mode 100644 index 0000000000000000000000000000000000000000..55ad8887f9909254e858b2dc7819c678b37ee9f4 GIT binary patch literal 75781 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 z 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

%xVd_aT0Bpe15RL%Gf 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 X57)!>hpyiO00960Z3E0@0H_E6!0;oN literal 0 HcmV?d00001 diff --git a/helm-charts/opentaco/charts/statesman-0.1.0.tgz b/helm-charts/opentaco/charts/statesman-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..74d32582bdb8d8ff959b20d83eb5b60346f921f7 GIT binary patch literal 2696 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 J3-: +ORCHESTRATOR_BACKEND_URL="http://opentaco-digger-backend-web:3000" +ORCHESTRATOR_BACKEND_SECRET=YOUR_ORCHESTRATOR_SECRET + +DRIFT_REPORTING_BACKEND_URL=http://opentaco-drift:3004 +DRIFT_REPORTING_BACKEND_WEBHOOK_SECRET=YOUR_DRIFT_WEBHOOK_SECRET + +STATESMAN_BACKEND_URL=http://opentaco-statesman:8080 +STATESMAN_BACKEND_WEBHOOK_SECRET=YOUR_STATESMAN_WEBHOOK_SECRET + diff --git a/helm-charts/taco-statesman/Chart.yaml b/helm-charts/taco-statesman/Chart.yaml index 42893c1db..fe1b1f26d 100644 --- a/helm-charts/taco-statesman/Chart.yaml +++ b/helm-charts/taco-statesman/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: taco-statesman -description: A minimalist Helm chart for Taco Statesman service +name: statesman +description: Taco Statesman - Infrastructure-as-Code state management and coordination service type: application version: 0.1.0 appVersion: "v0.1.0" diff --git a/helm-charts/taco-statesman/README.md b/helm-charts/taco-statesman/README.md deleted file mode 100644 index 4292dd0b8..000000000 --- a/helm-charts/taco-statesman/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Taco Statesman Helm Chart - -A minimalist Helm chart for deploying the Taco Statesman service. - -## Quick Start - -```bash -# Install with default settings (memory storage) -helm install taco-statesman ./helm-charts/taco-statesman - -# Install with S3 storage -helm install taco-statesman ./helm-charts/taco-statesman \ - --set taco.storage.type=s3 \ - --set taco.storage.s3.bucket=my-bucket \ - --set taco.storage.s3.region=us-east-1 - -# Disable authentication (development) -helm install taco-statesman ./helm-charts/taco-statesman \ - --set taco.auth.disable=true -``` - -## Configuration - -| Parameter | Description | Default | -|-----------|-------------|---------| -| `taco.image.repository` | Image repository | `ghcr.io/diggerhq/digger/taco-statesman` | -| `taco.image.tag` | Image tag | `v0.1.0` | -| `taco.replicaCount` | Number of replicas | `1` | -| `taco.service.port` | Service port | `8080` | -| `taco.storage.type` | Storage type (`memory` or `s3`) | `memory` | -| `taco.auth.disable` | Disable authentication | `false` | - -## Storage - -- **Memory**: Default, no configuration needed -- **S3**: Set `taco.storage.type=s3` and provide S3 credentials diff --git a/helm-charts/taco-statesman/templates/deployment.yaml b/helm-charts/taco-statesman/templates/deployment.yaml index 1bf125fde..9d64742e6 100644 --- a/helm-charts/taco-statesman/templates/deployment.yaml +++ b/helm-charts/taco-statesman/templates/deployment.yaml @@ -14,7 +14,15 @@ spec: labels: {{- include "taco-statesman.selectorLabels" . | nindent 8 }} spec: + {{- if .Values.taco.cloudSql.enabled }} + serviceAccountName: {{ .Values.taco.cloudSql.serviceAccount | default "default" }} + {{- end }} + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} containers: + - name: {{ .Chart.Name }} image: "{{ .Values.taco.image.repository }}:{{ .Values.taco.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.taco.image.pullPolicy | default "IfNotPresent" }} imagePullPolicy: {{ .Values.taco.image.pullPolicy | default "IfNotPresent" }} @@ -22,9 +30,15 @@ spec: - name: http containerPort: {{ .Values.taco.service.port }} protocol: TCP + {{- if .Values.taco.existingSecretName }} + envFrom: + - secretRef: + name: {{ .Values.taco.existingSecretName }} + {{- end }} env: - name: OPENTACO_PORT value: "{{ .Values.taco.service.port }}" + {{- if not .Values.taco.existingSecretName }} - name: OPENTACO_STORAGE value: "{{ .Values.taco.storage.type }}" {{- if .Values.taco.auth.disable }} @@ -51,6 +65,7 @@ spec: name: {{ .Values.taco.storage.s3.secretName }} key: secret-access-key {{- end }} + {{- end }} livenessProbe: httpGet: path: /healthz @@ -63,3 +78,26 @@ spec: port: http initialDelaySeconds: 5 periodSeconds: 5 + {{- if .Values.taco.cloudSql.enabled }} + - name: cloud-sql-proxy + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.0 + args: + - "--structured-logs" + - "--port=5432" + - "{{ .Values.taco.cloudSql.instanceConnectionName }}" + securityContext: + runAsNonRoot: true + {{- if .Values.taco.cloudSql.credentialsSecret }} + env: + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /secrets/cloudsql/credentials.json + volumeMounts: + - name: cloudsql-credentials + mountPath: /secrets/cloudsql + readOnly: true + {{- end }} + volumes: + - name: cloudsql-credentials + secret: + secretName: {{ .Values.taco.cloudSql.credentialsSecret }} + {{- end }} diff --git a/helm-charts/taco-statesman/values.yaml b/helm-charts/taco-statesman/values.yaml index fc355e2d6..924b40546 100644 --- a/helm-charts/taco-statesman/values.yaml +++ b/helm-charts/taco-statesman/values.yaml @@ -2,9 +2,11 @@ taco: # Image configuration + # NOTE: This image needs to be built first! See helm-charts/BUILD_IMAGES.md + # Build with: cd taco && docker build -t YOUR_REGISTRY/taco-statesman:v0.1.0 -f Dockerfile_statesman . image: - repository: ghcr.io/diggerhq/digger/taco-statesman - tag: "v0.1.0" + repository: us-central1-docker.pkg.dev/prod-415611/opentaco/taco-statesman + tag: "latest" pullPolicy: "IfNotPresent" # Number of replicas diff --git a/helm-charts/taco-ui/.helmignore b/helm-charts/taco-ui/.helmignore new file mode 100644 index 000000000..c479619bb --- /dev/null +++ b/helm-charts/taco-ui/.helmignore @@ -0,0 +1,27 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# Test files +tests/ +*.test.yaml + diff --git a/helm-charts/taco-ui/Chart.yaml b/helm-charts/taco-ui/Chart.yaml new file mode 100644 index 000000000..919fb933b --- /dev/null +++ b/helm-charts/taco-ui/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +name: ui +description: Taco UI - Web-based frontend for OpenTaco infrastructure management platform +type: application +version: 0.1.0 +appVersion: "v0.1.0" +icon: https://raw.githubusercontent.com/diggerhq/digger/main/docs/logo/digger-logo.png + diff --git a/helm-charts/taco-ui/templates/_helpers.tpl b/helm-charts/taco-ui/templates/_helpers.tpl new file mode 100644 index 000000000..717c4b3e2 --- /dev/null +++ b/helm-charts/taco-ui/templates/_helpers.tpl @@ -0,0 +1,59 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "taco-ui.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "taco-ui.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "taco-ui.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "taco-ui.labels" -}} +helm.sh/chart: {{ include "taco-ui.chart" . }} +{{ include "taco-ui.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "taco-ui.selectorLabels" -}} +app.kubernetes.io/name: {{ include "taco-ui.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "taco-ui.serviceAccountName" -}} +{{- default "default" .Values.ui.serviceAccount.name }} +{{- end }} + diff --git a/helm-charts/taco-ui/templates/deployment.yaml b/helm-charts/taco-ui/templates/deployment.yaml new file mode 100644 index 000000000..74b041378 --- /dev/null +++ b/helm-charts/taco-ui/templates/deployment.yaml @@ -0,0 +1,112 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "taco-ui.fullname" . }} + labels: + {{- include "taco-ui.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.ui.replicaCount }} + selector: + matchLabels: + {{- include "taco-ui.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "taco-ui.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.ui.image.pullPolicy | default "IfNotPresent" }} + ports: + - name: http + containerPort: {{ .Values.ui.service.port }} + protocol: TCP + {{- if .Values.ui.existingSecretName }} + envFrom: + - secretRef: + name: {{ .Values.ui.existingSecretName }} + {{- end }} + env: + - name: PORT + value: "{{ .Values.ui.service.port }}" + {{- if .Values.ui.env.apiUrl }} + - name: VITE_API_URL + value: "{{ .Values.ui.env.apiUrl }}" + {{- end }} + {{- if .Values.ui.env.allowedHosts }} + - name: ALLOWED_HOSTS + value: "{{ .Values.ui.env.allowedHosts }}" + {{- end }} + {{- if .Values.ui.env.workos.clientId }} + - name: WORKOS_CLIENT_ID + value: "{{ .Values.ui.env.workos.clientId }}" + {{- end }} + {{- if .Values.ui.env.workos.secretName }} + - name: WORKOS_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.ui.env.workos.secretName }} + key: api-key + - name: WORKOS_COOKIE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.ui.env.workos.secretName }} + key: cookie-password + {{- end }} + {{- if .Values.ui.env.stripe.publishableKey }} + - name: VITE_STRIPE_PUBLISHABLE_KEY + value: "{{ .Values.ui.env.stripe.publishableKey }}" + {{- end }} + {{- if .Values.ui.env.stripe.secretName }} + - name: STRIPE_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.ui.env.stripe.secretName }} + key: secret-key + {{- end }} + {{- if .Values.ui.env.posthog.key }} + - name: VITE_POSTHOG_KEY + value: "{{ .Values.ui.env.posthog.key }}" + {{- end }} + {{- if .Values.ui.env.posthog.host }} + - name: VITE_POSTHOG_HOST + value: "{{ .Values.ui.env.posthog.host }}" + {{- end }} + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + {{- with .Values.ui.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.ui.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + diff --git a/helm-charts/taco-ui/templates/ingress.yaml b/helm-charts/taco-ui/templates/ingress.yaml new file mode 100644 index 000000000..92be1dcc2 --- /dev/null +++ b/helm-charts/taco-ui/templates/ingress.yaml @@ -0,0 +1,42 @@ +{{- if .Values.ui.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "taco-ui.fullname" . }} + labels: + {{- include "taco-ui.labels" . | nindent 4 }} + {{- with .Values.ui.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ui.ingress.className }} + ingressClassName: {{ .Values.ui.ingress.className }} + {{- end }} + {{- if .Values.ui.ingress.tls }} + tls: + {{- range .Values.ui.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ui.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "taco-ui.fullname" $ }} + port: + name: http + {{- end }} + {{- end }} +{{- end }} + diff --git a/helm-charts/taco-ui/templates/service.yaml b/helm-charts/taco-ui/templates/service.yaml new file mode 100644 index 000000000..77412540d --- /dev/null +++ b/helm-charts/taco-ui/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "taco-ui.fullname" . }} + labels: + {{- include "taco-ui.labels" . | nindent 4 }} +spec: + type: {{ .Values.ui.service.type }} + ports: + - port: {{ .Values.ui.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "taco-ui.selectorLabels" . | nindent 4 }} + diff --git a/helm-charts/taco-ui/tests/deployment_test.yaml b/helm-charts/taco-ui/tests/deployment_test.yaml new file mode 100644 index 000000000..971ed8f8a --- /dev/null +++ b/helm-charts/taco-ui/tests/deployment_test.yaml @@ -0,0 +1,44 @@ +suite: test deployment +templates: + - deployment.yaml +tests: + - it: should create a deployment + asserts: + - isKind: + of: Deployment + - equal: + path: metadata.name + value: RELEASE-NAME-taco-ui + - equal: + path: spec.replicas + value: 1 + + - it: should set correct container image + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: "ghcr.io/diggerhq/digger/taco-ui:v0.1.0" + + - it: should set environment variables when apiUrl is provided + set: + ui.env.apiUrl: "http://test-backend:8080" + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: VITE_API_URL + value: "http://test-backend:8080" + + - it: should configure workos when secretName is provided + set: + ui.env.workos.secretName: "workos-test" + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: WORKOS_API_KEY + valueFrom: + secretKeyRef: + name: workos-test + key: api-key + diff --git a/helm-charts/taco-ui/values.yaml b/helm-charts/taco-ui/values.yaml new file mode 100644 index 000000000..1da24629e --- /dev/null +++ b/helm-charts/taco-ui/values.yaml @@ -0,0 +1,81 @@ +# values.yaml + +ui: + # Image configuration + # This is a standalone Node.js + TanStack Start SSR app (no Netlify!) + # The image includes: + # - Production-optimized Node.js server with static asset serving + # - Built for linux/amd64 platform (GCP/GKE compatible) + # + # To build and push: + # ./build-all-images.sh YOUR_REGISTRY VERSION + # docker push YOUR_REGISTRY/taco-ui:VERSION + image: + repository: us-central1-docker.pkg.dev/prod-415611/opentaco/taco-ui + tag: "latest" + pullPolicy: "IfNotPresent" + + # Number of replicas + replicaCount: 1 + + # Service configuration + service: + type: ClusterIP + port: 3030 # taco-ui Node.js server listens on port 3030 + + # Environment variables + env: + # Backend API URL + apiUrl: "http://taco-statesman:8080" + # Allowed hosts (comma-separated) + allowedHosts: "" + # WorkOS configuration (optional) + workos: + clientId: "" + # Use secretName for sensitive data + secretName: "" + # Stripe configuration (optional) + stripe: + publishableKey: "" + # Use secretName for sensitive data + secretName: "" + # PostHog configuration (optional) + posthog: + key: "" + host: "" + + # Ingress configuration + ingress: + enabled: false + className: "nginx" + annotations: {} + # cert-manager.io/cluster-issuer: letsencrypt-prod + # kubernetes.io/tls-acme: "true" + hosts: + - host: taco.example.com + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: taco-ui-tls + # hosts: + # - taco.example.com + + # Resource limits + resources: {} + # limits: + # cpu: 500m + # memory: 512Mi + # requests: + # cpu: 250m + # memory: 256Mi + + # Node selector + nodeSelector: {} + + # Tolerations + tolerations: [] + + # Affinity + affinity: {} + diff --git a/ui/package.json b/ui/package.json index a8eadfa0d..b7f637ebd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,7 +7,10 @@ "scripts": { "dev": "vite dev", "build": "vite build && tsc --noEmit", - "preview": "vite preview" + "start": "NODE_ENV=production node dist/server/server.js", + "preview": "vite preview", + "type-check": "tsc --noEmit", + "type-check:watch": "tsc --noEmit --watch" }, "keywords": [], "author": "", diff --git a/ui/server-start.js b/ui/server-start.js new file mode 100644 index 000000000..8e6682707 --- /dev/null +++ b/ui/server-start.js @@ -0,0 +1,102 @@ +// Simple Node.js HTTP server that runs the TanStack Start fetch handler +import { createServer } from 'node:http'; +import { readFile } from 'node:fs/promises'; +import { join, extname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import serverHandler from './dist/server/server.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const PORT = process.env.PORT || 3030; +const HOST = process.env.HOST || '0.0.0.0'; + +// MIME type mapping +const MIME_TYPES = { + '.html': 'text/html', + '.js': 'application/javascript', + '.mjs': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +}; + +const server = createServer(async (req, res) => { + try { + const url = new URL(req.url, `http://${req.headers.host}`); + const pathname = url.pathname; + + // Try to serve static files from dist/client first + if (pathname.startsWith('/assets/') || pathname === '/favicon.svg' || pathname === '/favicon.png' || pathname === '/favicon.ico') { + try { + const filePath = join(__dirname, 'dist', 'client', pathname); + const content = await readFile(filePath); + const ext = extname(pathname); + const mimeType = MIME_TYPES[ext] || 'application/octet-stream'; + + res.writeHead(200, { + 'Content-Type': mimeType, + 'Cache-Control': 'public, max-age=31536000, immutable', + }); + res.end(content); + return; + } catch (err) { + // File not found, fall through to SSR handler + } + } + + // Get request body if present + let body = undefined; + if (req.method !== 'GET' && req.method !== 'HEAD') { + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + } + body = Buffer.concat(chunks); + } + + // Create Web Standard Request + const request = new Request(`http://${req.headers.host}${req.url}`, { + method: req.method, + headers: req.headers, + body: body, + }); + + // Call the TanStack Start fetch handler + const response = await serverHandler.fetch(request); + + // Convert Web Standard Response to Node.js response + res.statusCode = response.status; + res.statusMessage = response.statusText; + + // Set headers + response.headers.forEach((value, key) => { + res.setHeader(key, value); + }); + + // Stream the response body + if (response.body) { + const reader = response.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + res.write(value); + } + } + + res.end(); + } catch (error) { + console.error('Server error:', error); + res.statusCode = 500; + res.end('Internal Server Error'); + } +}); + +server.listen(PORT, HOST, () => { + console.log(`🚀 Server running at http://${HOST}:${PORT}/`); +}); + diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 71212f1bc..741fcfa70 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -24,7 +24,7 @@ import { Route as AuthenticatedDashboardDashboardReposIndexRouteImport } from '. import { Route as AuthenticatedDashboardDashboardProjectsIndexRouteImport } from './routes/_authenticated/_dashboard/dashboard/projects.index' import { Route as AuthenticatedDashboardDashboardReposConnectRouteImport } from './routes/_authenticated/_dashboard/dashboard/repos.connect' import { Route as AuthenticatedDashboardDashboardReposRepoIdRouteImport } from './routes/_authenticated/_dashboard/dashboard/repos.$repoId' -import { Route as AuthenticatedDashboardDashboardProjectsProjectIdRouteImport } from './routes/_authenticated/_dashboard/dashboard/projects.$projectId' +import { Route as AuthenticatedDashboardDashboardProjectsProjectidRouteImport } from './routes/_authenticated/_dashboard/dashboard/projects.$projectid' import { Route as AuthenticatedDashboardDashboardConnectionsConnectionIdRouteImport } from './routes/_authenticated/_dashboard/dashboard/connections.$connectionId' const LogoutRoute = LogoutRouteImport.update({ @@ -110,10 +110,10 @@ const AuthenticatedDashboardDashboardReposRepoIdRoute = path: '/$repoId', getParentRoute: () => AuthenticatedDashboardDashboardReposRoute, } as any) -const AuthenticatedDashboardDashboardProjectsProjectIdRoute = - AuthenticatedDashboardDashboardProjectsProjectIdRouteImport.update({ - id: '/$projectId', - path: '/$projectId', +const AuthenticatedDashboardDashboardProjectsProjectidRoute = + AuthenticatedDashboardDashboardProjectsProjectidRouteImport.update({ + id: '/$projectid', + path: '/$projectid', getParentRoute: () => AuthenticatedDashboardDashboardProjectsRoute, } as any) const AuthenticatedDashboardDashboardConnectionsConnectionIdRoute = @@ -134,7 +134,7 @@ export interface FileRoutesByFullPath { '/dashboard/repos': typeof AuthenticatedDashboardDashboardReposRouteWithChildren '/dashboard/settings': typeof AuthenticatedDashboardDashboardSettingsRoute '/dashboard/connections/$connectionId': typeof AuthenticatedDashboardDashboardConnectionsConnectionIdRoute - '/dashboard/projects/$projectId': typeof AuthenticatedDashboardDashboardProjectsProjectIdRoute + '/dashboard/projects/$projectid': typeof AuthenticatedDashboardDashboardProjectsProjectidRoute '/dashboard/repos/$repoId': typeof AuthenticatedDashboardDashboardReposRepoIdRoute '/dashboard/repos/connect': typeof AuthenticatedDashboardDashboardReposConnectRoute '/dashboard/projects/': typeof AuthenticatedDashboardDashboardProjectsIndexRoute @@ -149,7 +149,7 @@ export interface FileRoutesByTo { '/dashboard/onboarding': typeof AuthenticatedDashboardDashboardOnboardingRoute '/dashboard/settings': typeof AuthenticatedDashboardDashboardSettingsRoute '/dashboard/connections/$connectionId': typeof AuthenticatedDashboardDashboardConnectionsConnectionIdRoute - '/dashboard/projects/$projectId': typeof AuthenticatedDashboardDashboardProjectsProjectIdRoute + '/dashboard/projects/$projectid': typeof AuthenticatedDashboardDashboardProjectsProjectidRoute '/dashboard/repos/$repoId': typeof AuthenticatedDashboardDashboardReposRepoIdRoute '/dashboard/repos/connect': typeof AuthenticatedDashboardDashboardReposConnectRoute '/dashboard/projects': typeof AuthenticatedDashboardDashboardProjectsIndexRoute @@ -169,7 +169,7 @@ export interface FileRoutesById { '/_authenticated/_dashboard/dashboard/repos': typeof AuthenticatedDashboardDashboardReposRouteWithChildren '/_authenticated/_dashboard/dashboard/settings': typeof AuthenticatedDashboardDashboardSettingsRoute '/_authenticated/_dashboard/dashboard/connections/$connectionId': typeof AuthenticatedDashboardDashboardConnectionsConnectionIdRoute - '/_authenticated/_dashboard/dashboard/projects/$projectId': typeof AuthenticatedDashboardDashboardProjectsProjectIdRoute + '/_authenticated/_dashboard/dashboard/projects/$projectid': typeof AuthenticatedDashboardDashboardProjectsProjectidRoute '/_authenticated/_dashboard/dashboard/repos/$repoId': typeof AuthenticatedDashboardDashboardReposRepoIdRoute '/_authenticated/_dashboard/dashboard/repos/connect': typeof AuthenticatedDashboardDashboardReposConnectRoute '/_authenticated/_dashboard/dashboard/projects/': typeof AuthenticatedDashboardDashboardProjectsIndexRoute @@ -188,7 +188,7 @@ export interface FileRouteTypes { | '/dashboard/repos' | '/dashboard/settings' | '/dashboard/connections/$connectionId' - | '/dashboard/projects/$projectId' + | '/dashboard/projects/$projectid' | '/dashboard/repos/$repoId' | '/dashboard/repos/connect' | '/dashboard/projects/' @@ -203,7 +203,7 @@ export interface FileRouteTypes { | '/dashboard/onboarding' | '/dashboard/settings' | '/dashboard/connections/$connectionId' - | '/dashboard/projects/$projectId' + | '/dashboard/projects/$projectid' | '/dashboard/repos/$repoId' | '/dashboard/repos/connect' | '/dashboard/projects' @@ -222,7 +222,7 @@ export interface FileRouteTypes { | '/_authenticated/_dashboard/dashboard/repos' | '/_authenticated/_dashboard/dashboard/settings' | '/_authenticated/_dashboard/dashboard/connections/$connectionId' - | '/_authenticated/_dashboard/dashboard/projects/$projectId' + | '/_authenticated/_dashboard/dashboard/projects/$projectid' | '/_authenticated/_dashboard/dashboard/repos/$repoId' | '/_authenticated/_dashboard/dashboard/repos/connect' | '/_authenticated/_dashboard/dashboard/projects/' @@ -343,11 +343,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedDashboardDashboardReposRepoIdRouteImport parentRoute: typeof AuthenticatedDashboardDashboardReposRoute } - '/_authenticated/_dashboard/dashboard/projects/$projectId': { - id: '/_authenticated/_dashboard/dashboard/projects/$projectId' - path: '/$projectId' - fullPath: '/dashboard/projects/$projectId' - preLoaderRoute: typeof AuthenticatedDashboardDashboardProjectsProjectIdRouteImport + '/_authenticated/_dashboard/dashboard/projects/$projectid': { + id: '/_authenticated/_dashboard/dashboard/projects/$projectid' + path: '/$projectid' + fullPath: '/dashboard/projects/$projectid' + preLoaderRoute: typeof AuthenticatedDashboardDashboardProjectsProjectidRouteImport parentRoute: typeof AuthenticatedDashboardDashboardProjectsRoute } '/_authenticated/_dashboard/dashboard/connections/$connectionId': { @@ -376,14 +376,14 @@ const AuthenticatedDashboardDashboardConnectionsRouteWithChildren = ) interface AuthenticatedDashboardDashboardProjectsRouteChildren { - AuthenticatedDashboardDashboardProjectsProjectIdRoute: typeof AuthenticatedDashboardDashboardProjectsProjectIdRoute + AuthenticatedDashboardDashboardProjectsProjectidRoute: typeof AuthenticatedDashboardDashboardProjectsProjectidRoute AuthenticatedDashboardDashboardProjectsIndexRoute: typeof AuthenticatedDashboardDashboardProjectsIndexRoute } const AuthenticatedDashboardDashboardProjectsRouteChildren: AuthenticatedDashboardDashboardProjectsRouteChildren = { - AuthenticatedDashboardDashboardProjectsProjectIdRoute: - AuthenticatedDashboardDashboardProjectsProjectIdRoute, + AuthenticatedDashboardDashboardProjectsProjectidRoute: + AuthenticatedDashboardDashboardProjectsProjectidRoute, AuthenticatedDashboardDashboardProjectsIndexRoute: AuthenticatedDashboardDashboardProjectsIndexRoute, } diff --git a/ui/src/routes/_authenticated/_dashboard/dashboard/projects.$projectid.tsx b/ui/src/routes/_authenticated/_dashboard/dashboard/projects.$projectid.tsx index fda67d1f1..44f46155d 100644 --- a/ui/src/routes/_authenticated/_dashboard/dashboard/projects.$projectid.tsx +++ b/ui/src/routes/_authenticated/_dashboard/dashboard/projects.$projectid.tsx @@ -34,12 +34,12 @@ const getDriftIcon = (status: string) => { } export const Route = createFileRoute( - '/_authenticated/_dashboard/dashboard/projects/$projectId', + '/_authenticated/_dashboard/dashboard/projects/$projectid', )({ component: RouteComponent, - loader: async ({ context, params: {projectId} }) => { + loader: async ({ context, params: {projectid} }) => { const { user, organisationId } = context; - const project = await getProjectFn({data: {projectId, organisationId, userId: user?.id || ''}}) + const project = await getProjectFn({data: {projectId: projectid, organisationId, userId: user?.id || ''}}) return { project } } }) diff --git a/ui/src/routes/_authenticated/_dashboard/dashboard/projects.index.tsx b/ui/src/routes/_authenticated/_dashboard/dashboard/projects.index.tsx index 730dd0a80..33fac3a05 100644 --- a/ui/src/routes/_authenticated/_dashboard/dashboard/projects.index.tsx +++ b/ui/src/routes/_authenticated/_dashboard/dashboard/projects.index.tsx @@ -129,7 +129,7 @@ function RouteComponent() { diff --git a/ui/vite.config.ts b/ui/vite.config.ts index bf3f0e1ab..b2c2bde88 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig, loadEnv } from 'vite'; import tsConfigPaths from 'vite-tsconfig-paths'; import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import viteReact from '@vitejs/plugin-react'; -import netlify from '@netlify/vite-plugin-tanstack-start'; +// import netlify from '@netlify/vite-plugin-tanstack-start'; export default defineConfig(({ mode }) => { @@ -26,8 +26,6 @@ export default defineConfig(({ mode }) => { tsConfigPaths({ projects: ['./tsconfig.json'], }), - netlify(), - // cloudflare({ viteEnvironment: { name: 'ssr' } }), tanstackStart(), viteReact(), ], From 407fb585401327903eb8764142bee1c022120635 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Fri, 17 Oct 2025 16:34:29 -0700 Subject: [PATCH 02/17] remove helm archives --- .gitignore | 6 +++++- helm-charts/opentaco/Chart.lock | 18 ------------------ .../opentaco/charts/digger-backend-0.1.12.tgz | Bin 2589 -> 0 bytes helm-charts/opentaco/charts/drift-0.1.0.tgz | Bin 3563 -> 0 bytes .../opentaco/charts/postgresql-15.5.38.tgz | Bin 75781 -> 0 bytes .../opentaco/charts/statesman-0.1.0.tgz | Bin 2696 -> 0 bytes helm-charts/opentaco/charts/ui-0.1.0.tgz | Bin 3067 -> 0 bytes 7 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 helm-charts/opentaco/Chart.lock delete mode 100644 helm-charts/opentaco/charts/digger-backend-0.1.12.tgz delete mode 100644 helm-charts/opentaco/charts/drift-0.1.0.tgz delete mode 100644 helm-charts/opentaco/charts/postgresql-15.5.38.tgz delete mode 100644 helm-charts/opentaco/charts/statesman-0.1.0.tgz delete mode 100644 helm-charts/opentaco/charts/ui-0.1.0.tgz diff --git a/.gitignore b/.gitignore index 38384ad25..84bc9c07d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,8 @@ data/ taco/data/ .registry-config -.secrets/ \ No newline at end of file +.secrets/ + +# Helm chart archives (generated by helm dependency update) +**/charts/*.tgz +**/Chart.lock \ No newline at end of file diff --git a/helm-charts/opentaco/Chart.lock b/helm-charts/opentaco/Chart.lock deleted file mode 100644 index 45cf8c680..000000000 --- a/helm-charts/opentaco/Chart.lock +++ /dev/null @@ -1,18 +0,0 @@ -dependencies: -- name: postgresql - repository: https://charts.bitnami.com/bitnami - version: 15.5.38 -- name: digger-backend - repository: file://../digger-backend - version: 0.1.12 -- name: statesman - repository: file://../taco-statesman - version: 0.1.0 -- name: drift - repository: file://../digger-drift - version: 0.1.0 -- name: ui - repository: file://../taco-ui - version: 0.1.0 -digest: sha256:cc82ef456f670919d3100e6336abc4a200917cf5ea37bfdbd3285a6d001bf130 -generated: "2025-10-17T13:52:03.661873-07:00" diff --git a/helm-charts/opentaco/charts/digger-backend-0.1.12.tgz b/helm-charts/opentaco/charts/digger-backend-0.1.12.tgz deleted file mode 100644 index 3ad304cf9e469e9533dfaf44f1be5133f4ad42c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2589 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 diff --git a/helm-charts/opentaco/charts/postgresql-15.5.38.tgz b/helm-charts/opentaco/charts/postgresql-15.5.38.tgz deleted file mode 100644 index 55ad8887f9909254e858b2dc7819c678b37ee9f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75781 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 z 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

%xVd_aT0Bpe15RL%Gf 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 X57)!>hpyiO00960Z3E0@0H_E6!0;oN diff --git a/helm-charts/opentaco/charts/statesman-0.1.0.tgz b/helm-charts/opentaco/charts/statesman-0.1.0.tgz deleted file mode 100644 index 74d32582bdb8d8ff959b20d83eb5b60346f921f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2696 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 J3 Date: Fri, 17 Oct 2025 16:55:31 -0700 Subject: [PATCH 03/17] add placeholder files --- .gitignore | 6 +++++- ...alues-production.yaml => values-production.yaml.example} | 4 ++-- .../opentaco/{values-test.yaml => values-test.yaml.example} | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) rename helm-charts/opentaco/{values-production.yaml => values-production.yaml.example} (96%) rename helm-charts/opentaco/{values-test.yaml => values-test.yaml.example} (90%) diff --git a/.gitignore b/.gitignore index 84bc9c07d..15fb568a7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,8 @@ taco/data/ # Helm chart archives (generated by helm dependency update) **/charts/*.tgz -**/Chart.lock \ No newline at end of file +**/Chart.lock + +# Helm values with actual infrastructure config (team-specific) +helm-charts/opentaco/values-production.yaml +helm-charts/opentaco/values-test.yaml \ No newline at end of file diff --git a/helm-charts/opentaco/values-production.yaml b/helm-charts/opentaco/values-production.yaml.example similarity index 96% rename from helm-charts/opentaco/values-production.yaml rename to helm-charts/opentaco/values-production.yaml.example index 3618304a5..eba6656d1 100644 --- a/helm-charts/opentaco/values-production.yaml +++ b/helm-charts/opentaco/values-production.yaml.example @@ -2,7 +2,7 @@ # Copy this file and customize for your environment global: - imageRegistry: us-central1-docker.pkg.dev/prod-415611/opentaco + imageRegistry: us-central1-docker.pkg.dev/YOUR-PROJECT-ID/opentaco imagePullPolicy: IfNotPresent # ============================================================================ @@ -13,7 +13,7 @@ postgresql: cloudSql: enabled: true - instanceConnectionName: "prod-415611:us-central1:opentaco-postgres" + instanceConnectionName: "YOUR-PROJECT-ID:YOUR-REGION:YOUR-INSTANCE" credentialsSecret: "cloudsql-credentials" serviceAccount: "cloudsql-sa" diff --git a/helm-charts/opentaco/values-test.yaml b/helm-charts/opentaco/values-test.yaml.example similarity index 90% rename from helm-charts/opentaco/values-test.yaml rename to helm-charts/opentaco/values-test.yaml.example index 7caa45741..90e4d2015 100644 --- a/helm-charts/opentaco/values-test.yaml +++ b/helm-charts/opentaco/values-test.yaml.example @@ -5,7 +5,7 @@ # Global Configuration # ============================================================================ global: - imageRegistry: us-central1-docker.pkg.dev/prod-415611/opentaco + imageRegistry: us-central1-docker.pkg.dev/YOUR-PROJECT-ID/opentaco imagePullPolicy: IfNotPresent imagePullSecrets: - name: gcr-json-key @@ -18,7 +18,7 @@ postgresql: cloudSql: enabled: false # Only statesman uses Cloud SQL, configured per-service below - instanceConnectionName: "prod-415611:us-central1:taco-postgres" + instanceConnectionName: "YOUR-PROJECT-ID:YOUR-REGION:YOUR-INSTANCE" credentialsSecret: "cloudsql-credentials" serviceAccount: "cloudsql-sa" @@ -46,7 +46,7 @@ statesman: existingSecretName: "statesman-secrets" cloudSql: enabled: true # Only statesman uses Cloud SQL - instanceConnectionName: "prod-415611:us-central1:taco-postgres" + instanceConnectionName: "YOUR-PROJECT-ID:YOUR-REGION:YOUR-INSTANCE" credentialsSecret: "cloudsql-credentials" storage: type: "s3" From 30f16e6b3602d63071ef3271a5e7679834f766bc Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Fri, 17 Oct 2025 17:04:23 -0700 Subject: [PATCH 04/17] enable custom env setting for tests --- helm-charts/digger-backend/templates/backend-deployment.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helm-charts/digger-backend/templates/backend-deployment.yaml b/helm-charts/digger-backend/templates/backend-deployment.yaml index 988e81198..cf92c5345 100644 --- a/helm-charts/digger-backend/templates/backend-deployment.yaml +++ b/helm-charts/digger-backend/templates/backend-deployment.yaml @@ -38,6 +38,10 @@ spec: - secretRef: name: {{ include "digger-backend.fullname" . }}-secret {{- end }} + {{- if .Values.digger.customEnv }} + env: + {{- toYaml .Values.digger.customEnv | nindent 10 }} + {{- end }} {{- if .Values.digger.livenessProbe }} livenessProbe: {{- toYaml .Values.digger.livenessProbe | nindent 10 }} From d19a8c0e80c166fb03b352788a1ca8c8bb1be7e5 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 09:47:02 -0700 Subject: [PATCH 05/17] release adjustments --- .github/workflows/backend-ee-release.yml | 114 ++++++++++++++++++ .github/workflows/drift-release.yml | 113 +++++++++++++++++ .github/workflows/ui-release.yml | 110 +++++++++++++++++ Dockerfile_backend_ee | 2 +- Dockerfile_drift | 2 +- .../opentaco/values-production.yaml.example | 2 +- helm-charts/opentaco/values-test.yaml.example | 39 +++--- 7 files changed, 362 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/backend-ee-release.yml create mode 100644 .github/workflows/drift-release.yml create mode 100644 .github/workflows/ui-release.yml diff --git a/.github/workflows/backend-ee-release.yml b/.github/workflows/backend-ee-release.yml new file mode 100644 index 000000000..0634d171f --- /dev/null +++ b/.github/workflows/backend-ee-release.yml @@ -0,0 +1,114 @@ +name: Backend EE Docker Release + +on: + push: + tags: + - 'backend-ee/v*' + +permissions: + contents: write + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/digger-backend-ee + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Derive version + id: meta + run: | + TAG="${GITHUB_REF_NAME}" # e.g. backend-ee/v1.2.3 + VERSION="${TAG##*/}" # v1.2.3 + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: docker-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ steps.meta.outputs.version }} + type=ref,event=tag + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile_backend_ee + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + build-args: | + COMMIT_SHA=${{ github.sha }} + VERSION=${{ steps.meta.outputs.version }} + + create-release: + needs: [build-and-push] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Derive version + id: meta + run: | + TAG="${GITHUB_REF_NAME}" # e.g. backend-ee/v1.2.3 + VERSION="${TAG##*/}" # v1.2.3 + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name }} + name: Digger Backend EE ${{ steps.meta.outputs.version }} + body: | + ## Digger Backend (Enterprise Edition) ${{ steps.meta.outputs.version }} + + Terraform orchestration service with advanced features and multi-VCS support. + + ### Docker Image + ```bash + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + ``` + + ### Kubernetes Deployment + ```bash + helm repo add digger https://diggerhq.github.io/digger + helm install digger-backend digger/digger-backend \ + --set digger.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ + --set digger.image.tag=${{ steps.meta.outputs.version }} + ``` + + ### Enterprise Features + - Multi-VCS support (GitHub, GitLab, Bitbucket) + - Web UI for management + - Policy engine for governance + - Advanced CI/CD integrations + - Artefacts storage and management + draft: false + prerelease: false + diff --git a/.github/workflows/drift-release.yml b/.github/workflows/drift-release.yml new file mode 100644 index 000000000..07d164dfe --- /dev/null +++ b/.github/workflows/drift-release.yml @@ -0,0 +1,113 @@ +name: Drift Docker Release + +on: + push: + tags: + - 'drift/v*' + +permissions: + contents: write + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/drift + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Derive version + id: meta + run: | + TAG="${GITHUB_REF_NAME}" # e.g. drift/v1.2.3 + VERSION="${TAG##*/}" # v1.2.3 + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: docker-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ steps.meta.outputs.version }} + type=ref,event=tag + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile_drift + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + build-args: | + COMMIT_SHA=${{ github.sha }} + VERSION=${{ steps.meta.outputs.version }} + + create-release: + needs: [build-and-push] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Derive version + id: meta + run: | + TAG="${GITHUB_REF_NAME}" # e.g. drift/v1.2.3 + VERSION="${TAG##*/}" # v1.2.3 + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name }} + name: Drift ${{ steps.meta.outputs.version }} + body: | + ## Drift Detection Service ${{ steps.meta.outputs.version }} + + Automated infrastructure drift detection and reporting service (Enterprise Edition). + + ### Docker Image + ```bash + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + ``` + + ### Kubernetes Deployment + ```bash + helm repo add digger https://diggerhq.github.io/digger + helm install drift digger/digger-drift \ + --set drift.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ + --set drift.image.tag=${{ steps.meta.outputs.version }} + ``` + + ### Features + - Multi-VCS support (GitHub, GitLab, Bitbucket) + - Automated drift detection + - Slack notifications + - GitHub issue creation + draft: false + prerelease: false + diff --git a/.github/workflows/ui-release.yml b/.github/workflows/ui-release.yml new file mode 100644 index 000000000..42f5d7999 --- /dev/null +++ b/.github/workflows/ui-release.yml @@ -0,0 +1,110 @@ +name: UI Docker Release + +on: + push: + tags: + - 'ui/v*' + +permissions: + contents: write + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/taco-ui + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Derive version + id: meta + run: | + TAG="${GITHUB_REF_NAME}" # e.g. ui/v1.2.3 + VERSION="${TAG##*/}" # v1.2.3 + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: docker-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ steps.meta.outputs.version }} + type=ref,event=tag + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile_ui + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + build-args: | + COMMIT_SHA=${{ github.sha }} + VERSION=${{ steps.meta.outputs.version }} + + create-release: + needs: [build-and-push] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Derive version + id: meta + run: | + TAG="${GITHUB_REF_NAME}" # e.g. ui/v1.2.3 + VERSION="${TAG##*/}" # v1.2.3 + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name }} + name: Taco UI ${{ steps.meta.outputs.version }} + body: | + ## Taco UI ${{ steps.meta.outputs.version }} + + Web-based frontend for OpenTaco infrastructure management platform. + + ### Docker Image + ```bash + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + ``` + + ### Kubernetes Deployment + ```bash + helm repo add digger https://diggerhq.github.io/digger + helm install taco-ui digger/taco-ui \ + --set ui.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ + --set ui.image.tag=${{ steps.meta.outputs.version }} + ``` + + ### Features + - Modern React-based interface + - Server-side rendering with TanStack Start + - WorkOS authentication + - Real-time project monitoring + - Infrastructure drift visualization + draft: false + prerelease: false + diff --git a/Dockerfile_backend_ee b/Dockerfile_backend_ee index 59bd3c085..f08a977d3 100644 --- a/Dockerfile_backend_ee +++ b/Dockerfile_backend_ee @@ -43,7 +43,7 @@ RUN curl -sSf https://atlasgo.sh | sh # Set gin to production -#ENV GIN_MODE=release +ENV GIN_MODE=release # Expose the running port EXPOSE 3000 diff --git a/Dockerfile_drift b/Dockerfile_drift index 984b1f89c..71d237f61 100644 --- a/Dockerfile_drift +++ b/Dockerfile_drift @@ -35,7 +35,7 @@ RUN curl -sSf https://atlasgo.sh | sh # Set gin to production -#ENV GIN_MODE=release +ENV GIN_MODE=release # Expose the running port EXPOSE 3000 diff --git a/helm-charts/opentaco/values-production.yaml.example b/helm-charts/opentaco/values-production.yaml.example index eba6656d1..b9bdd08eb 100644 --- a/helm-charts/opentaco/values-production.yaml.example +++ b/helm-charts/opentaco/values-production.yaml.example @@ -2,7 +2,7 @@ # Copy this file and customize for your environment global: - imageRegistry: us-central1-docker.pkg.dev/YOUR-PROJECT-ID/opentaco + imageRegistry: ghcr.io/diggerhq/digger imagePullPolicy: IfNotPresent # ============================================================================ diff --git a/helm-charts/opentaco/values-test.yaml.example b/helm-charts/opentaco/values-test.yaml.example index 90e4d2015..381279a41 100644 --- a/helm-charts/opentaco/values-test.yaml.example +++ b/helm-charts/opentaco/values-test.yaml.example @@ -5,10 +5,11 @@ # Global Configuration # ============================================================================ global: - imageRegistry: us-central1-docker.pkg.dev/YOUR-PROJECT-ID/opentaco + imageRegistry: ghcr.io/diggerhq/digger imagePullPolicy: IfNotPresent - imagePullSecrets: - - name: gcr-json-key + # Note: imagePullSecrets not needed for public GHCR images + # imagePullSecrets: + # - name: gcr-json-key # ============================================================================ # Database - Use Cloud SQL @@ -27,9 +28,10 @@ cloudSql: # ============================================================================ digger-backend: enabled: true - global: - imagePullSecrets: - - name: gcr-json-key + # Note: imagePullSecrets not needed for public images + # global: + # imagePullSecrets: + # - name: gcr-json-key digger: replicaCount: 1 secret: @@ -38,9 +40,10 @@ digger-backend: statesman: enabled: true - global: - imagePullSecrets: - - name: gcr-json-key + # Note: imagePullSecrets not needed for public images + # global: + # imagePullSecrets: + # - name: gcr-json-key taco: replicaCount: 1 existingSecretName: "statesman-secrets" @@ -53,20 +56,22 @@ statesman: drift: enabled: true - global: - imagePullSecrets: - - name: gcr-json-key + # Note: imagePullSecrets not needed for public images + # global: + # imagePullSecrets: + # - name: gcr-json-key drift: replicaCount: 1 existingSecretName: "drift-secrets" ui: enabled: true - imagePullSecrets: - - name: gcr-json-key - global: - imagePullSecrets: - - name: gcr-json-key + # Note: imagePullSecrets not needed for public images + # imagePullSecrets: + # - name: gcr-json-key + # global: + # imagePullSecrets: + # - name: gcr-json-key ui: replicaCount: 1 existingSecretName: "ui-secrets" From af22a8525eeaa76186ab568d36c0a9d35453bee1 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 11:17:29 -0700 Subject: [PATCH 06/17] edit release workflows and remove dead code --- .github/workflows/backend-ee-release.yml | 18 --------------- .github/workflows/drift-release.yml | 17 -------------- .github/workflows/ui-release.yml | 14 ------------ helm-charts/opentaco/values.yaml | 29 +++--------------------- 4 files changed, 3 insertions(+), 75 deletions(-) diff --git a/.github/workflows/backend-ee-release.yml b/.github/workflows/backend-ee-release.yml index 0634d171f..eb369f833 100644 --- a/.github/workflows/backend-ee-release.yml +++ b/.github/workflows/backend-ee-release.yml @@ -21,10 +21,6 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-go@v5 - with: - go-version: '1.24' - - name: Derive version id: meta run: | @@ -95,20 +91,6 @@ jobs: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} ``` - ### Kubernetes Deployment - ```bash - helm repo add digger https://diggerhq.github.io/digger - helm install digger-backend digger/digger-backend \ - --set digger.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ - --set digger.image.tag=${{ steps.meta.outputs.version }} - ``` - - ### Enterprise Features - - Multi-VCS support (GitHub, GitLab, Bitbucket) - - Web UI for management - - Policy engine for governance - - Advanced CI/CD integrations - - Artefacts storage and management draft: false prerelease: false diff --git a/.github/workflows/drift-release.yml b/.github/workflows/drift-release.yml index 07d164dfe..d1d53524f 100644 --- a/.github/workflows/drift-release.yml +++ b/.github/workflows/drift-release.yml @@ -21,10 +21,6 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-go@v5 - with: - go-version: '1.24' - - name: Derive version id: meta run: | @@ -95,19 +91,6 @@ jobs: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} ``` - ### Kubernetes Deployment - ```bash - helm repo add digger https://diggerhq.github.io/digger - helm install drift digger/digger-drift \ - --set drift.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ - --set drift.image.tag=${{ steps.meta.outputs.version }} - ``` - - ### Features - - Multi-VCS support (GitHub, GitLab, Bitbucket) - - Automated drift detection - - Slack notifications - - GitHub issue creation draft: false prerelease: false diff --git a/.github/workflows/ui-release.yml b/.github/workflows/ui-release.yml index 42f5d7999..0054dd658 100644 --- a/.github/workflows/ui-release.yml +++ b/.github/workflows/ui-release.yml @@ -91,20 +91,6 @@ jobs: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} ``` - ### Kubernetes Deployment - ```bash - helm repo add digger https://diggerhq.github.io/digger - helm install taco-ui digger/taco-ui \ - --set ui.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ - --set ui.image.tag=${{ steps.meta.outputs.version }} - ``` - - ### Features - - Modern React-based interface - - Server-side rendering with TanStack Start - - WorkOS authentication - - Real-time project monitoring - - Infrastructure drift visualization draft: false prerelease: false diff --git a/helm-charts/opentaco/values.yaml b/helm-charts/opentaco/values.yaml index 988ffb3ee..1858d0d7f 100644 --- a/helm-charts/opentaco/values.yaml +++ b/helm-charts/opentaco/values.yaml @@ -73,16 +73,7 @@ digger-backend: replicaCount: 1 - # Database connection (auto-configured based on postgresql/cloudSql settings) - # Will be overridden by this umbrella chart - database: - host: "" # Set via template - port: 5432 - name: digger - user: digger - # Password from secret - existingSecret: "digger-db-secret" - existingSecretKey: "password" + # Database configuration managed via secrets (see backend-secrets in values-test.yaml.example) service: type: ClusterIP @@ -110,14 +101,7 @@ taco-statesman: replicaCount: 1 - # Database connection (auto-configured) - database: - host: "" # Set via template - port: 5432 - name: taco - user: taco - existingSecret: "taco-db-secret" - existingSecretKey: "password" + # Database configuration managed via secrets (see statesman-secrets in values-test.yaml.example) service: type: ClusterIP @@ -143,14 +127,7 @@ drift: replicaCount: 1 - # Database connection (auto-configured) - database: - host: "" # Set via template - port: 5432 - name: drift - user: drift - existingSecret: "drift-db-secret" - existingSecretKey: "password" + # Database configuration managed via secrets (see drift-secrets in values-test.yaml.example) service: type: ClusterIP From d1275a801d8f8a502e0ccc7e121305e8758e863e Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 11:48:06 -0700 Subject: [PATCH 07/17] update image values --- helm-charts/digger-backend/values.yaml | 2 +- helm-charts/digger-drift/values.yaml | 5 ++--- helm-charts/opentaco/values-test.yaml.example | 2 +- helm-charts/opentaco/values.yaml | 10 +++++----- helm-charts/taco-statesman/values.yaml | 4 +--- helm-charts/taco-ui/values.yaml | 8 +++----- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/helm-charts/digger-backend/values.yaml b/helm-charts/digger-backend/values.yaml index 00ecdc2aa..e1953ffed 100644 --- a/helm-charts/digger-backend/values.yaml +++ b/helm-charts/digger-backend/values.yaml @@ -7,7 +7,7 @@ digger: # Image configuration image: - repository: us-central1-docker.pkg.dev/prod-415611/opentaco/digger-backend-ee + repository: ghcr.io/diggerhq/digger/digger-backend-ee tag: "latest" pullPolicy: IfNotPresent diff --git a/helm-charts/digger-drift/values.yaml b/helm-charts/digger-drift/values.yaml index 860ef9827..c22ae5b99 100644 --- a/helm-charts/digger-drift/values.yaml +++ b/helm-charts/digger-drift/values.yaml @@ -2,10 +2,9 @@ drift: # Image configuration - # NOTE: This image needs to be built first! See helm-charts/BUILD_IMAGES.md - # Build with: docker build -t YOUR_REGISTRY/drift:v0.1.0 -f Dockerfile_drift . + # Public image available at ghcr.io/diggerhq/digger/drift image: - repository: us-central1-docker.pkg.dev/prod-415611/opentaco/drift + repository: ghcr.io/diggerhq/digger/drift tag: "latest" pullPolicy: "IfNotPresent" diff --git a/helm-charts/opentaco/values-test.yaml.example b/helm-charts/opentaco/values-test.yaml.example index 381279a41..81740627d 100644 --- a/helm-charts/opentaco/values-test.yaml.example +++ b/helm-charts/opentaco/values-test.yaml.example @@ -77,7 +77,7 @@ ui: existingSecretName: "ui-secrets" env: # Backend URLs point to in-cluster services (default is good) - allowedHosts: "localhost" # ✅ CHANGED: Allow localhost for port-forward testing + allowedHosts: "localhost" ingress: enabled: false # Using port-forward for testing diff --git a/helm-charts/opentaco/values.yaml b/helm-charts/opentaco/values.yaml index 1858d0d7f..82167ae91 100644 --- a/helm-charts/opentaco/values.yaml +++ b/helm-charts/opentaco/values.yaml @@ -12,7 +12,7 @@ # ============================================================================ global: # Image registry for all custom images - imageRegistry: us-central1-docker.pkg.dev/prod-415611/opentaco + imageRegistry: ghcr.io/diggerhq/digger # Image pull policy imagePullPolicy: IfNotPresent @@ -68,7 +68,7 @@ digger-backend: digger: image: - repository: us-central1-docker.pkg.dev/prod-415611/opentaco/digger-backend-ee + repository: ghcr.io/diggerhq/digger/digger-backend-ee tag: "latest" replicaCount: 1 @@ -96,7 +96,7 @@ taco-statesman: taco: image: - repository: us-central1-docker.pkg.dev/prod-415611/opentaco/taco-statesman + repository: ghcr.io/diggerhq/digger/taco-statesman tag: "latest" replicaCount: 1 @@ -122,7 +122,7 @@ drift: drift: image: - repository: us-central1-docker.pkg.dev/prod-415611/opentaco/drift + repository: ghcr.io/diggerhq/digger/drift tag: "latest" replicaCount: 1 @@ -141,7 +141,7 @@ taco-ui: ui: image: - repository: us-central1-docker.pkg.dev/prod-415611/opentaco/taco-ui + repository: ghcr.io/diggerhq/digger/taco-ui tag: "latest" replicaCount: 1 diff --git a/helm-charts/taco-statesman/values.yaml b/helm-charts/taco-statesman/values.yaml index 924b40546..b2ecfa2a0 100644 --- a/helm-charts/taco-statesman/values.yaml +++ b/helm-charts/taco-statesman/values.yaml @@ -2,10 +2,8 @@ taco: # Image configuration - # NOTE: This image needs to be built first! See helm-charts/BUILD_IMAGES.md - # Build with: cd taco && docker build -t YOUR_REGISTRY/taco-statesman:v0.1.0 -f Dockerfile_statesman . image: - repository: us-central1-docker.pkg.dev/prod-415611/opentaco/taco-statesman + repository: ghcr.io/diggerhq/digger/taco-statesman tag: "latest" pullPolicy: "IfNotPresent" diff --git a/helm-charts/taco-ui/values.yaml b/helm-charts/taco-ui/values.yaml index 1da24629e..387e46c4d 100644 --- a/helm-charts/taco-ui/values.yaml +++ b/helm-charts/taco-ui/values.yaml @@ -2,16 +2,14 @@ ui: # Image configuration - # This is a standalone Node.js + TanStack Start SSR app (no Netlify!) + # This is a standalone Node.js + TanStack Start SSR app # The image includes: # - Production-optimized Node.js server with static asset serving # - Built for linux/amd64 platform (GCP/GKE compatible) # - # To build and push: - # ./build-all-images.sh YOUR_REGISTRY VERSION - # docker push YOUR_REGISTRY/taco-ui:VERSION + # Public image available at ghcr.io/diggerhq/digger/taco-ui image: - repository: us-central1-docker.pkg.dev/prod-415611/opentaco/taco-ui + repository: ghcr.io/diggerhq/digger/taco-ui tag: "latest" pullPolicy: "IfNotPresent" From 22c34bf9bb267ed4cef0a2907037b2c27bbb5b5b Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 12:10:44 -0700 Subject: [PATCH 08/17] remove stripe, hardcoded cloud sql instance id --- helm-charts/opentaco/values-production.yaml.example | 3 --- helm-charts/opentaco/values.yaml | 9 +-------- helm-charts/taco-ui/values.yaml | 5 ----- ui/vite.config.ts | 1 - 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/helm-charts/opentaco/values-production.yaml.example b/helm-charts/opentaco/values-production.yaml.example index b9bdd08eb..f998d939c 100644 --- a/helm-charts/opentaco/values-production.yaml.example +++ b/helm-charts/opentaco/values-production.yaml.example @@ -115,9 +115,6 @@ taco-ui: workos: clientId: "client_XXXXX" # CHANGE THIS - your WorkOS client ID secretName: "workos-secrets" - stripe: - publishableKey: "pk_live_XXXXX" # CHANGE THIS if using Stripe - secretName: "stripe-secrets" posthog: key: "" # Optional: Add PostHog key host: "https://app.posthog.com" diff --git a/helm-charts/opentaco/values.yaml b/helm-charts/opentaco/values.yaml index 82167ae91..103b03dc7 100644 --- a/helm-charts/opentaco/values.yaml +++ b/helm-charts/opentaco/values.yaml @@ -21,8 +21,6 @@ global: secrets: # WorkOS authentication (required for UI) workosSecretName: "workos-secrets" - # Stripe billing (optional) - stripeSecretName: "stripe-secrets" # ============================================================================ # Database Configuration @@ -54,7 +52,7 @@ postgresql: cloudSql: enabled: true # Set to true to use Cloud SQL # Connection details - instanceConnectionName: "prod-415611:us-central1:taco-postgres" + instanceConnectionName: "YOUR-PROJECT-ID:YOUR-REGION:YOUR-INSTANCE" # Database credentials (create secret first) credentialsSecret: "cloudsql-credentials" # Service account for Workload Identity @@ -165,11 +163,6 @@ taco-ui: clientId: "" # Set this secretName: "workos-secrets" - # Stripe billing (optional) - stripe: - publishableKey: "" - secretName: "stripe-secrets" - # PostHog analytics (optional) posthog: key: "" diff --git a/helm-charts/taco-ui/values.yaml b/helm-charts/taco-ui/values.yaml index 387e46c4d..355603f91 100644 --- a/helm-charts/taco-ui/values.yaml +++ b/helm-charts/taco-ui/values.yaml @@ -32,11 +32,6 @@ ui: clientId: "" # Use secretName for sensitive data secretName: "" - # Stripe configuration (optional) - stripe: - publishableKey: "" - # Use secretName for sensitive data - secretName: "" # PostHog configuration (optional) posthog: key: "" diff --git a/ui/vite.config.ts b/ui/vite.config.ts index b2c2bde88..658f7dbb2 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig, loadEnv } from 'vite'; import tsConfigPaths from 'vite-tsconfig-paths'; import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import viteReact from '@vitejs/plugin-react'; -// import netlify from '@netlify/vite-plugin-tanstack-start'; export default defineConfig(({ mode }) => { From b8eec4ef69cb55a8389ab4043c24c6a40ab0f2cb Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 12:16:17 -0700 Subject: [PATCH 09/17] remove build file --- build-all-images.sh | 131 -------------------------------------------- 1 file changed, 131 deletions(-) delete mode 100755 build-all-images.sh diff --git a/build-all-images.sh b/build-all-images.sh deleted file mode 100755 index 2536e3ff8..000000000 --- a/build-all-images.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/bin/bash -# Build all Docker images for Digger/Taco services -# -# IMPORTANT: Images are built locally and NOT pushed automatically. -# You control where/if they get pushed. -# -# Usage: -# ./build-all-images.sh [REGISTRY] [VERSION] -# -# Examples: -# ./build-all-images.sh us-central1-docker.pkg.dev/my-project/digger v0.1.0 # GCP Artifact Registry (private) -# ./build-all-images.sh ghcr.io/my-org v0.1.0 # GitHub (can be private/public) -# ./build-all-images.sh # Just tag locally, don't specify registry -# -set -e - -# Configuration - YOU MUST SET YOUR REGISTRY! -if [ -z "$1" ]; then - echo "ERROR: Registry not specified!" - echo "" - echo "Usage: $0 REGISTRY [VERSION]" - echo "" - echo "For GCP (private by default):" - echo " $0 REGION-docker.pkg.dev/PROJECT/REPO v0.1.0" - echo "" - echo "For GitHub Container Registry (set to private in settings):" - echo " $0 ghcr.io/YOUR_ORG v0.1.0" - echo "" - exit 1 -fi - -REGISTRY="${1}" -VERSION="${2:-v0.1.0}" -COMMIT_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown") - -# Colors for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" -echo -e "${GREEN}║ Building Digger/Taco Docker Images ║${NC}" -echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" -echo "" -echo -e "${YELLOW}Registry:${NC} $REGISTRY" -echo -e "${YELLOW}Version:${NC} $VERSION" -echo -e "${YELLOW}Commit:${NC} $COMMIT_SHA" -echo -e "${YELLOW}Platform:${NC} linux/amd64 (for GKE/GCP)" -echo "" -echo -e "${YELLOW}Note:${NC} Building on ARM64 Mac for AMD64 deployment" -echo -e " This ensures images run correctly in GCP/GKE" -echo "" - -# Build drift -echo -e "${YELLOW}[1/3] Building drift service...${NC}" -docker build \ - --platform linux/amd64 \ - -t ${REGISTRY}/drift:${VERSION} \ - -t ${REGISTRY}/drift:latest \ - --build-arg COMMIT_SHA=${COMMIT_SHA} \ - -f Dockerfile_drift \ - . -echo -e "${GREEN}✓ Drift built${NC}" -echo "" - -# Build taco-statesman -echo -e "${YELLOW}[2/3] Building taco-statesman...${NC}" -cd taco -docker build \ - --platform linux/amd64 \ - -t ${REGISTRY}/taco-statesman:${VERSION} \ - -t ${REGISTRY}/taco-statesman:latest \ - --build-arg COMMIT_SHA=${COMMIT_SHA} \ - -f Dockerfile_statesman \ - . -cd .. -echo -e "${GREEN}✓ Taco-statesman built${NC}" -echo "" - -# Build taco-ui (standalone Node.js SSR app - no Netlify!) -echo -e "${YELLOW}[3/3] Building taco-ui (Node.js + TanStack Start)...${NC}" -docker build \ - --platform linux/amd64 \ - -t ${REGISTRY}/taco-ui:${VERSION} \ - -t ${REGISTRY}/taco-ui:latest \ - --build-arg COMMIT_SHA=${COMMIT_SHA} \ - -f Dockerfile_ui \ - . -echo -e "${GREEN}✓ Taco-ui built (standalone Node.js, no Netlify dependencies)${NC}" -echo "" - -echo -e "${GREEN}═══════════════════════════════════════${NC}" -echo -e "${GREEN}All images built successfully!${NC}" -echo -e "${GREEN}═══════════════════════════════════════${NC}" -echo "" -echo -e "${YELLOW}Built images:${NC}" -echo " • ${REGISTRY}/drift:${VERSION}" -echo " • ${REGISTRY}/taco-statesman:${VERSION}" -echo " • ${REGISTRY}/taco-ui:${VERSION} (standalone Node.js + SSR)" -echo "" -echo -e "${YELLOW}Image Details:${NC}" -echo " • drift: Go-based drift detection service" -echo " • taco-statesman: Go-based IaC orchestration service" -echo " • taco-ui: React SSR app (TanStack Start, no Netlify)" -echo "" -echo -e "${YELLOW}IMPORTANT: Images are built locally only.${NC}" -echo -e "${YELLOW}They are NOT automatically pushed to any registry.${NC}" -echo "" -echo -e "${YELLOW}To push to your PRIVATE registry:${NC}" -echo "" -echo " # First, authenticate (if not already done):" -echo " gcloud auth configure-docker ${REGISTRY%%/*} # For GCP" -echo " # OR" -echo " docker login ${REGISTRY%%/*} # For other registries" -echo "" -echo " # Then push:" -echo " docker push ${REGISTRY}/drift:${VERSION}" -echo " docker push ${REGISTRY}/drift:latest" -echo " docker push ${REGISTRY}/taco-statesman:${VERSION}" -echo " docker push ${REGISTRY}/taco-statesman:latest" -echo " docker push ${REGISTRY}/taco-ui:${VERSION}" -echo " docker push ${REGISTRY}/taco-ui:latest" -echo "" -echo -e "${YELLOW}Or push all at once:${NC}" -cat < Date: Mon, 20 Oct 2025 12:23:54 -0700 Subject: [PATCH 10/17] make better notes of place holder values --- helm-charts/README.md | 109 +++++++++++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/helm-charts/README.md b/helm-charts/README.md index 7b886c5cb..b2af91803 100644 --- a/helm-charts/README.md +++ b/helm-charts/README.md @@ -5,10 +5,14 @@ Production-ready Kubernetes deployment for the OpenTaco infrastructure managemen ## Quick Start ```bash -# 1. Create namespace +# 1. Configure values file (see Configuration Checklist below) +cp opentaco/values-test.yaml.example opentaco/values-test.yaml +# Edit values-test.yaml with your GCP project ID and settings + +# 2. Create namespace kubectl create namespace opentaco -# 2. Create secrets (see Secret Management below) +# 3. Create secrets (see Secret Management below) kubectl create secret generic ui-secrets \ --from-env-file=.secrets/ui.env -n opentaco @@ -21,7 +25,7 @@ kubectl create secret generic statesman-secrets \ kubectl create secret generic drift-secrets \ --from-env-file=.secrets/drift.env -n opentaco -# 3. Deploy +# 4. Deploy cd opentaco helm install opentaco . -f values-test.yaml -n opentaco ``` @@ -35,6 +39,92 @@ The umbrella chart deploys 4 services: - **statesman** (port 8080) - IaC state management with Cloud SQL - **ui** (port 3030) - Web frontend +## Configuration Checklist + +Before deploying, you need to configure placeholder values in your values file. + +### Required Placeholders in `values-test.yaml` or `values-production.yaml` + +#### 1. **Cloud SQL Configuration** (if using Cloud SQL for statesman) + +```yaml +cloudSql: + enabled: true + instanceConnectionName: "YOUR-PROJECT-ID:YOUR-REGION:YOUR-INSTANCE" # ❌ CHANGE THIS + credentialsSecret: "cloudsql-credentials" + serviceAccount: "cloudsql-sa" +``` + +**What to do:** +- Replace `YOUR-PROJECT-ID` with your GCP project ID (e.g., `my-prod-project`) +- Replace `YOUR-REGION` with your Cloud SQL region (e.g., `us-central1`) +- Replace `YOUR-INSTANCE` with your Cloud SQL instance name (e.g., `opentaco-postgres`) + +Example: `my-prod-project:us-central1:opentaco-postgres` + +#### 2. **Image Registry** (optional - defaults to public GHCR) + +```yaml +global: + imageRegistry: ghcr.io/diggerhq/digger # ✅ Public registry (no auth needed) + # Or use your private registry: + # imageRegistry: us-central1-docker.pkg.dev/YOUR-PROJECT/YOUR-REPO +``` + +**What to do:** +- Keep default for public images (recommended) +- OR replace with your private registry path if using custom builds + +#### 3. **UI Ingress Configuration** (for production with custom domain) + +```yaml +taco-ui: + ui: + ingress: + enabled: false # Set to true for production + hosts: + - host: app.opentaco.example.com # ❌ CHANGE THIS + paths: + - path: / + pathType: Prefix + tls: + - secretName: opentaco-ui-tls + hosts: + - app.opentaco.example.com # ❌ CHANGE THIS +``` + +**What to do:** +- Replace `app.opentaco.example.com` with your actual domain +- Set `enabled: true` when ready to expose publicly +- Ensure you have an Ingress Controller installed (see Ingress Setup below) + +#### 4. **Service Replica Counts** (optional - defaults to 1) + +```yaml +digger-backend: + digger: + replicaCount: 1 # Increase for high availability + +taco-statesman: + taco: + replicaCount: 1 # Increase for high availability +``` + +**What to do:** +- Keep `1` for test/dev environments +- Increase to `2+` for production high availability + +### Quick Validation + +Before deploying, check your values file for these patterns: + +```bash +# In your values-test.yaml or values-production.yaml +grep -E "YOUR-|example\.com|CHANGE THIS" opentaco/values-test.yaml +``` + +If this returns any results, you have placeholders that need to be filled in! + ## Secret Management ### 1. Copy Example Files @@ -297,16 +387,3 @@ helm-charts/ | `values-production.yaml` | Production-ready settings | | `.secrets/*.env` | Environment-specific secrets (not committed) | -## Security Notes - -- Never commit `.secrets/` directory to version control -- Use strong random secrets (32-64 characters) -- Rotate secrets regularly -- Review service account permissions -- Enable network policies for production - -## Support - -For issues and documentation: -- GitHub: https://github.com/diggerhq/digger -- Docs: https://docs.digger.dev From c6950fa3a580e9d0c1cee99f4354ff61f026e1e0 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 12:38:10 -0700 Subject: [PATCH 11/17] address lint errors --- helm-charts/digger-drift/templates/deployment.yaml | 2 ++ helm-charts/taco-statesman/templates/deployment.yaml | 7 ++++--- helm-charts/taco-ui/templates/deployment.yaml | 11 ----------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/helm-charts/digger-drift/templates/deployment.yaml b/helm-charts/digger-drift/templates/deployment.yaml index 30833df55..1d1a82b37 100644 --- a/helm-charts/digger-drift/templates/deployment.yaml +++ b/helm-charts/digger-drift/templates/deployment.yaml @@ -14,10 +14,12 @@ spec: labels: {{- include "digger-drift.selectorLabels" . | nindent 8 }} spec: + {{- if .Values.global }} {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- end }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.drift.image.repository }}:{{ .Values.drift.image.tag | default .Chart.AppVersion }}" diff --git a/helm-charts/taco-statesman/templates/deployment.yaml b/helm-charts/taco-statesman/templates/deployment.yaml index 9d64742e6..bb23610ae 100644 --- a/helm-charts/taco-statesman/templates/deployment.yaml +++ b/helm-charts/taco-statesman/templates/deployment.yaml @@ -14,18 +14,19 @@ spec: labels: {{- include "taco-statesman.selectorLabels" . | nindent 8 }} spec: - {{- if .Values.taco.cloudSql.enabled }} + {{- if and .Values.taco.cloudSql .Values.taco.cloudSql.enabled }} serviceAccountName: {{ .Values.taco.cloudSql.serviceAccount | default "default" }} {{- end }} + {{- if .Values.global }} {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- end }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.taco.image.repository }}:{{ .Values.taco.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.taco.image.pullPolicy | default "IfNotPresent" }} - imagePullPolicy: {{ .Values.taco.image.pullPolicy | default "IfNotPresent" }} ports: - name: http containerPort: {{ .Values.taco.service.port }} @@ -78,7 +79,7 @@ spec: port: http initialDelaySeconds: 5 periodSeconds: 5 - {{- if .Values.taco.cloudSql.enabled }} + {{- if and .Values.taco.cloudSql .Values.taco.cloudSql.enabled }} - name: cloud-sql-proxy image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.0 args: diff --git a/helm-charts/taco-ui/templates/deployment.yaml b/helm-charts/taco-ui/templates/deployment.yaml index 74b041378..7a7c0f69c 100644 --- a/helm-charts/taco-ui/templates/deployment.yaml +++ b/helm-charts/taco-ui/templates/deployment.yaml @@ -58,17 +58,6 @@ spec: name: {{ .Values.ui.env.workos.secretName }} key: cookie-password {{- end }} - {{- if .Values.ui.env.stripe.publishableKey }} - - name: VITE_STRIPE_PUBLISHABLE_KEY - value: "{{ .Values.ui.env.stripe.publishableKey }}" - {{- end }} - {{- if .Values.ui.env.stripe.secretName }} - - name: STRIPE_SECRET_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.ui.env.stripe.secretName }} - key: secret-key - {{- end }} {{- if .Values.ui.env.posthog.key }} - name: VITE_POSTHOG_KEY value: "{{ .Values.ui.env.posthog.key }}" From 62e35b6b85dfe60ab032d51ddadcf410c038c6d2 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 18:17:32 -0700 Subject: [PATCH 12/17] address comments --- .github/workflows/helm-release.yml | 13 ++++++++++--- helm-charts/opentaco/Chart.yaml | 16 ++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml index 145d0a10e..41ca3b1d8 100644 --- a/.github/workflows/helm-release.yml +++ b/.github/workflows/helm-release.yml @@ -14,6 +14,13 @@ permissions: jobs: release: runs-on: ubuntu-latest + strategy: + matrix: + chart: + - digger-backend + - digger-drift + - taco-statesman + - taco-ui steps: - name: Checkout uses: actions/checkout@v4 @@ -25,8 +32,8 @@ jobs: run: | echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Package and push chart + - name: Package and push ${{ matrix.chart }} run: | - cd helm-charts/digger-backend + cd helm-charts/${{ matrix.chart }} helm package . - helm push digger-backend-*.tgz oci://ghcr.io/diggerhq/helm-charts \ No newline at end of file + helm push ${{ matrix.chart }}-*.tgz oci://ghcr.io/diggerhq/helm-charts \ No newline at end of file diff --git a/helm-charts/opentaco/Chart.yaml b/helm-charts/opentaco/Chart.yaml index 34ec5c611..dd0835914 100644 --- a/helm-charts/opentaco/Chart.yaml +++ b/helm-charts/opentaco/Chart.yaml @@ -15,9 +15,9 @@ appVersion: "0.1.0" dependencies: # Optional PostgreSQL - disable if using Cloud SQL - - name: postgresql - version: "15.x.x" - repository: https://charts.bitnami.com/bitnami + - name: cloudnative-pg + version: "~0.22.0" + repository: https://cloudnative-pg.github.io/charts condition: postgresql.enabled tags: - database @@ -25,7 +25,7 @@ dependencies: # Digger Backend - terraform orchestration - name: digger-backend version: "0.1.12" - repository: "file://../digger-backend" + repository: "oci://ghcr.io/diggerhq/helm-charts" condition: digger-backend.enabled tags: - backend @@ -33,7 +33,7 @@ dependencies: # Taco Statesman - IaC state management - name: statesman version: "0.1.0" - repository: "file://../taco-statesman" + repository: "oci://ghcr.io/diggerhq/helm-charts" condition: taco-statesman.enabled tags: - backend @@ -41,7 +41,7 @@ dependencies: # Drift Detection - name: drift version: "0.1.0" - repository: "file://../digger-drift" + repository: "oci://ghcr.io/diggerhq/helm-charts" condition: drift.enabled tags: - backend @@ -49,14 +49,14 @@ dependencies: # Taco UI - React frontend - name: ui version: "0.1.0" - repository: "file://../taco-ui" + repository: "oci://ghcr.io/diggerhq/helm-charts" condition: taco-ui.enabled tags: - frontend maintainers: - name: OpenTaco Team - email: team@opentaco.dev + email: info@digger.dev keywords: - infrastructure From 2b892a1bcbc29ea191e9ba1d80dcde54b195c35d Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 18:22:33 -0700 Subject: [PATCH 13/17] update readme --- helm-charts/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/helm-charts/README.md b/helm-charts/README.md index b2af91803..743631a82 100644 --- a/helm-charts/README.md +++ b/helm-charts/README.md @@ -6,7 +6,8 @@ Production-ready Kubernetes deployment for the OpenTaco infrastructure managemen ```bash # 1. Configure values file (see Configuration Checklist below) -cp opentaco/values-test.yaml.example opentaco/values-test.yaml +curl -O https://raw.githubusercontent.com/diggerhq/digger/develop/helm-charts/opentaco/values-test.yaml.example +mv values-test.yaml.example values-test.yaml # Edit values-test.yaml with your GCP project ID and settings # 2. Create namespace @@ -25,9 +26,10 @@ kubectl create secret generic statesman-secrets \ kubectl create secret generic drift-secrets \ --from-env-file=.secrets/drift.env -n opentaco -# 4. Deploy -cd opentaco -helm install opentaco . -f values-test.yaml -n opentaco +# 4. Deploy from OCI registry +helm install opentaco oci://ghcr.io/diggerhq/helm-charts/opentaco \ + -f values-test.yaml \ + -n opentaco ``` ## Architecture From 9e7413e76997cbbdca9c3a41de1e3285cf59fec9 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Mon, 20 Oct 2025 18:24:04 -0700 Subject: [PATCH 14/17] add umbrella chart to the workflow as well --- .github/workflows/helm-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml index 41ca3b1d8..d857a3fce 100644 --- a/.github/workflows/helm-release.yml +++ b/.github/workflows/helm-release.yml @@ -21,6 +21,7 @@ jobs: - digger-drift - taco-statesman - taco-ui + - opentaco steps: - name: Checkout uses: actions/checkout@v4 From 2ea64fdb8e7cba518078b55d2cba65cf396bde7e Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Wed, 22 Oct 2025 09:09:56 -0700 Subject: [PATCH 15/17] add back backend chart from ddevelop, adjust values --- helm-charts/digger-backend/Chart.yaml | 2 +- .../templates/backend-deployment.yaml | 90 ++++++----- .../templates/digger-secret.yaml | 20 +++ .../templates/postgres-secret.yaml | 13 ++ .../templates/postgres-service.yaml | 12 ++ .../templates/postgres-statefulset.yaml | 35 +++++ helm-charts/digger-backend/values.yaml | 127 +++++++++++---- .../digger-drift/templates/deployment.yaml | 135 ++-------------- .../digger-drift/templates/secret.yaml | 44 ++++++ helm-charts/digger-drift/values.yaml | 73 +++++---- helm-charts/digger-managed/.helmignore | 24 +++ helm-charts/digger-managed/Chart.yaml | 24 +++ .../digger-managed/templates/_helpers.tpl | 51 +++++++ .../templates/backend-deployment.yaml | 77 ++++++++++ .../templates/backend-ingress.yaml | 44 ++++++ .../templates/backend-service.yaml | 11 ++ .../templates/postgres-secret.yaml | 12 ++ .../digger-managed/templates/secret.yaml | 79 ++++++++++ .../tests/deployments_test.yaml | 0 helm-charts/digger-managed/values.yaml | 144 ++++++++++++++++++ helm-charts/opentaco/Chart.yaml | 12 +- helm-charts/opentaco/templates/NOTES.txt | 6 +- .../opentaco/values-production.yaml.example | 15 +- helm-charts/opentaco/values-test.yaml.example | 2 +- helm-charts/opentaco/values.yaml | 52 +++++-- helm-charts/secrets-example/drift.env | 15 +- .../taco-statesman/templates/deployment.yaml | 48 +++--- .../taco-statesman/templates/secret.yaml | 75 +++++++++ helm-charts/taco-statesman/values.yaml | 103 ++++++++++++- helm-charts/taco-ui/templates/deployment.yaml | 32 ++-- helm-charts/taco-ui/templates/secret.yaml | 47 ++++++ helm-charts/taco-ui/values.yaml | 74 +++++++-- 32 files changed, 1181 insertions(+), 317 deletions(-) create mode 100644 helm-charts/digger-backend/templates/digger-secret.yaml create mode 100644 helm-charts/digger-backend/templates/postgres-secret.yaml create mode 100644 helm-charts/digger-backend/templates/postgres-service.yaml create mode 100644 helm-charts/digger-backend/templates/postgres-statefulset.yaml create mode 100644 helm-charts/digger-drift/templates/secret.yaml create mode 100644 helm-charts/digger-managed/.helmignore create mode 100644 helm-charts/digger-managed/Chart.yaml create mode 100644 helm-charts/digger-managed/templates/_helpers.tpl create mode 100644 helm-charts/digger-managed/templates/backend-deployment.yaml create mode 100644 helm-charts/digger-managed/templates/backend-ingress.yaml create mode 100644 helm-charts/digger-managed/templates/backend-service.yaml create mode 100644 helm-charts/digger-managed/templates/postgres-secret.yaml create mode 100644 helm-charts/digger-managed/templates/secret.yaml rename helm-charts/{digger-backend => digger-managed}/tests/deployments_test.yaml (100%) create mode 100644 helm-charts/digger-managed/values.yaml create mode 100644 helm-charts/taco-statesman/templates/secret.yaml create mode 100644 helm-charts/taco-ui/templates/secret.yaml diff --git a/helm-charts/digger-backend/Chart.yaml b/helm-charts/digger-backend/Chart.yaml index 9a2836038..91113e436 100644 --- a/helm-charts/digger-backend/Chart.yaml +++ b/helm-charts/digger-backend/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: digger-backend -description: Digger Backend - Terraform orchestration and IaC management service +description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. # diff --git a/helm-charts/digger-backend/templates/backend-deployment.yaml b/helm-charts/digger-backend/templates/backend-deployment.yaml index cf92c5345..1b2c76a35 100644 --- a/helm-charts/digger-backend/templates/backend-deployment.yaml +++ b/helm-charts/digger-backend/templates/backend-deployment.yaml @@ -2,67 +2,73 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "digger-backend.fullname" . }}-web - labels: - {{- include "digger-backend.labels" . | nindent 4 }} spec: - replicas: {{ .Values.digger.replicaCount }} + replicas: 1 selector: matchLabels: - app: digger-backend-web + app: {{ include "digger-backend.name" . }}-web template: metadata: labels: - app: digger-backend-web - {{- include "digger-backend.selectorLabels" . | nindent 8 }} + app: {{ include "digger-backend.name" . }}-web spec: - {{- if .Values.global }} - {{- with .Values.global.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} + {{- if $.Values.digger.nodeSelector }} + nodeSelector: + {{- toYaml $.Values.digger.nodeSelector | nindent 8 }} {{- end }} + {{- if $.Values.digger.tolerations }} + tolerations: + {{- toYaml $.Values.digger.tolerations | nindent 8 }} {{- end }} containers: - name: web image: "{{ .Values.digger.image.repository }}:{{ .Values.digger.image.tag }}" - imagePullPolicy: {{ .Values.digger.image.pullPolicy | default "IfNotPresent" }} ports: - - name: http - containerPort: 3000 - protocol: TCP - {{- if .Values.digger.secret.useExistingSecret }} - envFrom: - - secretRef: - name: {{ .Values.digger.secret.existingSecretName }} - {{- else }} + - containerPort: 3000 + livenessProbe: + {{ .Values.digger.livenessProbe | toYaml | indent 10 | trim }} + startupProbe: + {{ .Values.digger.startupProbe | toYaml | indent 10 | trim }} envFrom: - secretRef: + {{- if not .Values.digger.secret.useExistingSecret }} name: {{ include "digger-backend.fullname" . }}-secret - {{- end }} - {{- if .Values.digger.customEnv }} - env: - {{- toYaml .Values.digger.customEnv | nindent 10 }} - {{- end }} - {{- if .Values.digger.livenessProbe }} - livenessProbe: - {{- toYaml .Values.digger.livenessProbe | nindent 10 }} - {{- end }} - {{- if .Values.digger.startupProbe }} - startupProbe: - {{- toYaml .Values.digger.startupProbe | nindent 10 }} + {{- else }} + name: {{ .Values.digger.secret.existingSecretName }} {{- end }} {{- with .Values.digger.resources }} resources: {{- toYaml . | nindent 10 }} {{- end }} - {{- with .Values.digger.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.digger.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.digger.tolerations }} - tolerations: + env: + - name: POSTGRES_PASSWORD + {{- if and .Values.postgres.enabled .Values.postgres.secret.useExistingSecret }} + valueFrom: + secretKeyRef: + name: {{ .Values.postgres.secret.existingSecretName }} + key: postgres-password + {{- else if .Values.digger.postgres.existingSecretName }} + valueFrom: + secretKeyRef: + name: {{ .Values.digger.postgres.existingSecretName }} + key: {{ .Values.digger.postgres.existingSecretKey }} + {{- else }} + valueFrom: + secretKeyRef: + name: {{ include "digger-backend.fullname" . }}-postgres-secret + key: postgres-password + {{- end }} + - name: DATABASE_URL + {{- if .Values.postgres.enabled }} + value: "postgres://postgres:$(POSTGRES_PASSWORD)@{{ include "digger-backend.fullname" . }}-postgres:5432/postgres?sslmode={{ .Values.postgres.sslmode }}" + {{- else }} + {{- $pg := .Values.digger.postgres }} + value: "postgres://{{ $pg.user }}:$(POSTGRES_PASSWORD)@{{ $pg.host }}:{{ $pg.port }}/{{ $pg.database }}?sslmode={{ $pg.sslmode }}" + {{- end }} + - name: ALLOW_DIRTY + value: "{{ .Values.digger.postgres.allow_dirty }}" + - name: DIGGER_LOG_LEVEL + value: {{ .Values.digger.logLevel | quote }} + {{- with .Values.digger.customEnv }} {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} diff --git a/helm-charts/digger-backend/templates/digger-secret.yaml b/helm-charts/digger-backend/templates/digger-secret.yaml new file mode 100644 index 000000000..1b7c5549d --- /dev/null +++ b/helm-charts/digger-backend/templates/digger-secret.yaml @@ -0,0 +1,20 @@ +{{- if not .Values.digger.secret.useExistingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "digger-backend.fullname" . }}-secret +type: Opaque +data: + HTTP_BASIC_AUTH_USERNAME: {{ .Values.digger.secret.httpBasicAuthUsername | b64enc | quote }} + HTTP_BASIC_AUTH_PASSWORD: {{ .Values.digger.secret.httpBasicAuthPassword | b64enc | quote }} + BEARER_AUTH_TOKEN: {{ .Values.digger.secret.bearerAuthToken | b64enc | quote }} + HOSTNAME: {{ .Values.digger.secret.hostname | b64enc | quote }} + GITHUB_ORG: {{ .Values.digger.secret.githubOrg | b64enc | quote}} + GITHUB_APP_ID: {{ .Values.digger.secret.githubAppID | b64enc | quote }} + GITHUB_APP_CLIENT_ID: {{ .Values.digger.secret.githubAppClientID | b64enc | quote }} + GITHUB_APP_CLIENT_SECRET: {{ .Values.digger.secret.githubAppClientSecret | b64enc | quote }} + GITHUB_APP_PRIVATE_KEY: {{ .Values.digger.secret.githubAppKeyFile | quote }} + # Note we keeping the one without _BASE64 suffix for backward compatibility + GITHUB_APP_PRIVATE_KEY_BASE64: {{ .Values.digger.secret.githubAppKeyFile | b64enc | quote }} + GITHUB_WEBHOOK_SECRET: {{ .Values.digger.secret.githubWebhookSecret | b64enc | quote }} +{{- end }} diff --git a/helm-charts/digger-backend/templates/postgres-secret.yaml b/helm-charts/digger-backend/templates/postgres-secret.yaml new file mode 100644 index 000000000..eef730519 --- /dev/null +++ b/helm-charts/digger-backend/templates/postgres-secret.yaml @@ -0,0 +1,13 @@ +{{- if or .Values.postgres.enabled .Values.digger.postgres.password }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "digger-backend.fullname" . }}-postgres-secret +type: Opaque +data: + {{- if and .Values.postgres.secret.password (not .Values.postgres.secret.useExistingSecret) }} + postgres-password: {{ .Values.postgres.secret.password | b64enc | quote }} + {{- else }} + postgres-password: {{ .Values.digger.postgres.password | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/helm-charts/digger-backend/templates/postgres-service.yaml b/helm-charts/digger-backend/templates/postgres-service.yaml new file mode 100644 index 000000000..95c32b0c2 --- /dev/null +++ b/helm-charts/digger-backend/templates/postgres-service.yaml @@ -0,0 +1,12 @@ +{{- if .Values.postgres.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "digger-backend.fullname" . }}-postgres +spec: + ports: + - port: 5432 + targetPort: 5432 + selector: + app: {{ include "digger-backend.name" . }}-postgres +{{- end }} diff --git a/helm-charts/digger-backend/templates/postgres-statefulset.yaml b/helm-charts/digger-backend/templates/postgres-statefulset.yaml new file mode 100644 index 000000000..b46cdc460 --- /dev/null +++ b/helm-charts/digger-backend/templates/postgres-statefulset.yaml @@ -0,0 +1,35 @@ +{{- if .Values.postgres.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "digger-backend.fullname" . }}-postgres +spec: + serviceName: "{{ include "digger-backend.fullname" . }}-postgres" + replicas: 1 + selector: + matchLabels: + app: {{ include "digger-backend.name" . }}-postgres + template: + metadata: + labels: + app: {{ include "digger-backend.name" . }}-postgres + spec: + containers: + - name: postgres + image: {{ printf "%s:%s" .Values.postgres.image .Values.postgres.tag }} + ports: + - containerPort: 5432 + {{- if .Values.resources }} + resources: {{- toYaml .Values.postgres.resources | nindent 10 }} + {{- end }} + env: + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + key: postgres-password + {{- if .Values.postgres.secret.useExistingSecret }} + name: {{ .Values.postgres.secret.existingSecretName }} + {{- else }} + name: {{ include "digger-backend.fullname" . }}-postgres-secret + {{- end }} +{{- end }} diff --git a/helm-charts/digger-backend/values.yaml b/helm-charts/digger-backend/values.yaml index e1953ffed..f3fc30131 100644 --- a/helm-charts/digger-backend/values.yaml +++ b/helm-charts/digger-backend/values.yaml @@ -1,42 +1,49 @@ -# Digger Backend - Terraform Orchestration Service -# This chart deploys the Digger backend service +# values.yaml digger: - # Replica count - replicaCount: 1 - - # Image configuration + # image values + # repository: digger backend image repository + # tag: digger backend image tag image: - repository: ghcr.io/diggerhq/digger/digger-backend-ee - tag: "latest" - pullPolicy: IfNotPresent + repository: registry.digger.dev/diggerhq/digger_backend + tag: "v0.6.101" - # Custom environment variables + # Custom environment variables to be added to the backend deployment # Format: # customEnv: # - name: MY_CUSTOM_ENV # value: "my-value" + # - name: ANOTHER_ENV + # value: "another-value" customEnv: [] - # Log level: DEBUG or INFO + # Set the log level for the backend + # DEBUG will enable the debug logs, any other value will set it to INFO logLevel: "INFO" - # Resource limits and requests + # Resource limits and requests for the pods resources: {} # requests: # cpu: 100m - # memory: 256Mi + # memory: 140Mi # limits: # cpu: 500m - # memory: 512Mi + # memory: 200Mi + + # livenessProbe and startupProbe settings + # https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ - # Health check probes + # livenessProbe: configure probing of /health endpoint + # periodSeconds: how often to perform the probe (20 seconds) livenessProbe: httpGet: path: /health port: 3000 periodSeconds: 20 + # startupProbe: configure probing of /health endpoint for startup + # failureThreshold: how many times the probe can fail before the container is considered failed (30 times) + # periodSeconds: how often to perform the probe (10 seconds) startupProbe: httpGet: path: /health @@ -44,33 +51,91 @@ digger: failureThreshold: 30 periodSeconds: 10 - # Service configuration + # service values + # type: service type (ClusterIP, LoadBalancer, etc) + # port: port number to expose service: type: ClusterIP - port: 3000 + port: 3000 # default port for digger backend - # Ingress configuration + # ingress values + # enabled: enable ingress resource or not (default: true) + # host: hostname to use (default: "") + # path: path to expose (default: "/") + # tls: tls settings if using https + # secretName: name of k8s secret for tls certs (default: "digger-backend-tls") ingress: - enabled: false + enabled: true className: "" - annotations: {} + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: 'true' host: "" path: / tls: secretName: "digger-backend-tls" - # Secret configuration - # REQUIRED: Always use an existing secret for production - # Create secret with: kubectl create secret generic backend-secrets --from-env-file=backend.env + # digger requires a secret with the following key-value pairs: + # HTTP_BASIC_AUTH_USERNAME: + # HTTP_BASIC_AUTH_PASSWORD: + # BEARER_AUTH_TOKEN: + # HOSTNAME: + # GITHUB_ORG: + # GITHUB_APP_ID: + # GITHUB_APP_CLIENT_ID: + # GITHUB_APP_CLIENT_SECRET: + # GITHUB_APP_PRIVATE_KEY: + # GITHUB_WEBHOOK_SECRET: + # POSTGRES_PASSWORD: + # pass the content in clear or specify the name of the existing secret secret: - useExistingSecret: true - existingSecretName: "backend-secrets" + useExistingSecret: false + existingSecretName: "" + + httpBasicAuthUsername: "admin" + httpBasicAuthPassword: "admin" + bearerAuthToken: "" # You should generate + hostname: "" + githubOrg: "" + githubAppID: "" + githubAppClientID: "" + githubAppClientSecret: "" + githubAppKeyFile: "" #base64 encoded file + githubWebhookSecret: "" + + # configure this section if you want to use an external postgres database - # Node selector - nodeSelector: {} + postgres: + # specify the secret name and key to pull the existing postgres database password from + existingSecretName: "" + existingSecretKey: "postgres-password" - # Tolerations - tolerations: [] + # to define connection details in chart: + sslmode: "disable" + user: "postgres" + database: "digger" + host: "pg-postgresql.db" + password: "password" + port: "5432" + allow_dirty: false # set to true if the database has already a schema + +# configure this section if you want to deploy a postgres db +# WARNING: use only for test purposes, no persistency has been configured +postgres: + sslmode: "disable" + enabled: false + image: postgres + tag: "14" + resources: + limits: {} + requests: {} + # postgres requires a secret with the following key-value pairs: + # postgres-password: + + # pass the content in clear or specify the name of the existing secret + secret: + useExistingSecret: true + existingSecretName: "new-pg-creds" - # Affinity - affinity: {} + password: "password" diff --git a/helm-charts/digger-drift/templates/deployment.yaml b/helm-charts/digger-drift/templates/deployment.yaml index 1d1a82b37..2a740013f 100644 --- a/helm-charts/digger-drift/templates/deployment.yaml +++ b/helm-charts/digger-drift/templates/deployment.yaml @@ -28,11 +28,13 @@ spec: - name: http containerPort: {{ .Values.drift.service.port }} protocol: TCP - {{- if .Values.drift.existingSecretName }} envFrom: - secretRef: - name: {{ .Values.drift.existingSecretName }} - {{- end }} + {{- if .Values.drift.secret.useExistingSecret }} + name: {{ .Values.drift.secret.existingSecretName }} + {{- else }} + name: {{ include "digger-drift.fullname" . }}-secret + {{- end }} env: - name: DIGGER_PORT value: "{{ .Values.drift.service.port }}" @@ -42,7 +44,6 @@ spec: - name: SENTRY_DSN value: "{{ .Values.drift.sentry.dsn }}" {{- end }} - {{- if not .Values.drift.existingSecretName }} # PostgreSQL configuration - name: POSTGRES_HOST value: "{{ .Values.drift.postgres.host }}" @@ -60,124 +61,13 @@ spec: secretKeyRef: name: {{ .Values.drift.postgres.existingSecretName }} key: {{ .Values.drift.postgres.existingSecretKey }} - {{- else }} + {{- else if .Values.drift.postgres.password }} - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: POSTGRES_PASSWORD - {{- end }} - # Configuration from secret - {{- if .Values.drift.github.existingSecretName }} - - name: DIGGER_HOSTNAME - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: DIGGER_HOSTNAME - - name: DIGGER_WEBHOOK_SECRET - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: DIGGER_WEBHOOK_SECRET - - name: DIGGER_APP_URL - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: DIGGER_APP_URL - optional: true - - name: DIGGER_DRIFT_REPORTER_HOSTNAME - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: DIGGER_DRIFT_REPORTER_HOSTNAME - optional: true - - name: GITHUB_ORG - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: GITHUB_ORG - - name: GITHUB_APP_ID - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: GITHUB_APP_ID - - name: GITHUB_APP_CLIENT_ID - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: GITHUB_APP_CLIENT_ID - - name: GITHUB_APP_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: GITHUB_APP_CLIENT_SECRET - - name: GITHUB_APP_PRIVATE_KEY_BASE64 - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: GITHUB_APP_PRIVATE_KEY_BASE64 - - name: GITHUB_WEBHOOK_SECRET - valueFrom: - secretKeyRef: - name: {{ .Values.drift.github.existingSecretName }} - key: GITHUB_WEBHOOK_SECRET - {{- else }} - - name: DIGGER_HOSTNAME - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: DIGGER_HOSTNAME - - name: DIGGER_WEBHOOK_SECRET - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: DIGGER_WEBHOOK_SECRET - {{- if .Values.drift.config.appUrl }} - - name: DIGGER_APP_URL - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: DIGGER_APP_URL - {{- end }} - {{- if .Values.drift.config.driftReporterHostname }} - - name: DIGGER_DRIFT_REPORTER_HOSTNAME - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: DIGGER_DRIFT_REPORTER_HOSTNAME - {{- end }} - - name: GITHUB_ORG - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: GITHUB_ORG - - name: GITHUB_APP_ID - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: GITHUB_APP_ID - - name: GITHUB_APP_CLIENT_ID - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: GITHUB_APP_CLIENT_ID - - name: GITHUB_APP_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: GITHUB_APP_CLIENT_SECRET - - name: GITHUB_APP_PRIVATE_KEY_BASE64 - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: GITHUB_APP_PRIVATE_KEY_BASE64 - - name: GITHUB_WEBHOOK_SECRET - valueFrom: - secretKeyRef: - name: {{ include "digger-drift.fullname" . }} - key: GITHUB_WEBHOOK_SECRET - {{- end }} + value: "{{ .Values.drift.postgres.password }}" {{- end }} + # Build DATABASE_URL from components + - name: DATABASE_URL + value: "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=$(POSTGRES_SSLMODE)" # Custom environment variables {{- range .Values.drift.customEnv }} - name: {{ .name }} @@ -191,6 +81,10 @@ spec: startupProbe: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.drift.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} {{- with .Values.drift.resources }} resources: {{- toYaml . | nindent 12 }} @@ -207,4 +101,3 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} - diff --git a/helm-charts/digger-drift/templates/secret.yaml b/helm-charts/digger-drift/templates/secret.yaml new file mode 100644 index 000000000..9fd7841fb --- /dev/null +++ b/helm-charts/digger-drift/templates/secret.yaml @@ -0,0 +1,44 @@ +{{- if not .Values.drift.secret.useExistingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "digger-drift.fullname" . }}-secret + labels: + {{- include "digger-drift.labels" . | nindent 4 }} +type: Opaque +stringData: + # Drift service configuration + {{- if .Values.drift.config.hostname }} + DIGGER_HOSTNAME: {{ .Values.drift.config.hostname | quote }} + {{- end }} + {{- if .Values.drift.config.webhookSecret }} + DIGGER_WEBHOOK_SECRET: {{ .Values.drift.config.webhookSecret | quote }} + {{- end }} + {{- if .Values.drift.config.appUrl }} + DIGGER_APP_URL: {{ .Values.drift.config.appUrl | quote }} + {{- end }} + {{- if .Values.drift.config.driftReporterHostname }} + DIGGER_DRIFT_REPORTER_HOSTNAME: {{ .Values.drift.config.driftReporterHostname | quote }} + {{- end }} + + # GitHub configuration + {{- if .Values.drift.github.org }} + GITHUB_ORG: {{ .Values.drift.github.org | quote }} + {{- end }} + {{- if .Values.drift.github.appID }} + GITHUB_APP_ID: {{ .Values.drift.github.appID | quote }} + {{- end }} + {{- if .Values.drift.github.appClientID }} + GITHUB_APP_CLIENT_ID: {{ .Values.drift.github.appClientID | quote }} + {{- end }} + {{- if .Values.drift.github.appClientSecret }} + GITHUB_APP_CLIENT_SECRET: {{ .Values.drift.github.appClientSecret | quote }} + {{- end }} + {{- if .Values.drift.github.appPrivateKey }} + GITHUB_APP_PRIVATE_KEY_BASE64: {{ .Values.drift.github.appPrivateKey | quote }} + {{- end }} + {{- if .Values.drift.github.webhookSecret }} + GITHUB_WEBHOOK_SECRET: {{ .Values.drift.github.webhookSecret | quote }} + {{- end }} +{{- end }} + diff --git a/helm-charts/digger-drift/values.yaml b/helm-charts/digger-drift/values.yaml index c22ae5b99..5fcd2e4c1 100644 --- a/helm-charts/digger-drift/values.yaml +++ b/helm-charts/digger-drift/values.yaml @@ -1,4 +1,11 @@ # values.yaml +# +# This chart creates environment variables from these values. +# You can either: +# 1. Set values here (chart creates secrets automatically) +# 2. Use secret.useExistingSecret=true and create your own secret +# +# See secrets-example/drift.env for a complete example drift: # Image configuration @@ -17,13 +24,15 @@ drift: # value: "my-value" # Set the log level for the drift service + # Creates: DIGGER_LOG_LEVEL # DEBUG will enable the debug logs, any other value will set it to INFO - logLevel: "INFO" + logLevel: "INFO" # DIGGER_LOG_LEVEL # Service configuration + # Creates: DIGGER_PORT (set automatically from port) service: type: ClusterIP - port: 3000 + port: 3000 # DIGGER_PORT # Ingress configuration ingress: @@ -75,35 +84,34 @@ drift: affinity: {} # Drift service configuration - # hostname: The public hostname for this drift service - # webhookSecret: Secret for internal webhook authentication - # appUrl: URL for the Digger app UI (for notifications) - # driftReporterHostname: Hostname for the drift reporter (optional) + # Creates: DIGGER_HOSTNAME, DIGGER_WEBHOOK_SECRET, DIGGER_APP_URL, + # DIGGER_DRIFT_REPORTER_HOSTNAME config: - hostname: "" - webhookSecret: "" - appUrl: "" - driftReporterHostname: "" + hostname: "" # DIGGER_HOSTNAME (public URL for this drift service) + webhookSecret: "" # DIGGER_WEBHOOK_SECRET (32 char secret for webhook auth) + appUrl: "" # DIGGER_APP_URL (URL for Digger app UI) + driftReporterHostname: "" # DIGGER_DRIFT_REPORTER_HOSTNAME (optional) # Sentry configuration (optional) + # Creates: SENTRY_DSN sentry: - dsn: "" + dsn: "" # SENTRY_DSN - # GitHub App configuration + # GitHub App configuration (get from https://github.com/settings/apps) + # Creates: GITHUB_ORG, GITHUB_APP_ID, GITHUB_APP_CLIENT_ID, + # GITHUB_APP_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY_BASE64, GITHUB_WEBHOOK_SECRET # These are required for drift detection to work with GitHub github: - # Use existingSecret to reference an existing secret - # or fill in the values below - existingSecretName: "" - - org: "" - appID: "" - appClientID: "" - appClientSecret: "" - appPrivateKey: "" # base64 encoded private key - webhookSecret: "" + org: "" # GITHUB_ORG (GitHub organization name) + appID: "" # GITHUB_APP_ID + appClientID: "" # GITHUB_APP_CLIENT_ID + appClientSecret: "" # GITHUB_APP_CLIENT_SECRET + appPrivateKey: "" # GITHUB_APP_PRIVATE_KEY_BASE64 (base64 encoded private key) + webhookSecret: "" # GITHUB_WEBHOOK_SECRET # PostgreSQL configuration + # Creates: POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_DB, + # POSTGRES_SSLMODE, POSTGRES_PASSWORD (if not using existingSecret) # The drift service uses the same database as the digger backend postgres: # Use existingSecret to pull password from an existing secret @@ -111,10 +119,19 @@ drift: existingSecretKey: "postgres-password" # Database connection details - sslmode: "disable" - user: "postgres" - database: "digger" - host: "postgresql.default.svc.cluster.local" - password: "" - port: "5432" + sslmode: "disable" # POSTGRES_SSLMODE + user: "postgres" # POSTGRES_USER + database: "digger" # POSTGRES_DB + host: "postgresql.default.svc.cluster.local" # POSTGRES_HOST + password: "" # POSTGRES_PASSWORD + port: "5432" # POSTGRES_PORT + + # Secret configuration + # Use an existing secret or let the chart create one from values above + secret: + useExistingSecret: false + existingSecretName: "" +# Global configuration (optional) +global: + imagePullSecrets: [] diff --git a/helm-charts/digger-managed/.helmignore b/helm-charts/digger-managed/.helmignore new file mode 100644 index 000000000..5b6e763e5 --- /dev/null +++ b/helm-charts/digger-managed/.helmignore @@ -0,0 +1,24 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +tests/ \ No newline at end of file diff --git a/helm-charts/digger-managed/Chart.yaml b/helm-charts/digger-managed/Chart.yaml new file mode 100644 index 000000000..7079289a4 --- /dev/null +++ b/helm-charts/digger-managed/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: digger-managed +description: A Helm chart for Digger Backend (Managed Version) - requires external secrets + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "latest" diff --git a/helm-charts/digger-managed/templates/_helpers.tpl b/helm-charts/digger-managed/templates/_helpers.tpl new file mode 100644 index 000000000..502c34e45 --- /dev/null +++ b/helm-charts/digger-managed/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "digger-managed.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "digger-managed.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "digger-managed.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "digger-managed.labels" -}} +helm.sh/chart: {{ include "digger-managed.chart" . }} +{{ include "digger-managed.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "digger-managed.selectorLabels" -}} +app.kubernetes.io/name: {{ include "digger-managed.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/helm-charts/digger-managed/templates/backend-deployment.yaml b/helm-charts/digger-managed/templates/backend-deployment.yaml new file mode 100644 index 000000000..e41916e2a --- /dev/null +++ b/helm-charts/digger-managed/templates/backend-deployment.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "digger-managed.fullname" . }}-web + labels: + {{- include "digger-managed.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.digger.replicaCount }} + selector: + matchLabels: + app: {{ include "digger-managed.name" . }}-web + template: + metadata: + labels: + app: {{ include "digger-managed.name" . }}-web + {{- include "digger-managed.selectorLabels" . | nindent 8 }} + spec: + {{- if .Values.global }} + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + containers: + - name: web + image: "{{ .Values.digger.image.repository }}:{{ .Values.digger.image.tag }}" + imagePullPolicy: {{ .Values.digger.image.pullPolicy | default "IfNotPresent" }} + ports: + - name: http + containerPort: 3000 + protocol: TCP + envFrom: + - secretRef: + {{- if .Values.digger.secret.useExistingSecret }} + name: {{ .Values.digger.secret.existingSecretName }} + {{- else }} + name: {{ include "digger-managed.fullname" . }}-secret + {{- end }} + env: + {{- if .Values.digger.postgres.existingSecretName }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.digger.postgres.existingSecretName }} + key: {{ .Values.digger.postgres.existingSecretKey }} + - name: DATABASE_URL + value: "postgres://{{ .Values.digger.postgres.user }}:$(POSTGRES_PASSWORD)@{{ .Values.digger.postgres.host }}:{{ .Values.digger.postgres.port }}/{{ .Values.digger.postgres.database }}?sslmode={{ .Values.digger.postgres.sslmode }}" + {{- end }} + - name: ALLOW_DIRTY + value: "{{ .Values.digger.postgres.allowDirty }}" + {{- with .Values.digger.customEnv }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.digger.livenessProbe }} + livenessProbe: + {{- toYaml .Values.digger.livenessProbe | nindent 10 }} + {{- end }} + {{- if .Values.digger.startupProbe }} + startupProbe: + {{- toYaml .Values.digger.startupProbe | nindent 10 }} + {{- end }} + {{- with .Values.digger.resources }} + resources: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.digger.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.digger.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.digger.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm-charts/digger-managed/templates/backend-ingress.yaml b/helm-charts/digger-managed/templates/backend-ingress.yaml new file mode 100644 index 000000000..bc9192898 --- /dev/null +++ b/helm-charts/digger-managed/templates/backend-ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.digger.ingress.enabled -}} +{{- if and .Values.digger.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.digger.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.digger.ingress.annotations "kubernetes.io/ingress.class" .Values.digger.ingress.className }} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ include "digger-managed.fullname" . }} + annotations: + {{- range $key, $value := .Values.digger.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + labels: + {{- include "digger-managed.labels" . | nindent 4 }} +spec: + {{- if and .Values.digger.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.digger.ingress.className }} + {{- end }} + {{- if .Values.digger.ingress.tls }} + tls: + - hosts: + - {{ .Values.digger.ingress.host }} + secretName: {{ .Values.digger.ingress.tls.secretName }} + {{- end }} + rules: + - host: {{ .Values.digger.ingress.host }} + http: + paths: + - path: {{ .Values.digger.ingress.path }} + pathType: Prefix + backend: + service: + name: {{ include "digger-managed.fullname" . }}-web + port: + number: {{ .Values.digger.service.port }} +{{- end }} diff --git a/helm-charts/digger-managed/templates/backend-service.yaml b/helm-charts/digger-managed/templates/backend-service.yaml new file mode 100644 index 000000000..2859e7964 --- /dev/null +++ b/helm-charts/digger-managed/templates/backend-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "digger-managed.fullname" . }}-web +spec: + type: {{ .Values.digger.service.type }} + ports: + - port: {{ .Values.digger.service.port }} + targetPort: 3000 + selector: + app: {{ include "digger-managed.name" . }}-web diff --git a/helm-charts/digger-managed/templates/postgres-secret.yaml b/helm-charts/digger-managed/templates/postgres-secret.yaml new file mode 100644 index 000000000..301396c55 --- /dev/null +++ b/helm-charts/digger-managed/templates/postgres-secret.yaml @@ -0,0 +1,12 @@ +{{- if and (not .Values.digger.secret.useExistingSecret) (not .Values.digger.postgres.existingSecretName) .Values.digger.postgres.password }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "digger-managed.fullname" . }}-postgres-secret + labels: + {{- include "digger-managed.labels" . | nindent 4 }} +type: Opaque +stringData: + postgres-password: {{ .Values.digger.postgres.password | quote }} +{{- end }} + diff --git a/helm-charts/digger-managed/templates/secret.yaml b/helm-charts/digger-managed/templates/secret.yaml new file mode 100644 index 000000000..5d6aae06b --- /dev/null +++ b/helm-charts/digger-managed/templates/secret.yaml @@ -0,0 +1,79 @@ +{{- if not .Values.digger.secret.useExistingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "digger-managed.fullname" . }}-secret + labels: + {{- include "digger-managed.labels" . | nindent 4 }} +type: Opaque +stringData: + # Database Configuration + {{- if or .Values.digger.postgres.host .Values.digger.postgres.user .Values.digger.postgres.database }} + DATABASE_URL: "postgres://{{ .Values.digger.postgres.user }}:{{ .Values.digger.postgres.password }}@{{ .Values.digger.postgres.host }}:{{ .Values.digger.postgres.port }}/{{ .Values.digger.postgres.database }}?sslmode={{ .Values.digger.postgres.sslmode }}" + {{- end }} + + # Digger Configuration + {{- if .Values.digger.config.enableApiEndpoints }} + DIGGER_ENABLE_API_ENDPOINTS: "{{ .Values.digger.config.enableApiEndpoints }}" + {{- end }} + {{- if .Values.digger.config.enableInternalEndpoints }} + DIGGER_ENABLE_INTERNAL_ENDPOINTS: "{{ .Values.digger.config.enableInternalEndpoints }}" + {{- end }} + {{- if .Values.digger.config.encryptionSecret }} + DIGGER_ENCRYPTION_SECRET: {{ .Values.digger.config.encryptionSecret | quote }} + {{- end }} + {{- if .Values.digger.config.generationApiToken }} + DIGGER_GENERATION_API_TOKEN: {{ .Values.digger.config.generationApiToken | quote }} + {{- end }} + {{- if .Values.digger.config.generationEndpoint }} + DIGGER_GENERATION_ENDPOINT: {{ .Values.digger.config.generationEndpoint | quote }} + {{- end }} + {{- if .Values.digger.config.internalSecret }} + DIGGER_INTERNAL_SECRET: {{ .Values.digger.config.internalSecret | quote }} + {{- end }} + {{- if .Values.digger.config.licenseKey }} + DIGGER_LICENSE_KEY: {{ .Values.digger.config.licenseKey | quote }} + {{- end }} + DIGGER_LOAD_PROJECTS_ON_PUSH: "{{ .Values.digger.config.loadProjectsOnPush }}" + DIGGER_LOG_LEVEL: {{ .Values.digger.logLevel | quote }} + DIGGER_MAX_PROJECTS_PER_CHANGE: "{{ .Values.digger.config.maxProjectsPerChange }}" + {{- if .Values.digger.config.projectsSvcAppName }} + DIGGER_PROJECTS_SVC_APP_NAME: {{ .Values.digger.config.projectsSvcAppName | quote }} + {{- end }} + + # GitHub App Configuration + {{- if .Values.digger.github.appClientId }} + GITHUB_APP_CLIENT_ID: {{ .Values.digger.github.appClientId | quote }} + {{- end }} + {{- if .Values.digger.github.appClientSecret }} + GITHUB_APP_CLIENT_SECRET: {{ .Values.digger.github.appClientSecret | quote }} + {{- end }} + {{- if .Values.digger.github.appId }} + GITHUB_APP_ID: {{ .Values.digger.github.appId | quote }} + {{- end }} + {{- if .Values.digger.github.appPrivateKeyBase64 }} + GITHUB_APP_PRIVATE_KEY_BASE64: {{ .Values.digger.github.appPrivateKeyBase64 | quote }} + {{- end }} + {{- if .Values.digger.github.webhookSecret }} + GITHUB_WEBHOOK_SECRET: {{ .Values.digger.github.webhookSecret | quote }} + {{- end }} + + # Go Configuration + GODEBUG: {{ .Values.digger.godebug | quote }} + GOFIPS140: {{ .Values.digger.gofips140 | quote }} + + # Application URLs + {{- if .Values.digger.hostname }} + HOSTNAME: {{ .Values.digger.hostname | quote }} + {{- end }} + JWT_AUTH: "{{ .Values.digger.jwtAuth }}" + + # Optional: Analytics & Monitoring + {{- if .Values.digger.segmentApiKey }} + SEGMENT_API_KEY: {{ .Values.digger.segmentApiKey | quote }} + {{- end }} + {{- if .Values.digger.sentryDsn }} + SENTRY_DSN: {{ .Values.digger.sentryDsn | quote }} + {{- end }} +{{- end }} + diff --git a/helm-charts/digger-backend/tests/deployments_test.yaml b/helm-charts/digger-managed/tests/deployments_test.yaml similarity index 100% rename from helm-charts/digger-backend/tests/deployments_test.yaml rename to helm-charts/digger-managed/tests/deployments_test.yaml diff --git a/helm-charts/digger-managed/values.yaml b/helm-charts/digger-managed/values.yaml new file mode 100644 index 000000000..ffc088837 --- /dev/null +++ b/helm-charts/digger-managed/values.yaml @@ -0,0 +1,144 @@ +# Digger Managed - Terraform Orchestration Service +# +# This chart creates environment variables from these values. +# You can either: +# 1. Set values here (chart creates secrets automatically) +# 2. Use secret.useExistingSecret=true and create your own secret +# +# See secrets-example/digger-backend.env for a complete example + +digger: + # Replica count + replicaCount: 1 + + # Image configuration + image: + repository: ghcr.io/diggerhq/digger/digger-backend-ee + tag: "latest" + pullPolicy: IfNotPresent + + # Custom environment variables + # Format: + # customEnv: + # - name: MY_CUSTOM_ENV + # value: "my-value" + customEnv: [] + + # Log level: DEBUG or INFO + # Creates: DIGGER_LOG_LEVEL + logLevel: "INFO" # DIGGER_LOG_LEVEL + + # Resource limits and requests + resources: {} + # requests: + # cpu: 100m + # memory: 256Mi + # limits: + # cpu: 500m + # memory: 512Mi + + # Health check probes + livenessProbe: + httpGet: + path: /health + port: 3000 + periodSeconds: 20 + + startupProbe: + httpGet: + path: /health + port: 3000 + failureThreshold: 30 + periodSeconds: 10 + + # Service configuration + service: + type: ClusterIP + port: 3000 + + # Ingress configuration + ingress: + enabled: false + className: "" + annotations: {} + host: "" + path: / + tls: + secretName: "digger-backend-tls" + + # PostgreSQL configuration + # Creates: DATABASE_URL (constructed), ALLOW_DIRTY + postgres: + # Use existingSecret to pull password from an existing secret + existingSecretName: "" + existingSecretKey: "postgres-password" + + # Database connection details + sslmode: "disable" # Part of DATABASE_URL + user: "postgres" # Part of DATABASE_URL + database: "digger" # Part of DATABASE_URL + host: "postgresql.default.svc.cluster.local" # Part of DATABASE_URL + password: "" # Part of DATABASE_URL (from secret if existingSecretName set) + port: "5432" # Part of DATABASE_URL + allowDirty: false # ALLOW_DIRTY + + # Digger configuration + # Creates: DIGGER_ENABLE_API_ENDPOINTS, DIGGER_ENABLE_INTERNAL_ENDPOINTS, + # DIGGER_ENCRYPTION_SECRET, DIGGER_GENERATION_API_TOKEN, DIGGER_GENERATION_ENDPOINT, + # DIGGER_INTERNAL_SECRET, DIGGER_LICENSE_KEY, DIGGER_LOAD_PROJECTS_ON_PUSH, + # DIGGER_LOG_LEVEL, DIGGER_MAX_PROJECTS_PER_CHANGE, DIGGER_PROJECTS_SVC_APP_NAME + config: + enableApiEndpoints: true # DIGGER_ENABLE_API_ENDPOINTS + enableInternalEndpoints: true # DIGGER_ENABLE_INTERNAL_ENDPOINTS + encryptionSecret: "" # DIGGER_ENCRYPTION_SECRET (32 chars) + generationApiToken: "" # DIGGER_GENERATION_API_TOKEN + generationEndpoint: "" # DIGGER_GENERATION_ENDPOINT + internalSecret: "" # DIGGER_INTERNAL_SECRET (50 chars) + licenseKey: "" # DIGGER_LICENSE_KEY + loadProjectsOnPush: false # DIGGER_LOAD_PROJECTS_ON_PUSH + maxProjectsPerChange: 100 # DIGGER_MAX_PROJECTS_PER_CHANGE + projectsSvcAppName: "projects-refresh-service" # DIGGER_PROJECTS_SVC_APP_NAME + + # GitHub App configuration (get from https://github.com/settings/apps) + # Creates: GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET, GITHUB_APP_ID, + # GITHUB_APP_PRIVATE_KEY_BASE64, GITHUB_WEBHOOK_SECRET + github: + appClientId: "" # GITHUB_APP_CLIENT_ID + appClientSecret: "" # GITHUB_APP_CLIENT_SECRET + appId: "" # GITHUB_APP_ID + appPrivateKeyBase64: "" # GITHUB_APP_PRIVATE_KEY_BASE64 (base64 encoded private key) + webhookSecret: "" # GITHUB_WEBHOOK_SECRET + + # Application URLs + # Creates: HOSTNAME, JWT_AUTH + hostname: "" # HOSTNAME (e.g., https://app.yourdomain.com) + jwtAuth: true # JWT_AUTH + + # Optional: Analytics & Monitoring + # Creates: SEGMENT_API_KEY, SENTRY_DSN + segmentApiKey: "" # SEGMENT_API_KEY (optional) + sentryDsn: "" # SENTRY_DSN (optional) + + # Go Configuration + # Creates: GODEBUG, GOFIPS140 + godebug: "off" # GODEBUG + gofips140: "off" # GOFIPS140 + + # Secret configuration + # Use an existing secret or let the chart create one from values above + secret: + useExistingSecret: false + existingSecretName: "" + + # Node selector + nodeSelector: {} + + # Tolerations + tolerations: [] + + # Affinity + affinity: {} + +# Global configuration (optional) +global: + imagePullSecrets: [] diff --git a/helm-charts/opentaco/Chart.yaml b/helm-charts/opentaco/Chart.yaml index dd0835914..c51d96743 100644 --- a/helm-charts/opentaco/Chart.yaml +++ b/helm-charts/opentaco/Chart.yaml @@ -8,7 +8,7 @@ appVersion: "0.1.0" # Umbrella chart that deploys all OpenTaco components # This chart orchestrates: # - PostgreSQL database (optional - can use Cloud SQL instead) -# - Digger Backend (terraform orchestration backend) +# - Digger Managed (terraform orchestration backend) # - Taco Statesman (IaC state management) # - Drift Detection service # - Taco UI (React frontend) @@ -22,11 +22,11 @@ dependencies: tags: - database - # Digger Backend - terraform orchestration - - name: digger-backend - version: "0.1.12" - repository: "oci://ghcr.io/diggerhq/helm-charts" - condition: digger-backend.enabled + # Digger Managed - terraform orchestration backend + - name: digger-managed + version: "0.1.0" + repository: "oci://ghcr.io/diggerhq/helm-charts" + condition: digger-managed.enabled tags: - backend diff --git a/helm-charts/opentaco/templates/NOTES.txt b/helm-charts/opentaco/templates/NOTES.txt index 19bf78ffc..b0d39b71a 100644 --- a/helm-charts/opentaco/templates/NOTES.txt +++ b/helm-charts/opentaco/templates/NOTES.txt @@ -20,9 +20,9 @@ DEPLOYMENT STATUS: Please enable postgresql or cloudSql in values.yaml {{- end }} -{{- if index .Values "digger-backend" "enabled" }} -✓ Digger Backend: Enabled - Service: digger-backend:3000 +{{- if index .Values "digger-managed" "enabled" }} +✓ Digger Managed: Enabled + Service: digger-managed:3000 {{- end }} {{- if index .Values "taco-statesman" "enabled" }} diff --git a/helm-charts/opentaco/values-production.yaml.example b/helm-charts/opentaco/values-production.yaml.example index f998d939c..43fc20510 100644 --- a/helm-charts/opentaco/values-production.yaml.example +++ b/helm-charts/opentaco/values-production.yaml.example @@ -18,9 +18,9 @@ cloudSql: serviceAccount: "cloudsql-sa" # ============================================================================ -# Digger Backend +# Digger Managed # ============================================================================ -digger-backend: +digger-managed: enabled: true digger: replicaCount: 2 @@ -36,15 +36,10 @@ digger-backend: className: "nginx" annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" - hosts: - - host: api.opentaco.example.com # CHANGE THIS - paths: - - path: / - pathType: Prefix + host: "api.opentaco.example.com" # CHANGE THIS + path: / tls: - - secretName: digger-backend-tls - hosts: - - api.opentaco.example.com # CHANGE THIS + secretName: digger-managed-tls # ============================================================================ # Taco Statesman diff --git a/helm-charts/opentaco/values-test.yaml.example b/helm-charts/opentaco/values-test.yaml.example index 81740627d..6ddbb1cbe 100644 --- a/helm-charts/opentaco/values-test.yaml.example +++ b/helm-charts/opentaco/values-test.yaml.example @@ -26,7 +26,7 @@ cloudSql: # ============================================================================ # Components - All enabled with defaults # ============================================================================ -digger-backend: +digger-managed: enabled: true # Note: imagePullSecrets not needed for public images # global: diff --git a/helm-charts/opentaco/values.yaml b/helm-charts/opentaco/values.yaml index 103b03dc7..668382e28 100644 --- a/helm-charts/opentaco/values.yaml +++ b/helm-charts/opentaco/values.yaml @@ -2,7 +2,7 @@ # # This chart deploys the complete OpenTaco platform: # - Database (PostgreSQL or Cloud SQL) -# - Digger Backend +# - Digger Managed # - Taco Statesman # - Drift Detection # - Taco UI @@ -59,9 +59,9 @@ cloudSql: serviceAccount: "cloudsql-sa" # ============================================================================ -# Digger Backend Configuration +# Digger Managed Configuration # ============================================================================ -digger-backend: +digger-managed: enabled: true digger: @@ -71,7 +71,42 @@ digger-backend: replicaCount: 1 - # Database configuration managed via secrets (see backend-secrets in values-test.yaml.example) + # Secret configuration + secret: + useExistingSecret: false + existingSecretName: "" + + # Database configuration + postgres: + host: "postgresql.default.svc.cluster.local" + port: "5432" + user: "postgres" + database: "digger" + password: "" + sslmode: "disable" + allowDirty: false + + # Digger configuration + config: + enableApiEndpoints: true + enableInternalEndpoints: true + encryptionSecret: "" + generationApiToken: "" + generationEndpoint: "" + internalSecret: "" + licenseKey: "" + loadProjectsOnPush: false + maxProjectsPerChange: 100 + + # GitHub App configuration + github: + appClientId: "" + appClientSecret: "" + appId: "" + appPrivateKeyBase64: "" + webhookSecret: "" + + hostname: "" service: type: ClusterIP @@ -80,11 +115,8 @@ digger-backend: ingress: enabled: false className: "nginx" - hosts: - - host: api.opentaco.example.com - paths: - - path: / - pathType: Prefix + host: "api.opentaco.example.com" + path: / # ============================================================================ # Taco Statesman Configuration @@ -152,7 +184,7 @@ taco-ui: env: # Backend API URLs (auto-configured to point to services in cluster) apiUrl: "http://taco-statesman:8080" - orchestratorBackendUrl: "http://digger-backend:3000" + orchestratorBackendUrl: "http://digger-managed:3000" driftReportingBackendUrl: "http://drift:3004" # Allowed hosts (customize for your domain) diff --git a/helm-charts/secrets-example/drift.env b/helm-charts/secrets-example/drift.env index fdc9b585a..3c82f5134 100644 --- a/helm-charts/secrets-example/drift.env +++ b/helm-charts/secrets-example/drift.env @@ -10,10 +10,19 @@ DIGGER_APP_URL=https://app.yourdomain.com DIGGER_DRIFT_REPORTER_HOSTNAME=https://app.yourdomain.com/drift-reporting DIGGER_HOSTNAME=https://app.yourdomain.com -# Webhook Secret (should match statesman) +# Webhook Secret DIGGER_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET_32_CHARS -# GitHub App Configuration (same as backend) -# Must be base64-encoded and wrapped in quotes +# GitHub App Configuration +# Get these from https://github.com/settings/apps/YOUR_APP +# Should be the same credentials as digger-backend +GITHUB_ORG=your-github-org +GITHUB_APP_ID=YOUR_GITHUB_APP_ID +GITHUB_APP_CLIENT_ID=YOUR_GITHUB_APP_CLIENT_ID +GITHUB_APP_CLIENT_SECRET=YOUR_GITHUB_APP_CLIENT_SECRET GITHUB_APP_PRIVATE_KEY_BASE64="YOUR_BASE64_ENCODED_PRIVATE_KEY" +GITHUB_WEBHOOK_SECRET=YOUR_GITHUB_WEBHOOK_SECRET + +# Optional: Sentry Monitoring +SENTRY_DSN=YOUR_SENTRY_DSN diff --git a/helm-charts/taco-statesman/templates/deployment.yaml b/helm-charts/taco-statesman/templates/deployment.yaml index bb23610ae..dc668879b 100644 --- a/helm-charts/taco-statesman/templates/deployment.yaml +++ b/helm-charts/taco-statesman/templates/deployment.yaml @@ -31,41 +31,31 @@ spec: - name: http containerPort: {{ .Values.taco.service.port }} protocol: TCP - {{- if .Values.taco.existingSecretName }} envFrom: - secretRef: - name: {{ .Values.taco.existingSecretName }} - {{- end }} + {{- if .Values.taco.secret.useExistingSecret }} + name: {{ .Values.taco.secret.existingSecretName }} + {{- else }} + name: {{ include "taco-statesman.fullname" . }}-secret + {{- end }} env: - name: OPENTACO_PORT value: "{{ .Values.taco.service.port }}" - {{- if not .Values.taco.existingSecretName }} - name: OPENTACO_STORAGE value: "{{ .Values.taco.storage.type }}" {{- if .Values.taco.auth.disable }} - name: OPENTACO_AUTH_DISABLE value: "true" {{- end }} - {{- if and (eq .Values.taco.storage.type "s3") .Values.taco.storage.s3.bucket }} - - name: OPENTACO_S3_BUCKET - value: "{{ .Values.taco.storage.s3.bucket }}" - {{- end }} - {{- if and (eq .Values.taco.storage.type "s3") .Values.taco.storage.s3.region }} - - name: OPENTACO_S3_REGION - value: "{{ .Values.taco.storage.s3.region }}" - {{- end }} - {{- if and (eq .Values.taco.storage.type "s3") .Values.taco.storage.s3.secretName }} - - name: OPENTACO_S3_ACCESS_KEY_ID + {{- if .Values.taco.postgres.existingSecretName }} + - name: OPENTACO_POSTGRES_PASSWORD valueFrom: secretKeyRef: - name: {{ .Values.taco.storage.s3.secretName }} - key: access-key-id - - name: OPENTACO_S3_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.taco.storage.s3.secretName }} - key: secret-access-key + name: {{ .Values.taco.postgres.existingSecretName }} + key: {{ .Values.taco.postgres.existingSecretKey }} {{- end }} + {{- with .Values.taco.customEnv }} + {{- toYaml . | nindent 12 }} {{- end }} livenessProbe: httpGet: @@ -79,6 +69,10 @@ spec: port: http initialDelaySeconds: 5 periodSeconds: 5 + {{- with .Values.taco.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} {{- if and .Values.taco.cloudSql .Values.taco.cloudSql.enabled }} - name: cloud-sql-proxy image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.0 @@ -102,3 +96,15 @@ spec: secret: secretName: {{ .Values.taco.cloudSql.credentialsSecret }} {{- end }} + {{- with .Values.taco.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.taco.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.taco.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm-charts/taco-statesman/templates/secret.yaml b/helm-charts/taco-statesman/templates/secret.yaml new file mode 100644 index 000000000..2764cfd0a --- /dev/null +++ b/helm-charts/taco-statesman/templates/secret.yaml @@ -0,0 +1,75 @@ +{{- if not .Values.taco.secret.useExistingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "taco-statesman.fullname" . }}-secret + labels: + {{- include "taco-statesman.labels" . | nindent 4 }} +type: Opaque +stringData: + # Storage configuration + {{- if eq .Values.taco.storage.type "s3" }} + OPENTACO_S3_BUCKET: {{ .Values.taco.storage.s3.bucket | quote }} + OPENTACO_S3_REGION: {{ .Values.taco.storage.s3.region | quote }} + {{- if .Values.taco.storage.s3.prefix }} + OPENTACO_S3_PREFIX: {{ .Values.taco.storage.s3.prefix | quote }} + {{- end }} + {{- if .Values.taco.storage.s3.endpoint }} + AWS_ENDPOINT: {{ .Values.taco.storage.s3.endpoint | quote }} + {{- end }} + {{- if .Values.taco.storage.s3.accessKeyId }} + AWS_ACCESS_KEY_ID: {{ .Values.taco.storage.s3.accessKeyId | quote }} + {{- end }} + {{- if .Values.taco.storage.s3.secretAccessKey }} + AWS_SECRET_ACCESS_KEY: {{ .Values.taco.storage.s3.secretAccessKey | quote }} + {{- end }} + {{- if .Values.taco.storage.s3.awsRegion }} + AWS_REGION: {{ .Values.taco.storage.s3.awsRegion | quote }} + {{- end }} + {{- end }} + + # Auth configuration + {{- if not .Values.taco.auth.disable }} + {{- if .Values.taco.auth.issuer }} + OPENTACO_AUTH_ISSUER: {{ .Values.taco.auth.issuer | quote }} + {{- end }} + {{- if .Values.taco.auth.clientId }} + OPENTACO_AUTH_CLIENT_ID: {{ .Values.taco.auth.clientId | quote }} + {{- end }} + {{- if .Values.taco.auth.clientSecret }} + OPENTACO_AUTH_CLIENT_SECRET: {{ .Values.taco.auth.clientSecret | quote }} + {{- end }} + {{- if .Values.taco.auth.authUrl }} + OPENTACO_AUTH_AUTH_URL: {{ .Values.taco.auth.authUrl | quote }} + {{- end }} + {{- if .Values.taco.auth.tokenUrl }} + OPENTACO_AUTH_TOKEN_URL: {{ .Values.taco.auth.tokenUrl | quote }} + {{- end }} + {{- end }} + + # Internal API configuration + {{- if .Values.taco.enableInternalEndpoints }} + OPENTACO_ENABLE_INTERNAL_ENDPOINTS: {{ .Values.taco.internalSecret | quote }} + {{- end }} + + # PostgreSQL configuration + {{- if .Values.taco.postgres.host }} + OPENTACO_POSTGRES_HOST: {{ .Values.taco.postgres.host | quote }} + {{- end }} + {{- if .Values.taco.postgres.port }} + OPENTACO_POSTGRES_PORT: {{ .Values.taco.postgres.port | quote }} + {{- end }} + {{- if .Values.taco.postgres.user }} + OPENTACO_POSTGRES_USER: {{ .Values.taco.postgres.user | quote }} + {{- end }} + {{- if .Values.taco.postgres.database }} + OPENTACO_POSTGRES_DBNAME: {{ .Values.taco.postgres.database | quote }} + {{- end }} + {{- if .Values.taco.postgres.queryBackend }} + OPENTACO_QUERY_BACKEND: {{ .Values.taco.postgres.queryBackend | quote }} + {{- end }} + {{- if and (not .Values.taco.postgres.existingSecretName) .Values.taco.postgres.password }} + OPENTACO_POSTGRES_PASSWORD: {{ .Values.taco.postgres.password | quote }} + {{- end }} +{{- end }} + diff --git a/helm-charts/taco-statesman/values.yaml b/helm-charts/taco-statesman/values.yaml index b2ecfa2a0..d1da8df80 100644 --- a/helm-charts/taco-statesman/values.yaml +++ b/helm-charts/taco-statesman/values.yaml @@ -1,4 +1,11 @@ # values.yaml +# +# This chart creates environment variables from these values. +# You can either: +# 1. Set values here (chart creates secrets automatically) +# 2. Use secret.useExistingSecret=true and create your own secret +# +# See secrets-example/statesman.env for a complete example taco: # Image configuration @@ -11,20 +18,102 @@ taco: replicaCount: 1 # Service configuration + # Creates: OPENTACO_PORT (set automatically from port) service: type: ClusterIP - port: 8080 + port: 8080 # OPENTACO_PORT - # Storage type: "memory" or "s3" + # Storage configuration + # Creates: OPENTACO_STORAGE storage: + # Storage type: "memory" or "s3" type: "memory" + # S3 configuration (if using S3 storage) + # Creates: OPENTACO_S3_BUCKET, OPENTACO_S3_REGION, OPENTACO_S3_PREFIX + # AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_ENDPOINT s3: - bucket: "" - region: "us-east-1" - # Use Kubernetes secrets for credentials - secretName: "taco-s3-credentials" + bucket: "" # OPENTACO_S3_BUCKET + region: "us-east-1" # OPENTACO_S3_REGION + prefix: "" # OPENTACO_S3_PREFIX (optional) + endpoint: "" # AWS_ENDPOINT (optional, for S3-compatible storage like Tigris) + # AWS credentials - can be overridden by existingSecretName + accessKeyId: "" # AWS_ACCESS_KEY_ID + secretAccessKey: "" # AWS_SECRET_ACCESS_KEY + awsRegion: "auto" # AWS_REGION (for S3-compatible storage) - # Authentication + # Authentication configuration + # Creates: OPENTACO_AUTH_DISABLE, OPENTACO_AUTH_ISSUER, OPENTACO_AUTH_CLIENT_ID, + # OPENTACO_AUTH_CLIENT_SECRET, OPENTACO_AUTH_AUTH_URL, OPENTACO_AUTH_TOKEN_URL auth: disable: false + # Auth0 configuration (get from https://manage.auth0.com/) + issuer: "" # OPENTACO_AUTH_ISSUER (e.g., https://your-tenant.auth0.com/) + clientId: "" # OPENTACO_AUTH_CLIENT_ID + clientSecret: "" # OPENTACO_AUTH_CLIENT_SECRET + authUrl: "" # OPENTACO_AUTH_AUTH_URL (e.g., https://your-tenant.auth0.com/authorize) + tokenUrl: "" # OPENTACO_AUTH_TOKEN_URL (e.g., https://your-tenant.auth0.com/oauth/token) + + # Internal API endpoints + # Creates: OPENTACO_ENABLE_INTERNAL_ENDPOINTS + # Set to true to enable internal endpoints (requires secret) + enableInternalEndpoints: false + internalSecret: "" # OPENTACO_ENABLE_INTERNAL_ENDPOINTS (64 char secret) + + # PostgreSQL configuration + # Creates: OPENTACO_POSTGRES_HOST, OPENTACO_POSTGRES_PORT, OPENTACO_POSTGRES_USER, + # OPENTACO_POSTGRES_PASSWORD, OPENTACO_POSTGRES_DBNAME, OPENTACO_QUERY_BACKEND + postgres: + # Use existingSecret to pull password from an existing secret + existingSecretName: "" + existingSecretKey: "postgres-password" + + # Database connection details + # Note: For Cloud SQL, use "localhost" (proxy provides local access) + host: "localhost" # OPENTACO_POSTGRES_HOST + port: "5432" # OPENTACO_POSTGRES_PORT + user: "taco" # OPENTACO_POSTGRES_USER + password: "" # OPENTACO_POSTGRES_PASSWORD + database: "taco" # OPENTACO_POSTGRES_DBNAME + # Query backend: "postgres" or other supported types + queryBackend: "postgres" # OPENTACO_QUERY_BACKEND + + # Custom environment variables + customEnv: [] + # - name: MY_CUSTOM_ENV + # value: "my-value" + + # Secret configuration + # Use an existing secret or let the chart create one from values above + secret: + useExistingSecret: false + existingSecretName: "" + + # Resource limits + resources: {} + # requests: + # cpu: 100m + # memory: 256Mi + # limits: + # cpu: 500m + # memory: 512Mi + + # Node selector + nodeSelector: {} + + # Tolerations + tolerations: [] + + # Affinity + affinity: {} + + # Cloud SQL Proxy configuration (for GCP) + cloudSql: + enabled: false + instanceConnectionName: "" + serviceAccount: "default" + credentialsSecret: "" + +# Global configuration (optional) +global: + imagePullSecrets: [] diff --git a/helm-charts/taco-ui/templates/deployment.yaml b/helm-charts/taco-ui/templates/deployment.yaml index 7a7c0f69c..95195c47a 100644 --- a/helm-charts/taco-ui/templates/deployment.yaml +++ b/helm-charts/taco-ui/templates/deployment.yaml @@ -14,10 +14,12 @@ spec: labels: {{- include "taco-ui.selectorLabels" . | nindent 8 }} spec: - {{- with .Values.imagePullSecrets }} + {{- if .Values.global }} + {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- end }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag | default .Chart.AppVersion }}" @@ -26,11 +28,13 @@ spec: - name: http containerPort: {{ .Values.ui.service.port }} protocol: TCP - {{- if .Values.ui.existingSecretName }} envFrom: - secretRef: - name: {{ .Values.ui.existingSecretName }} - {{- end }} + {{- if .Values.ui.secret.useExistingSecret }} + name: {{ .Values.ui.secret.existingSecretName }} + {{- else }} + name: {{ include "taco-ui.fullname" . }}-secret + {{- end }} env: - name: PORT value: "{{ .Values.ui.service.port }}" @@ -42,22 +46,6 @@ spec: - name: ALLOWED_HOSTS value: "{{ .Values.ui.env.allowedHosts }}" {{- end }} - {{- if .Values.ui.env.workos.clientId }} - - name: WORKOS_CLIENT_ID - value: "{{ .Values.ui.env.workos.clientId }}" - {{- end }} - {{- if .Values.ui.env.workos.secretName }} - - name: WORKOS_API_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.ui.env.workos.secretName }} - key: api-key - - name: WORKOS_COOKIE_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.ui.env.workos.secretName }} - key: cookie-password - {{- end }} {{- if .Values.ui.env.posthog.key }} - name: VITE_POSTHOG_KEY value: "{{ .Values.ui.env.posthog.key }}" @@ -66,6 +54,9 @@ spec: - name: VITE_POSTHOG_HOST value: "{{ .Values.ui.env.posthog.host }}" {{- end }} + {{- with .Values.ui.customEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} livenessProbe: httpGet: path: / @@ -98,4 +89,3 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} - diff --git a/helm-charts/taco-ui/templates/secret.yaml b/helm-charts/taco-ui/templates/secret.yaml new file mode 100644 index 000000000..a3b72f986 --- /dev/null +++ b/helm-charts/taco-ui/templates/secret.yaml @@ -0,0 +1,47 @@ +{{- if not .Values.ui.secret.useExistingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "taco-ui.fullname" . }}-secret + labels: + {{- include "taco-ui.labels" . | nindent 4 }} +type: Opaque +stringData: + # WorkOS configuration + {{- if .Values.ui.env.workos.redirectUri }} + WORKOS_REDIRECT_URI: {{ .Values.ui.env.workos.redirectUri | quote }} + {{- end }} + {{- if .Values.ui.env.workos.apiKey }} + WORKOS_API_KEY: {{ .Values.ui.env.workos.apiKey | quote }} + {{- end }} + {{- if .Values.ui.env.workos.clientId }} + WORKOS_CLIENT_ID: {{ .Values.ui.env.workos.clientId | quote }} + {{- end }} + {{- if .Values.ui.env.workos.cookiePassword }} + WORKOS_COOKIE_PASSWORD: {{ .Values.ui.env.workos.cookiePassword | quote }} + {{- end }} + {{- if .Values.ui.env.workos.webhookSecret }} + WORKOS_WEBHOOK_SECRET: {{ .Values.ui.env.workos.webhookSecret | quote }} + {{- end }} + + # Backend service URLs + {{- if .Values.ui.env.backends.orchestratorUrl }} + ORCHESTRATOR_BACKEND_URL: {{ .Values.ui.env.backends.orchestratorUrl | quote }} + {{- end }} + {{- if .Values.ui.env.backends.orchestratorSecret }} + ORCHESTRATOR_BACKEND_SECRET: {{ .Values.ui.env.backends.orchestratorSecret | quote }} + {{- end }} + {{- if .Values.ui.env.backends.driftReportingUrl }} + DRIFT_REPORTING_BACKEND_URL: {{ .Values.ui.env.backends.driftReportingUrl | quote }} + {{- end }} + {{- if .Values.ui.env.backends.driftReportingWebhookSecret }} + DRIFT_REPORTING_BACKEND_WEBHOOK_SECRET: {{ .Values.ui.env.backends.driftReportingWebhookSecret | quote }} + {{- end }} + {{- if .Values.ui.env.backends.statesmanUrl }} + STATESMAN_BACKEND_URL: {{ .Values.ui.env.backends.statesmanUrl | quote }} + {{- end }} + {{- if .Values.ui.env.backends.statesmanWebhookSecret }} + STATESMAN_BACKEND_WEBHOOK_SECRET: {{ .Values.ui.env.backends.statesmanWebhookSecret | quote }} + {{- end }} +{{- end }} + diff --git a/helm-charts/taco-ui/values.yaml b/helm-charts/taco-ui/values.yaml index 355603f91..18ee8bc93 100644 --- a/helm-charts/taco-ui/values.yaml +++ b/helm-charts/taco-ui/values.yaml @@ -1,4 +1,11 @@ # values.yaml +# +# This chart creates environment variables from these values. +# You can either: +# 1. Set values here (chart creates secrets automatically) +# 2. Use secret.useExistingSecret=true and create your own secret +# +# See secrets-example/ui.env for a complete example ui: # Image configuration @@ -16,26 +23,66 @@ ui: # Number of replicas replicaCount: 1 - # Service configuration + # Service configuration + # Creates: PORT (set automatically from port) service: type: ClusterIP - port: 3030 # taco-ui Node.js server listens on port 3030 + port: 3030 # PORT - taco-ui Node.js server listens on port 3030 - # Environment variables + # Environment configuration + # Creates environment variables for the UI service env: - # Backend API URL - apiUrl: "http://taco-statesman:8080" + # Backend API URL (Statesman service) + # Creates: VITE_API_URL + apiUrl: "http://taco-statesman:8080" # VITE_API_URL + # Allowed hosts (comma-separated) - allowedHosts: "" - # WorkOS configuration (optional) + # Creates: ALLOWED_HOSTS + allowedHosts: "" # ALLOWED_HOSTS + + # WorkOS configuration (get from https://dashboard.workos.com/) + # Creates: WORKOS_REDIRECT_URI, WORKOS_API_KEY, WORKOS_CLIENT_ID, + # WORKOS_COOKIE_PASSWORD, WORKOS_WEBHOOK_SECRET workos: - clientId: "" - # Use secretName for sensitive data - secretName: "" + redirectUri: "" # WORKOS_REDIRECT_URI (e.g., https://your-domain.com/api/auth/callback) + apiKey: "" # WORKOS_API_KEY + clientId: "" # WORKOS_CLIENT_ID + cookiePassword: "" # WORKOS_COOKIE_PASSWORD (32 char random string) + webhookSecret: "" # WORKOS_WEBHOOK_SECRET + # PostHog configuration (optional) + # Creates: VITE_POSTHOG_KEY, VITE_POSTHOG_HOST posthog: - key: "" - host: "" + key: "" # VITE_POSTHOG_KEY + host: "" # VITE_POSTHOG_HOST + + # Backend service URLs (for in-cluster communication) + # Creates: ORCHESTRATOR_BACKEND_URL, ORCHESTRATOR_BACKEND_SECRET, + # DRIFT_REPORTING_BACKEND_URL, DRIFT_REPORTING_BACKEND_WEBHOOK_SECRET, + # STATESMAN_BACKEND_URL, STATESMAN_BACKEND_WEBHOOK_SECRET + backends: + # Orchestrator (Digger Backend) + orchestratorUrl: "" # ORCHESTRATOR_BACKEND_URL (e.g., http://opentaco-digger-managed-web:3000) + orchestratorSecret: "" # ORCHESTRATOR_BACKEND_SECRET + + # Drift Reporting + driftReportingUrl: "" # DRIFT_REPORTING_BACKEND_URL (e.g., http://opentaco-drift:3004) + driftReportingWebhookSecret: "" # DRIFT_REPORTING_BACKEND_WEBHOOK_SECRET + + # Statesman + statesmanUrl: "" # STATESMAN_BACKEND_URL (e.g., http://opentaco-statesman:8080) + statesmanWebhookSecret: "" # STATESMAN_BACKEND_WEBHOOK_SECRET + + # Custom environment variables + customEnv: [] + # - name: MY_CUSTOM_ENV + # value: "my-value" + + # Secret configuration + # Use an existing secret or let the chart create one from values above + secret: + useExistingSecret: false + existingSecretName: "" # Ingress configuration ingress: @@ -72,3 +119,6 @@ ui: # Affinity affinity: {} +# Global configuration (optional) +global: + imagePullSecrets: [] From 6b58199cfa64e70ea01f849106a9a0ba422d36fe Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Wed, 22 Oct 2025 14:16:10 -0700 Subject: [PATCH 16/17] update to values style, adjust some envs --- .../digger-drift/templates/deployment.yaml | 30 +--- .../digger-drift/templates/secret.yaml | 43 ++--- helm-charts/digger-drift/values.yaml | 65 ++----- .../templates/backend-deployment.yaml | 2 +- .../digger-managed/templates/secret.yaml | 3 - helm-charts/digger-managed/values.yaml | 15 +- helm-charts/opentaco/Chart.yaml | 16 +- helm-charts/opentaco/values.yaml | 167 ++++++++++-------- .../secrets-example/digger-backend.env | 1 - helm-charts/secrets-example/drift.env | 8 - helm-charts/secrets-example/statesman.env | 19 ++ .../taco-statesman/templates/deployment.yaml | 2 +- .../taco-statesman/templates/secret.yaml | 33 +++- helm-charts/taco-statesman/values.yaml | 50 ++++-- helm-charts/taco-ui/templates/deployment.yaml | 2 +- helm-charts/taco-ui/values.yaml | 22 ++- 16 files changed, 253 insertions(+), 225 deletions(-) diff --git a/helm-charts/digger-drift/templates/deployment.yaml b/helm-charts/digger-drift/templates/deployment.yaml index 2a740013f..4a53587d9 100644 --- a/helm-charts/digger-drift/templates/deployment.yaml +++ b/helm-charts/digger-drift/templates/deployment.yaml @@ -22,7 +22,7 @@ spec: {{- end }} containers: - name: {{ .Chart.Name }} - image: "{{ .Values.drift.image.repository }}:{{ .Values.drift.image.tag | default .Chart.AppVersion }}" + image: "{{ .Values.global.imageRegistry | default "ghcr.io/diggerhq/digger" }}/{{ .Values.drift.image.repository }}:{{ .Values.drift.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.drift.image.pullPolicy | default "IfNotPresent" }} ports: - name: http @@ -40,34 +40,6 @@ spec: value: "{{ .Values.drift.service.port }}" - name: DIGGER_LOG_LEVEL value: "{{ .Values.drift.logLevel }}" - {{- if .Values.drift.sentry.dsn }} - - name: SENTRY_DSN - value: "{{ .Values.drift.sentry.dsn }}" - {{- end }} - # PostgreSQL configuration - - name: POSTGRES_HOST - value: "{{ .Values.drift.postgres.host }}" - - name: POSTGRES_PORT - value: "{{ .Values.drift.postgres.port }}" - - name: POSTGRES_USER - value: "{{ .Values.drift.postgres.user }}" - - name: POSTGRES_DB - value: "{{ .Values.drift.postgres.database }}" - - name: POSTGRES_SSLMODE - value: "{{ .Values.drift.postgres.sslmode }}" - {{- if .Values.drift.postgres.existingSecretName }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.drift.postgres.existingSecretName }} - key: {{ .Values.drift.postgres.existingSecretKey }} - {{- else if .Values.drift.postgres.password }} - - name: POSTGRES_PASSWORD - value: "{{ .Values.drift.postgres.password }}" - {{- end }} - # Build DATABASE_URL from components - - name: DATABASE_URL - value: "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=$(POSTGRES_SSLMODE)" # Custom environment variables {{- range .Values.drift.customEnv }} - name: {{ .name }} diff --git a/helm-charts/digger-drift/templates/secret.yaml b/helm-charts/digger-drift/templates/secret.yaml index 9fd7841fb..f87049cb2 100644 --- a/helm-charts/digger-drift/templates/secret.yaml +++ b/helm-charts/digger-drift/templates/secret.yaml @@ -7,38 +7,33 @@ metadata: {{- include "digger-drift.labels" . | nindent 4 }} type: Opaque stringData: + # Database configuration + {{- if .Values.drift.secret.databaseUrl }} + DATABASE_URL: {{ .Values.drift.secret.databaseUrl | quote }} + {{- end }} + # Drift service configuration - {{- if .Values.drift.config.hostname }} - DIGGER_HOSTNAME: {{ .Values.drift.config.hostname | quote }} + {{- if .Values.drift.secret.diggerAppUrl }} + DIGGER_APP_URL: {{ .Values.drift.secret.diggerAppUrl | quote }} {{- end }} - {{- if .Values.drift.config.webhookSecret }} - DIGGER_WEBHOOK_SECRET: {{ .Values.drift.config.webhookSecret | quote }} + {{- if .Values.drift.secret.diggerDriftReporterHostname }} + DIGGER_DRIFT_REPORTER_HOSTNAME: {{ .Values.drift.secret.diggerDriftReporterHostname | quote }} {{- end }} - {{- if .Values.drift.config.appUrl }} - DIGGER_APP_URL: {{ .Values.drift.config.appUrl | quote }} + {{- if .Values.drift.secret.diggerHostname }} + DIGGER_HOSTNAME: {{ .Values.drift.secret.diggerHostname | quote }} {{- end }} - {{- if .Values.drift.config.driftReporterHostname }} - DIGGER_DRIFT_REPORTER_HOSTNAME: {{ .Values.drift.config.driftReporterHostname | quote }} + {{- if .Values.drift.secret.diggerWebhookSecret }} + DIGGER_WEBHOOK_SECRET: {{ .Values.drift.secret.diggerWebhookSecret | quote }} {{- end }} # GitHub configuration - {{- if .Values.drift.github.org }} - GITHUB_ORG: {{ .Values.drift.github.org | quote }} - {{- end }} - {{- if .Values.drift.github.appID }} - GITHUB_APP_ID: {{ .Values.drift.github.appID | quote }} - {{- end }} - {{- if .Values.drift.github.appClientID }} - GITHUB_APP_CLIENT_ID: {{ .Values.drift.github.appClientID | quote }} + {{- if .Values.drift.secret.githubAppPrivateKeyBase64 }} + GITHUB_APP_PRIVATE_KEY_BASE64: {{ .Values.drift.secret.githubAppPrivateKeyBase64 | quote }} {{- end }} - {{- if .Values.drift.github.appClientSecret }} - GITHUB_APP_CLIENT_SECRET: {{ .Values.drift.github.appClientSecret | quote }} - {{- end }} - {{- if .Values.drift.github.appPrivateKey }} - GITHUB_APP_PRIVATE_KEY_BASE64: {{ .Values.drift.github.appPrivateKey | quote }} - {{- end }} - {{- if .Values.drift.github.webhookSecret }} - GITHUB_WEBHOOK_SECRET: {{ .Values.drift.github.webhookSecret | quote }} + + # Optional: Sentry monitoring + {{- if .Values.drift.secret.sentryDsn }} + SENTRY_DSN: {{ .Values.drift.secret.sentryDsn | quote }} {{- end }} {{- end }} diff --git a/helm-charts/digger-drift/values.yaml b/helm-charts/digger-drift/values.yaml index 5fcd2e4c1..26c0a2a73 100644 --- a/helm-charts/digger-drift/values.yaml +++ b/helm-charts/digger-drift/values.yaml @@ -9,9 +9,10 @@ drift: # Image configuration - # Public image available at ghcr.io/diggerhq/digger/drift + # Note: Full registry path comes from global.imageRegistry + # Public image: ghcr.io/diggerhq/digger/drift image: - repository: ghcr.io/diggerhq/digger/drift + repository: drift tag: "latest" pullPolicy: "IfNotPresent" @@ -32,7 +33,7 @@ drift: # Creates: DIGGER_PORT (set automatically from port) service: type: ClusterIP - port: 3000 # DIGGER_PORT + port: 3004 # DIGGER_PORT # Ingress configuration ingress: @@ -83,54 +84,20 @@ drift: # Affinity affinity: {} - # Drift service configuration - # Creates: DIGGER_HOSTNAME, DIGGER_WEBHOOK_SECRET, DIGGER_APP_URL, - # DIGGER_DRIFT_REPORTER_HOSTNAME - config: - hostname: "" # DIGGER_HOSTNAME (public URL for this drift service) - webhookSecret: "" # DIGGER_WEBHOOK_SECRET (32 char secret for webhook auth) - appUrl: "" # DIGGER_APP_URL (URL for Digger app UI) - driftReporterHostname: "" # DIGGER_DRIFT_REPORTER_HOSTNAME (optional) - - # Sentry configuration (optional) - # Creates: SENTRY_DSN - sentry: - dsn: "" # SENTRY_DSN - - # GitHub App configuration (get from https://github.com/settings/apps) - # Creates: GITHUB_ORG, GITHUB_APP_ID, GITHUB_APP_CLIENT_ID, - # GITHUB_APP_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY_BASE64, GITHUB_WEBHOOK_SECRET - # These are required for drift detection to work with GitHub - github: - org: "" # GITHUB_ORG (GitHub organization name) - appID: "" # GITHUB_APP_ID - appClientID: "" # GITHUB_APP_CLIENT_ID - appClientSecret: "" # GITHUB_APP_CLIENT_SECRET - appPrivateKey: "" # GITHUB_APP_PRIVATE_KEY_BASE64 (base64 encoded private key) - webhookSecret: "" # GITHUB_WEBHOOK_SECRET - - # PostgreSQL configuration - # Creates: POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_DB, - # POSTGRES_SSLMODE, POSTGRES_PASSWORD (if not using existingSecret) - # The drift service uses the same database as the digger backend - postgres: - # Use existingSecret to pull password from an existing secret - existingSecretName: "" - existingSecretKey: "postgres-password" - - # Database connection details - sslmode: "disable" # POSTGRES_SSLMODE - user: "postgres" # POSTGRES_USER - database: "digger" # POSTGRES_DB - host: "postgresql.default.svc.cluster.local" # POSTGRES_HOST - password: "" # POSTGRES_PASSWORD - port: "5432" # POSTGRES_PORT - # Secret configuration - # Use an existing secret or let the chart create one from values above + # For production, create secret externally: kubectl create secret generic drift-secrets --from-env-file=.secrets/drift.env + # For development, set useExistingSecret=false and fill secret fields below secret: - useExistingSecret: false - existingSecretName: "" + useExistingSecret: true + existingSecretName: "drift-secrets" + # Required secret fields (only used if useExistingSecret=false): + databaseUrl: "" # DATABASE_URL - PostgreSQL connection string + diggerAppUrl: "" # DIGGER_APP_URL + diggerDriftReporterHostname: "" # DIGGER_DRIFT_REPORTER_HOSTNAME + diggerHostname: "" # DIGGER_HOSTNAME + diggerWebhookSecret: "" # DIGGER_WEBHOOK_SECRET + githubAppPrivateKeyBase64: "" # GITHUB_APP_PRIVATE_KEY_BASE64 + sentryDsn: "" # SENTRY_DSN (optional) # Global configuration (optional) global: diff --git a/helm-charts/digger-managed/templates/backend-deployment.yaml b/helm-charts/digger-managed/templates/backend-deployment.yaml index e41916e2a..1acbc9e02 100644 --- a/helm-charts/digger-managed/templates/backend-deployment.yaml +++ b/helm-charts/digger-managed/templates/backend-deployment.yaml @@ -23,7 +23,7 @@ spec: {{- end }} containers: - name: web - image: "{{ .Values.digger.image.repository }}:{{ .Values.digger.image.tag }}" + image: "{{ .Values.global.imageRegistry | default "ghcr.io/diggerhq/digger" }}/{{ .Values.digger.image.repository }}:{{ .Values.digger.image.tag }}" imagePullPolicy: {{ .Values.digger.image.pullPolicy | default "IfNotPresent" }} ports: - name: http diff --git a/helm-charts/digger-managed/templates/secret.yaml b/helm-charts/digger-managed/templates/secret.yaml index 5d6aae06b..7ca18e0b5 100644 --- a/helm-charts/digger-managed/templates/secret.yaml +++ b/helm-charts/digger-managed/templates/secret.yaml @@ -37,9 +37,6 @@ stringData: DIGGER_LOAD_PROJECTS_ON_PUSH: "{{ .Values.digger.config.loadProjectsOnPush }}" DIGGER_LOG_LEVEL: {{ .Values.digger.logLevel | quote }} DIGGER_MAX_PROJECTS_PER_CHANGE: "{{ .Values.digger.config.maxProjectsPerChange }}" - {{- if .Values.digger.config.projectsSvcAppName }} - DIGGER_PROJECTS_SVC_APP_NAME: {{ .Values.digger.config.projectsSvcAppName | quote }} - {{- end }} # GitHub App Configuration {{- if .Values.digger.github.appClientId }} diff --git a/helm-charts/digger-managed/values.yaml b/helm-charts/digger-managed/values.yaml index ffc088837..aa11494ef 100644 --- a/helm-charts/digger-managed/values.yaml +++ b/helm-charts/digger-managed/values.yaml @@ -12,8 +12,10 @@ digger: replicaCount: 1 # Image configuration + # Note: Full registry path comes from global.imageRegistry + # Public image: ghcr.io/diggerhq/digger/digger-backend-ee image: - repository: ghcr.io/diggerhq/digger/digger-backend-ee + repository: digger-backend-ee tag: "latest" pullPolicy: IfNotPresent @@ -86,7 +88,7 @@ digger: # Creates: DIGGER_ENABLE_API_ENDPOINTS, DIGGER_ENABLE_INTERNAL_ENDPOINTS, # DIGGER_ENCRYPTION_SECRET, DIGGER_GENERATION_API_TOKEN, DIGGER_GENERATION_ENDPOINT, # DIGGER_INTERNAL_SECRET, DIGGER_LICENSE_KEY, DIGGER_LOAD_PROJECTS_ON_PUSH, - # DIGGER_LOG_LEVEL, DIGGER_MAX_PROJECTS_PER_CHANGE, DIGGER_PROJECTS_SVC_APP_NAME + # DIGGER_LOG_LEVEL, DIGGER_MAX_PROJECTS_PER_CHANGE, config: enableApiEndpoints: true # DIGGER_ENABLE_API_ENDPOINTS enableInternalEndpoints: true # DIGGER_ENABLE_INTERNAL_ENDPOINTS @@ -97,7 +99,7 @@ digger: licenseKey: "" # DIGGER_LICENSE_KEY loadProjectsOnPush: false # DIGGER_LOAD_PROJECTS_ON_PUSH maxProjectsPerChange: 100 # DIGGER_MAX_PROJECTS_PER_CHANGE - projectsSvcAppName: "projects-refresh-service" # DIGGER_PROJECTS_SVC_APP_NAME + # GitHub App configuration (get from https://github.com/settings/apps) # Creates: GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET, GITHUB_APP_ID, @@ -125,10 +127,11 @@ digger: gofips140: "off" # GOFIPS140 # Secret configuration - # Use an existing secret or let the chart create one from values above + # For production, create secret externally: kubectl create secret generic digger-managed-secrets --from-env-file=.secrets/digger-backend.env + # For development, set useExistingSecret=false and fill secret fields below secret: - useExistingSecret: false - existingSecretName: "" + useExistingSecret: true + existingSecretName: "digger-managed-secrets" # Node selector nodeSelector: {} diff --git a/helm-charts/opentaco/Chart.yaml b/helm-charts/opentaco/Chart.yaml index c51d96743..e2ea1d75b 100644 --- a/helm-charts/opentaco/Chart.yaml +++ b/helm-charts/opentaco/Chart.yaml @@ -25,15 +25,22 @@ dependencies: # Digger Managed - terraform orchestration backend - name: digger-managed version: "0.1.0" - repository: "oci://ghcr.io/diggerhq/helm-charts" + # For production: use OCI registry + repository: "oci://ghcr.io/diggerhq/helm-charts" + # For local testing: use file reference + #repository: "file://../digger-managed" condition: digger-managed.enabled tags: - backend # Taco Statesman - IaC state management - name: statesman + alias: taco-statesman version: "0.1.0" + # For production: use OCI registry repository: "oci://ghcr.io/diggerhq/helm-charts" + # For local testing: use file reference + #repository: "file://../taco-statesman" condition: taco-statesman.enabled tags: - backend @@ -41,15 +48,22 @@ dependencies: # Drift Detection - name: drift version: "0.1.0" + # For production: use OCI registry repository: "oci://ghcr.io/diggerhq/helm-charts" + # For local testing: use file reference + #repository: "file://../digger-drift" condition: drift.enabled tags: - backend # Taco UI - React frontend - name: ui + alias: taco-ui version: "0.1.0" + # For production: use OCI registry repository: "oci://ghcr.io/diggerhq/helm-charts" + # For local testing: use file reference + #repository: "file://../taco-ui" condition: taco-ui.enabled tags: - frontend diff --git a/helm-charts/opentaco/values.yaml b/helm-charts/opentaco/values.yaml index 668382e28..87e754ab0 100644 --- a/helm-charts/opentaco/values.yaml +++ b/helm-charts/opentaco/values.yaml @@ -66,57 +66,34 @@ digger-managed: digger: image: - repository: ghcr.io/diggerhq/digger/digger-backend-ee + repository: digger-backend-ee tag: "latest" replicaCount: 1 - # Secret configuration + # Secret management secret: - useExistingSecret: false - existingSecretName: "" - - # Database configuration - postgres: - host: "postgresql.default.svc.cluster.local" - port: "5432" - user: "postgres" - database: "digger" - password: "" - sslmode: "disable" - allowDirty: false - - # Digger configuration - config: - enableApiEndpoints: true - enableInternalEndpoints: true - encryptionSecret: "" - generationApiToken: "" - generationEndpoint: "" - internalSecret: "" - licenseKey: "" - loadProjectsOnPush: false - maxProjectsPerChange: 100 - - # GitHub App configuration - github: - appClientId: "" - appClientSecret: "" - appId: "" - appPrivateKeyBase64: "" - webhookSecret: "" - - hostname: "" - - service: - type: ClusterIP - port: 3000 - + useExistingSecret: true + existingSecretName: "digger-managed-secrets" + + # Resource limits (IMPORTANT: set for production) + resources: {} + # requests: + # memory: 512Mi + # cpu: 500m + # limits: + # memory: 2Gi + # cpu: 2000m + + # Ingress configuration ingress: enabled: false className: "nginx" host: "api.opentaco.example.com" path: / + + # For detailed configuration (database, GitHub App, secrets, etc.), + # see helm-charts/digger-managed/values.yaml or use existingSecret above # ============================================================================ # Taco Statesman Configuration @@ -126,23 +103,41 @@ taco-statesman: taco: image: - repository: ghcr.io/diggerhq/digger/taco-statesman + repository: taco-statesman tag: "latest" replicaCount: 1 - # Database configuration managed via secrets (see statesman-secrets in values-test.yaml.example) - - service: - type: ClusterIP - port: 8080 + # Secret management + secret: + useExistingSecret: true + existingSecretName: "statesman-secrets" - # Storage configuration + # Storage configuration (critical: choose memory or s3) storage: - type: "memory" # or "s3" + type: "memory" # "memory" for dev, "s3" for production s3: - bucket: "" - region: "" + bucket: "" # S3 bucket name (required if type=s3) + region: "us-east-1" + + # Cloud SQL configuration (for GCP) + cloudSql: + enabled: false + instanceConnectionName: "" + serviceAccount: "default" + credentialsSecret: "" + + # Resource limits (IMPORTANT: set for production) + resources: {} + # requests: + # memory: 256Mi + # cpu: 250m + # limits: + # memory: 1Gi + # cpu: 1000m + + # For detailed configuration (auth, JWT, postgres, OAuth, etc.), + # see helm-charts/taco-statesman/values.yaml or use existingSecret above # ============================================================================ # Drift Detection Configuration @@ -152,16 +147,34 @@ drift: drift: image: - repository: ghcr.io/diggerhq/digger/drift + repository: drift tag: "latest" replicaCount: 1 - # Database configuration managed via secrets (see drift-secrets in values-test.yaml.example) + # Secret management + secret: + useExistingSecret: true + existingSecretName: "drift-secrets" + + # Resource limits (IMPORTANT: set for production) + resources: {} + # requests: + # memory: 256Mi + # cpu: 250m + # limits: + # memory: 1Gi + # cpu: 1000m + + # Ingress configuration + ingress: + enabled: false + className: "nginx" + host: "drift.opentaco.example.com" + path: / - service: - type: ClusterIP - port: 3004 + # For detailed configuration (GitHub App, database, webhook secrets, etc.), + # see helm-charts/digger-drift/values.yaml or use existingSecret above # ============================================================================ # Taco UI Configuration @@ -171,34 +184,26 @@ taco-ui: ui: image: - repository: ghcr.io/diggerhq/digger/taco-ui + repository: taco-ui tag: "latest" replicaCount: 1 - service: - type: ClusterIP - port: 3030 + # Secret management + secret: + useExistingSecret: true + existingSecretName: "ui-secrets" - # Environment configuration + # Environment configuration (cross-service URLs) env: - # Backend API URLs (auto-configured to point to services in cluster) - apiUrl: "http://taco-statesman:8080" - orchestratorBackendUrl: "http://digger-managed:3000" - driftReportingBackendUrl: "http://drift:3004" - # Allowed hosts (customize for your domain) allowedHosts: "" - # WorkOS authentication - workos: - clientId: "" # Set this - secretName: "workos-secrets" - - # PostHog analytics (optional) - posthog: - key: "" - host: "" + # Backend service URLs (for server-side API calls) + backends: + orchestratorUrl: "http://digger-managed:3000" + driftReportingUrl: "http://drift:3004" + statesmanUrl: "http://taco-statesman:8080" ingress: enabled: false @@ -214,6 +219,18 @@ taco-ui: - secretName: opentaco-ui-tls hosts: - app.opentaco.example.com + + # Resource limits (IMPORTANT: set for production) + resources: {} + # requests: + # memory: 256Mi + # cpu: 250m + # limits: + # memory: 1Gi + # cpu: 1000m + + # For detailed configuration (WorkOS, PostHog, webhook secrets, etc.), + # see helm-charts/taco-ui/values.yaml or use existingSecret above # ============================================================================ # Additional Configuration diff --git a/helm-charts/secrets-example/digger-backend.env b/helm-charts/secrets-example/digger-backend.env index fbffc0712..7d0781679 100644 --- a/helm-charts/secrets-example/digger-backend.env +++ b/helm-charts/secrets-example/digger-backend.env @@ -17,7 +17,6 @@ DIGGER_LICENSE_KEY=YOUR_LICENSE_KEY DIGGER_LOAD_PROJECTS_ON_PUSH=false DIGGER_LOG_LEVEL=DEBUG DIGGER_MAX_PROJECTS_PER_CHANGE=100 -DIGGER_PROJECTS_SVC_APP_NAME=projects-refresh-service # GitHub App Configuration # Get these from https://github.com/settings/apps/YOUR_APP diff --git a/helm-charts/secrets-example/drift.env b/helm-charts/secrets-example/drift.env index 3c82f5134..df0d3cfac 100644 --- a/helm-charts/secrets-example/drift.env +++ b/helm-charts/secrets-example/drift.env @@ -13,15 +13,7 @@ DIGGER_HOSTNAME=https://app.yourdomain.com # Webhook Secret DIGGER_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET_32_CHARS -# GitHub App Configuration -# Get these from https://github.com/settings/apps/YOUR_APP -# Should be the same credentials as digger-backend -GITHUB_ORG=your-github-org -GITHUB_APP_ID=YOUR_GITHUB_APP_ID -GITHUB_APP_CLIENT_ID=YOUR_GITHUB_APP_CLIENT_ID -GITHUB_APP_CLIENT_SECRET=YOUR_GITHUB_APP_CLIENT_SECRET GITHUB_APP_PRIVATE_KEY_BASE64="YOUR_BASE64_ENCODED_PRIVATE_KEY" -GITHUB_WEBHOOK_SECRET=YOUR_GITHUB_WEBHOOK_SECRET # Optional: Sentry Monitoring SENTRY_DSN=YOUR_SENTRY_DSN diff --git a/helm-charts/secrets-example/statesman.env b/helm-charts/secrets-example/statesman.env index 6b9b9abe0..ef56a1e96 100644 --- a/helm-charts/secrets-example/statesman.env +++ b/helm-charts/secrets-example/statesman.env @@ -23,6 +23,24 @@ AWS_ENDPOINT=https://your-s3-endpoint.com # Internal API Authentication OPENTACO_ENABLE_INTERNAL_ENDPOINTS=YOUR_INTERNAL_SECRET_64_CHARS +# JWT Token Configuration (Optional - uses secure defaults if not set) +# Key ID for JWT signing +OPENTACO_TOKENS_KID=k1 +# Token lifetimes +OPENTACO_TOKENS_ACCESS_TTL=1h +OPENTACO_TOKENS_REFRESH_TTL=720h +# Optional: Path to Ed25519 private key PEM file (generates ephemeral key if not set) +# OPENTACO_TOKENS_PRIVATE_KEY_PEM_PATH=/secrets/jwt-private-key.pem + +# Public Base URL (used as JWT issuer and for OAuth redirects) +# IMPORTANT: Set to your actual public URL in production +OPENTACO_PUBLIC_BASE_URL=https://your-domain.com + +# OAuth State Encryption Key +# CRITICAL: Set to a random 32+ character string in production! +# Used to encrypt OAuth state parameters during PKCE authentication flows +OPENTACO_OAUTH_STATE_KEY=CHANGE_THIS_TO_RANDOM_32_CHAR_STRING_IN_PRODUCTION + # Example CloudSQL + PostgreSQL Configuration # Note: For Cloud SQL, use localhost:5432 (Cloud SQL proxy handles connection) OPENTACO_POSTGRES_HOST=localhost @@ -30,5 +48,6 @@ OPENTACO_POSTGRES_PORT=5432 OPENTACO_POSTGRES_USER=taco OPENTACO_POSTGRES_PASSWORD=YOUR_DB_PASSWORD OPENTACO_POSTGRES_DBNAME=taco +OPENTACO_POSTGRES_SSLMODE=disable OPENTACO_QUERY_BACKEND=postgres diff --git a/helm-charts/taco-statesman/templates/deployment.yaml b/helm-charts/taco-statesman/templates/deployment.yaml index dc668879b..b01476236 100644 --- a/helm-charts/taco-statesman/templates/deployment.yaml +++ b/helm-charts/taco-statesman/templates/deployment.yaml @@ -25,7 +25,7 @@ spec: {{- end }} containers: - name: {{ .Chart.Name }} - image: "{{ .Values.taco.image.repository }}:{{ .Values.taco.image.tag | default .Chart.AppVersion }}" + image: "{{ .Values.global.imageRegistry | default "ghcr.io/diggerhq/digger" }}/{{ .Values.taco.image.repository }}:{{ .Values.taco.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.taco.image.pullPolicy | default "IfNotPresent" }} ports: - name: http diff --git a/helm-charts/taco-statesman/templates/secret.yaml b/helm-charts/taco-statesman/templates/secret.yaml index 2764cfd0a..2f15d751e 100644 --- a/helm-charts/taco-statesman/templates/secret.yaml +++ b/helm-charts/taco-statesman/templates/secret.yaml @@ -48,10 +48,34 @@ stringData: {{- end }} # Internal API configuration - {{- if .Values.taco.enableInternalEndpoints }} + {{- if .Values.taco.internalSecret }} OPENTACO_ENABLE_INTERNAL_ENDPOINTS: {{ .Values.taco.internalSecret | quote }} {{- end }} + # JWT Token configuration + {{- if .Values.taco.tokens.kid }} + OPENTACO_TOKENS_KID: {{ .Values.taco.tokens.kid | quote }} + {{- end }} + {{- if .Values.taco.tokens.accessTTL }} + OPENTACO_TOKENS_ACCESS_TTL: {{ .Values.taco.tokens.accessTTL | quote }} + {{- end }} + {{- if .Values.taco.tokens.refreshTTL }} + OPENTACO_TOKENS_REFRESH_TTL: {{ .Values.taco.tokens.refreshTTL | quote }} + {{- end }} + {{- if .Values.taco.tokens.privateKeyPemPath }} + OPENTACO_TOKENS_PRIVATE_KEY_PEM_PATH: {{ .Values.taco.tokens.privateKeyPemPath | quote }} + {{- end }} + + # Public Base URL + {{- if .Values.taco.publicBaseUrl }} + OPENTACO_PUBLIC_BASE_URL: {{ .Values.taco.publicBaseUrl | quote }} + {{- end }} + + # OAuth State Key + {{- if .Values.taco.oauthStateKey }} + OPENTACO_OAUTH_STATE_KEY: {{ .Values.taco.oauthStateKey | quote }} + {{- end }} + # PostgreSQL configuration {{- if .Values.taco.postgres.host }} OPENTACO_POSTGRES_HOST: {{ .Values.taco.postgres.host | quote }} @@ -65,8 +89,11 @@ stringData: {{- if .Values.taco.postgres.database }} OPENTACO_POSTGRES_DBNAME: {{ .Values.taco.postgres.database | quote }} {{- end }} - {{- if .Values.taco.postgres.queryBackend }} - OPENTACO_QUERY_BACKEND: {{ .Values.taco.postgres.queryBackend | quote }} + {{- if .Values.taco.postgres.sslmode }} + OPENTACO_POSTGRES_SSLMODE: {{ .Values.taco.postgres.sslmode | quote }} + {{- end }} + {{- if .Values.taco.queryBackend }} + OPENTACO_QUERY_BACKEND: {{ .Values.taco.queryBackend | quote }} {{- end }} {{- if and (not .Values.taco.postgres.existingSecretName) .Values.taco.postgres.password }} OPENTACO_POSTGRES_PASSWORD: {{ .Values.taco.postgres.password | quote }} diff --git a/helm-charts/taco-statesman/values.yaml b/helm-charts/taco-statesman/values.yaml index d1da8df80..71156e889 100644 --- a/helm-charts/taco-statesman/values.yaml +++ b/helm-charts/taco-statesman/values.yaml @@ -9,8 +9,10 @@ taco: # Image configuration + # Note: Full registry path comes from global.imageRegistry + # Public image: ghcr.io/diggerhq/digger/taco-statesman image: - repository: ghcr.io/diggerhq/digger/taco-statesman + repository: taco-statesman tag: "latest" pullPolicy: "IfNotPresent" @@ -56,13 +58,39 @@ taco: # Internal API endpoints # Creates: OPENTACO_ENABLE_INTERNAL_ENDPOINTS - # Set to true to enable internal endpoints (requires secret) - enableInternalEndpoints: false - internalSecret: "" # OPENTACO_ENABLE_INTERNAL_ENDPOINTS (64 char secret) + # If set, enables internal endpoints with this secret (64 char secret) + internalSecret: "" # OPENTACO_ENABLE_INTERNAL_ENDPOINTS + - # PostgreSQL configuration + + # JWT Token Configuration + # Creates: OPENTACO_TOKENS_KID, OPENTACO_TOKENS_ACCESS_TTL, OPENTACO_TOKENS_REFRESH_TTL, + # OPENTACO_TOKENS_PRIVATE_KEY_PEM_PATH + tokens: + kid: "k1" # OPENTACO_TOKENS_KID (Key ID for JWT signing) + accessTTL: "1h" # OPENTACO_TOKENS_ACCESS_TTL (access token lifetime) + refreshTTL: "720h" # OPENTACO_TOKENS_REFRESH_TTL (refresh token lifetime, 30 days) + privateKeyPemPath: "" # OPENTACO_TOKENS_PRIVATE_KEY_PEM_PATH (optional, generates ephemeral key if empty) + + # Public Base URL + # Creates: OPENTACO_PUBLIC_BASE_URL + # This is the public URL where OpenTaco is accessible (used as JWT issuer) + publicBaseUrl: "http://localhost:8080" # OPENTACO_PUBLIC_BASE_URL + + # OAuth Configuration + # Creates: OPENTACO_OAUTH_STATE_KEY + # IMPORTANT: Set this to a random 32-character string in production for security! + # Used to encrypt OAuth state parameters during PKCE flow + oauthStateKey: "" # OPENTACO_OAUTH_STATE_KEY (32+ char random string, REQUIRED for production) + + # Query Backend Configuration + # Creates: OPENTACO_QUERY_BACKEND + # Options: "postgres", "mssql", "sqlite", or other supported backends + queryBackend: "postgres" # OPENTACO_QUERY_BACKEND + + # PostgreSQL configuration (only used if queryBackend=postgres) # Creates: OPENTACO_POSTGRES_HOST, OPENTACO_POSTGRES_PORT, OPENTACO_POSTGRES_USER, - # OPENTACO_POSTGRES_PASSWORD, OPENTACO_POSTGRES_DBNAME, OPENTACO_QUERY_BACKEND + # OPENTACO_POSTGRES_PASSWORD, OPENTACO_POSTGRES_DBNAME, OPENTACO_POSTGRES_SSLMODE postgres: # Use existingSecret to pull password from an existing secret existingSecretName: "" @@ -75,8 +103,7 @@ taco: user: "taco" # OPENTACO_POSTGRES_USER password: "" # OPENTACO_POSTGRES_PASSWORD database: "taco" # OPENTACO_POSTGRES_DBNAME - # Query backend: "postgres" or other supported types - queryBackend: "postgres" # OPENTACO_QUERY_BACKEND + sslmode: "disable" # OPENTACO_POSTGRES_SSLMODE (disable, require, verify-ca, verify-full) # Custom environment variables customEnv: [] @@ -84,10 +111,11 @@ taco: # value: "my-value" # Secret configuration - # Use an existing secret or let the chart create one from values above + # For production, create secret externally: kubectl create secret generic statesman-secrets --from-env-file=.secrets/statesman.env + # For development, set useExistingSecret=false and fill secret fields below secret: - useExistingSecret: false - existingSecretName: "" + useExistingSecret: true + existingSecretName: "statesman-secrets" # Resource limits resources: {} diff --git a/helm-charts/taco-ui/templates/deployment.yaml b/helm-charts/taco-ui/templates/deployment.yaml index 95195c47a..3e07b4729 100644 --- a/helm-charts/taco-ui/templates/deployment.yaml +++ b/helm-charts/taco-ui/templates/deployment.yaml @@ -22,7 +22,7 @@ spec: {{- end }} containers: - name: {{ .Chart.Name }} - image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag | default .Chart.AppVersion }}" + image: "{{ .Values.global.imageRegistry | default "ghcr.io/diggerhq/digger" }}/{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.ui.image.pullPolicy | default "IfNotPresent" }} ports: - name: http diff --git a/helm-charts/taco-ui/values.yaml b/helm-charts/taco-ui/values.yaml index 18ee8bc93..aedebb5fe 100644 --- a/helm-charts/taco-ui/values.yaml +++ b/helm-charts/taco-ui/values.yaml @@ -14,9 +14,10 @@ ui: # - Production-optimized Node.js server with static asset serving # - Built for linux/amd64 platform (GCP/GKE compatible) # - # Public image available at ghcr.io/diggerhq/digger/taco-ui + # Note: Full registry path comes from global.imageRegistry + # Public image: ghcr.io/diggerhq/digger/taco-ui image: - repository: ghcr.io/diggerhq/digger/taco-ui + repository: taco-ui tag: "latest" pullPolicy: "IfNotPresent" @@ -32,10 +33,6 @@ ui: # Environment configuration # Creates environment variables for the UI service env: - # Backend API URL (Statesman service) - # Creates: VITE_API_URL - apiUrl: "http://taco-statesman:8080" # VITE_API_URL - # Allowed hosts (comma-separated) # Creates: ALLOWED_HOSTS allowedHosts: "" # ALLOWED_HOSTS @@ -62,15 +59,15 @@ ui: # STATESMAN_BACKEND_URL, STATESMAN_BACKEND_WEBHOOK_SECRET backends: # Orchestrator (Digger Backend) - orchestratorUrl: "" # ORCHESTRATOR_BACKEND_URL (e.g., http://opentaco-digger-managed-web:3000) + orchestratorUrl: "" # ORCHESTRATOR_BACKEND_URL (e.g., http://opentaco-digger-managed-web:3000) -> the orchestrator is the digger-managed service orchestratorSecret: "" # ORCHESTRATOR_BACKEND_SECRET # Drift Reporting - driftReportingUrl: "" # DRIFT_REPORTING_BACKEND_URL (e.g., http://opentaco-drift:3004) + driftReportingUrl: "" # DRIFT_REPORTING_BACKEND_URL (e.g., http://opentaco-drift:3004) -> the drift service driftReportingWebhookSecret: "" # DRIFT_REPORTING_BACKEND_WEBHOOK_SECRET # Statesman - statesmanUrl: "" # STATESMAN_BACKEND_URL (e.g., http://opentaco-statesman:8080) + statesmanUrl: "" # STATESMAN_BACKEND_URL (e.g., http://opentaco-statesman:8080) -> the statesman service statesmanWebhookSecret: "" # STATESMAN_BACKEND_WEBHOOK_SECRET # Custom environment variables @@ -79,10 +76,11 @@ ui: # value: "my-value" # Secret configuration - # Use an existing secret or let the chart create one from values above + # For production, create secret externally: kubectl create secret generic ui-secrets --from-env-file=.secrets/ui.env + # For development, set useExistingSecret=false and fill secret fields below secret: - useExistingSecret: false - existingSecretName: "" + useExistingSecret: true + existingSecretName: "ui-secrets" # Ingress configuration ingress: From cdcaf7b37d6040dad87aecab035ce59549547d4d Mon Sep 17 00:00:00 2001 From: motatoes Date: Tue, 28 Oct 2025 09:17:08 -0700 Subject: [PATCH 17/17] rename digger-drift to taco-drift, rename digger-managed to taco-orchestrator --- .github/workflows/helm-release.yml | 2 +- go.work.sum | 3 +++ helm-charts/opentaco/Chart.yaml | 8 ++++---- helm-charts/opentaco/templates/NOTES.txt | 2 +- helm-charts/opentaco/values.yaml | 6 +++--- helm-charts/{digger-drift => taco-drift}/.helmignore | 0 helm-charts/{digger-drift => taco-drift}/Chart.yaml | 0 .../{digger-drift => taco-drift}/templates/_helpers.tpl | 0 .../templates/deployment.yaml | 0 .../{digger-drift => taco-drift}/templates/ingress.yaml | 0 .../{digger-drift => taco-drift}/templates/secret.yaml | 0 .../{digger-drift => taco-drift}/templates/service.yaml | 0 helm-charts/{digger-drift => taco-drift}/values.yaml | 2 +- .../{digger-managed => taco-orchestrator}/.helmignore | 0 .../{digger-managed => taco-orchestrator}/Chart.yaml | 0 .../templates/_helpers.tpl | 0 .../templates/backend-deployment.yaml | 0 .../templates/backend-ingress.yaml | 0 .../templates/backend-service.yaml | 0 .../templates/postgres-secret.yaml | 0 .../templates/secret.yaml | 0 .../tests/deployments_test.yaml | 0 .../{digger-managed => taco-orchestrator}/values.yaml | 4 ++-- helm-charts/taco-ui/values.yaml | 2 +- 24 files changed, 16 insertions(+), 13 deletions(-) rename helm-charts/{digger-drift => taco-drift}/.helmignore (100%) rename helm-charts/{digger-drift => taco-drift}/Chart.yaml (100%) rename helm-charts/{digger-drift => taco-drift}/templates/_helpers.tpl (100%) rename helm-charts/{digger-drift => taco-drift}/templates/deployment.yaml (100%) rename helm-charts/{digger-drift => taco-drift}/templates/ingress.yaml (100%) rename helm-charts/{digger-drift => taco-drift}/templates/secret.yaml (100%) rename helm-charts/{digger-drift => taco-drift}/templates/service.yaml (100%) rename helm-charts/{digger-drift => taco-drift}/values.yaml (98%) rename helm-charts/{digger-managed => taco-orchestrator}/.helmignore (100%) rename helm-charts/{digger-managed => taco-orchestrator}/Chart.yaml (100%) rename helm-charts/{digger-managed => taco-orchestrator}/templates/_helpers.tpl (100%) rename helm-charts/{digger-managed => taco-orchestrator}/templates/backend-deployment.yaml (100%) rename helm-charts/{digger-managed => taco-orchestrator}/templates/backend-ingress.yaml (100%) rename helm-charts/{digger-managed => taco-orchestrator}/templates/backend-service.yaml (100%) rename helm-charts/{digger-managed => taco-orchestrator}/templates/postgres-secret.yaml (100%) rename helm-charts/{digger-managed => taco-orchestrator}/templates/secret.yaml (100%) rename helm-charts/{digger-managed => taco-orchestrator}/tests/deployments_test.yaml (100%) rename helm-charts/{digger-managed => taco-orchestrator}/values.yaml (97%) diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml index d857a3fce..793f047f2 100644 --- a/.github/workflows/helm-release.yml +++ b/.github/workflows/helm-release.yml @@ -18,7 +18,7 @@ jobs: matrix: chart: - digger-backend - - digger-drift + - taco-drift - taco-statesman - taco-ui - opentaco diff --git a/go.work.sum b/go.work.sum index 6d7b134a1..67c9e0cac 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,5 +1,6 @@ ariga.io/atlas v0.14.3-0.20231010104048-0c071bfc9161 h1:xZS2wAf1AzRNA/8iD2LTAXtIZuIDYDXsZRlYBAyBu0A= ariga.io/atlas v0.14.3-0.20231010104048-0c071bfc9161/go.mod h1:isZrlzJ5cpoCoKFoY9knZug7Lq4pP1cm8g3XciLZ0Pw= +ariga.io/atlas v0.32.0 h1:y+77nueMrExLiKlz1CcPKh/nU7VSlWfBbwCShsJyvCw= ariga.io/atlas v0.32.0/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w= ariga.io/atlas-go-sdk v0.7.2/go.mod h1:cFq7bnvHgKTWHCsU46mtkGxdl41rx2o7SjaLoh6cO8M= cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= @@ -1050,6 +1051,7 @@ github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iauee github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1 h1:wSt/4CYxs70xbATrGXhokKF1i0tZjENLOo1ioIO13zk= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9 h1:tF+augKRWlWx0J0B7ZyyKSiTyV6E1zZe+7b3qQlcEf8= @@ -1723,6 +1725,7 @@ golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b h1:DU+gwOBXU+6bO0sEyO7o/NeMlxZxCZEvI7v+J4a1zRQ= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= diff --git a/helm-charts/opentaco/Chart.yaml b/helm-charts/opentaco/Chart.yaml index e2ea1d75b..c0d189884 100644 --- a/helm-charts/opentaco/Chart.yaml +++ b/helm-charts/opentaco/Chart.yaml @@ -23,13 +23,13 @@ dependencies: - database # Digger Managed - terraform orchestration backend - - name: digger-managed + - name: taco-orchestrator version: "0.1.0" # For production: use OCI registry repository: "oci://ghcr.io/diggerhq/helm-charts" # For local testing: use file reference - #repository: "file://../digger-managed" - condition: digger-managed.enabled + #repository: "file://../taco-orchestrator" + condition: taco-orchestrator.enabled tags: - backend @@ -51,7 +51,7 @@ dependencies: # For production: use OCI registry repository: "oci://ghcr.io/diggerhq/helm-charts" # For local testing: use file reference - #repository: "file://../digger-drift" + #repository: "file://../taco-drift" condition: drift.enabled tags: - backend diff --git a/helm-charts/opentaco/templates/NOTES.txt b/helm-charts/opentaco/templates/NOTES.txt index b0d39b71a..aceb38db8 100644 --- a/helm-charts/opentaco/templates/NOTES.txt +++ b/helm-charts/opentaco/templates/NOTES.txt @@ -20,7 +20,7 @@ DEPLOYMENT STATUS: Please enable postgresql or cloudSql in values.yaml {{- end }} -{{- if index .Values "digger-managed" "enabled" }} +{{- if index .Values "taco-orchestrator" "enabled" }} ✓ Digger Managed: Enabled Service: digger-managed:3000 {{- end }} diff --git a/helm-charts/opentaco/values.yaml b/helm-charts/opentaco/values.yaml index 87e754ab0..cd06566c2 100644 --- a/helm-charts/opentaco/values.yaml +++ b/helm-charts/opentaco/values.yaml @@ -74,7 +74,7 @@ digger-managed: # Secret management secret: useExistingSecret: true - existingSecretName: "digger-managed-secrets" + existingSecretName: "taco-orchestrator-secrets" # Resource limits (IMPORTANT: set for production) resources: {} @@ -93,7 +93,7 @@ digger-managed: path: / # For detailed configuration (database, GitHub App, secrets, etc.), - # see helm-charts/digger-managed/values.yaml or use existingSecret above + # see helm-charts/taco-orchestrator/values.yaml or use existingSecret above # ============================================================================ # Taco Statesman Configuration @@ -174,7 +174,7 @@ drift: path: / # For detailed configuration (GitHub App, database, webhook secrets, etc.), - # see helm-charts/digger-drift/values.yaml or use existingSecret above + # see helm-charts/taco-drift/values.yaml or use existingSecret above # ============================================================================ # Taco UI Configuration diff --git a/helm-charts/digger-drift/.helmignore b/helm-charts/taco-drift/.helmignore similarity index 100% rename from helm-charts/digger-drift/.helmignore rename to helm-charts/taco-drift/.helmignore diff --git a/helm-charts/digger-drift/Chart.yaml b/helm-charts/taco-drift/Chart.yaml similarity index 100% rename from helm-charts/digger-drift/Chart.yaml rename to helm-charts/taco-drift/Chart.yaml diff --git a/helm-charts/digger-drift/templates/_helpers.tpl b/helm-charts/taco-drift/templates/_helpers.tpl similarity index 100% rename from helm-charts/digger-drift/templates/_helpers.tpl rename to helm-charts/taco-drift/templates/_helpers.tpl diff --git a/helm-charts/digger-drift/templates/deployment.yaml b/helm-charts/taco-drift/templates/deployment.yaml similarity index 100% rename from helm-charts/digger-drift/templates/deployment.yaml rename to helm-charts/taco-drift/templates/deployment.yaml diff --git a/helm-charts/digger-drift/templates/ingress.yaml b/helm-charts/taco-drift/templates/ingress.yaml similarity index 100% rename from helm-charts/digger-drift/templates/ingress.yaml rename to helm-charts/taco-drift/templates/ingress.yaml diff --git a/helm-charts/digger-drift/templates/secret.yaml b/helm-charts/taco-drift/templates/secret.yaml similarity index 100% rename from helm-charts/digger-drift/templates/secret.yaml rename to helm-charts/taco-drift/templates/secret.yaml diff --git a/helm-charts/digger-drift/templates/service.yaml b/helm-charts/taco-drift/templates/service.yaml similarity index 100% rename from helm-charts/digger-drift/templates/service.yaml rename to helm-charts/taco-drift/templates/service.yaml diff --git a/helm-charts/digger-drift/values.yaml b/helm-charts/taco-drift/values.yaml similarity index 98% rename from helm-charts/digger-drift/values.yaml rename to helm-charts/taco-drift/values.yaml index 26c0a2a73..cbeebb717 100644 --- a/helm-charts/digger-drift/values.yaml +++ b/helm-charts/taco-drift/values.yaml @@ -44,7 +44,7 @@ drift: host: "drift.example.com" path: / tls: - secretName: "digger-drift-tls" + secretName: "taco-drift-tls" # Liveness and startup probe settings livenessProbe: diff --git a/helm-charts/digger-managed/.helmignore b/helm-charts/taco-orchestrator/.helmignore similarity index 100% rename from helm-charts/digger-managed/.helmignore rename to helm-charts/taco-orchestrator/.helmignore diff --git a/helm-charts/digger-managed/Chart.yaml b/helm-charts/taco-orchestrator/Chart.yaml similarity index 100% rename from helm-charts/digger-managed/Chart.yaml rename to helm-charts/taco-orchestrator/Chart.yaml diff --git a/helm-charts/digger-managed/templates/_helpers.tpl b/helm-charts/taco-orchestrator/templates/_helpers.tpl similarity index 100% rename from helm-charts/digger-managed/templates/_helpers.tpl rename to helm-charts/taco-orchestrator/templates/_helpers.tpl diff --git a/helm-charts/digger-managed/templates/backend-deployment.yaml b/helm-charts/taco-orchestrator/templates/backend-deployment.yaml similarity index 100% rename from helm-charts/digger-managed/templates/backend-deployment.yaml rename to helm-charts/taco-orchestrator/templates/backend-deployment.yaml diff --git a/helm-charts/digger-managed/templates/backend-ingress.yaml b/helm-charts/taco-orchestrator/templates/backend-ingress.yaml similarity index 100% rename from helm-charts/digger-managed/templates/backend-ingress.yaml rename to helm-charts/taco-orchestrator/templates/backend-ingress.yaml diff --git a/helm-charts/digger-managed/templates/backend-service.yaml b/helm-charts/taco-orchestrator/templates/backend-service.yaml similarity index 100% rename from helm-charts/digger-managed/templates/backend-service.yaml rename to helm-charts/taco-orchestrator/templates/backend-service.yaml diff --git a/helm-charts/digger-managed/templates/postgres-secret.yaml b/helm-charts/taco-orchestrator/templates/postgres-secret.yaml similarity index 100% rename from helm-charts/digger-managed/templates/postgres-secret.yaml rename to helm-charts/taco-orchestrator/templates/postgres-secret.yaml diff --git a/helm-charts/digger-managed/templates/secret.yaml b/helm-charts/taco-orchestrator/templates/secret.yaml similarity index 100% rename from helm-charts/digger-managed/templates/secret.yaml rename to helm-charts/taco-orchestrator/templates/secret.yaml diff --git a/helm-charts/digger-managed/tests/deployments_test.yaml b/helm-charts/taco-orchestrator/tests/deployments_test.yaml similarity index 100% rename from helm-charts/digger-managed/tests/deployments_test.yaml rename to helm-charts/taco-orchestrator/tests/deployments_test.yaml diff --git a/helm-charts/digger-managed/values.yaml b/helm-charts/taco-orchestrator/values.yaml similarity index 97% rename from helm-charts/digger-managed/values.yaml rename to helm-charts/taco-orchestrator/values.yaml index aa11494ef..7eff08a2b 100644 --- a/helm-charts/digger-managed/values.yaml +++ b/helm-charts/taco-orchestrator/values.yaml @@ -127,11 +127,11 @@ digger: gofips140: "off" # GOFIPS140 # Secret configuration - # For production, create secret externally: kubectl create secret generic digger-managed-secrets --from-env-file=.secrets/digger-backend.env + # For production, create secret externally: kubectl create secret generic taco-orchestrator-secrets --from-env-file=.secrets/digger-backend.env # For development, set useExistingSecret=false and fill secret fields below secret: useExistingSecret: true - existingSecretName: "digger-managed-secrets" + existingSecretName: "taco-orchestrator-secrets" # Node selector nodeSelector: {} diff --git a/helm-charts/taco-ui/values.yaml b/helm-charts/taco-ui/values.yaml index aedebb5fe..ec315660b 100644 --- a/helm-charts/taco-ui/values.yaml +++ b/helm-charts/taco-ui/values.yaml @@ -59,7 +59,7 @@ ui: # STATESMAN_BACKEND_URL, STATESMAN_BACKEND_WEBHOOK_SECRET backends: # Orchestrator (Digger Backend) - orchestratorUrl: "" # ORCHESTRATOR_BACKEND_URL (e.g., http://opentaco-digger-managed-web:3000) -> the orchestrator is the digger-managed service + orchestratorUrl: "" # ORCHESTRATOR_BACKEND_URL (e.g., http://opentaco-digger-managed-web:3000) -> the orchestrator is the taco-orchestrator service orchestratorSecret: "" # ORCHESTRATOR_BACKEND_SECRET # Drift Reporting