PSA/helm/templates/deployment.yaml
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

1019 lines
46 KiB
YAML

{{- if and (not .Values.devEnv.enabled) (not (include "sebastian.hostedEnvEnabled" .)) }}
{{- $waitForBootstrapImageName := default .Values.setup.image.name .Values.setup.waitForBootstrap.image.name }}
{{- $waitForBootstrapImageTag := default .Values.setup.image.tag .Values.setup.waitForBootstrap.image.tag }}
{{- $waitForBootstrapPullPolicy := default .Values.setup.pullPolicy .Values.setup.waitForBootstrap.pullPolicy }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sebastian.fullname" . }}
namespace: {{ include "sebastian.namespace" . }}
labels:
{{- include "sebastian.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.server.replicaCount }}
{{- if .Values.server.progressDeadlineSeconds }}
progressDeadlineSeconds: {{ .Values.server.progressDeadlineSeconds }}
{{- end }}
{{- if .Values.server.hostNetwork }}
strategy:
type: Recreate
{{- else }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
{{- end }}
selector:
matchLabels:
{{- include "sebastian.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.vaultAgent.enabled }}
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "{{ .Values.vaultAgent.role }}"
vault.hashicorp.com/agent-inject-secret-alga_auth_key: "{{ .Values.vaultAgent.sharedSecretPath }}"
vault.hashicorp.com/agent-inject-template-alga_auth_key: |
{{`{{- with secret "`}}{{ .Values.vaultAgent.sharedSecretPath }}{{`" -}}
{{- .Data.data.alga_auth_key -}}
{{- end }}`}}
vault.hashicorp.com/agent-inject-secret-server-secrets: "{{ .Values.vaultAgent.secretPath }}"
vault.hashicorp.com/agent-inject-template-server-secrets: |
{{`{{- with secret "`}}{{ .Values.vaultAgent.secretPath }}{{`" -}}
{{- range $k, $v := .Data.data -}}
export {{ $k }}="{{ $v }}"
{{- end -}}
{{- end }}`}}
{{- if .Values.vaultAgent.gcpServiceAccount.secretPath }}
vault.hashicorp.com/agent-inject-secret-google-application-credentials: "{{ .Values.vaultAgent.gcpServiceAccount.secretPath }}"
vault.hashicorp.com/agent-inject-file-google-application-credentials: "{{ .Values.vaultAgent.gcpServiceAccount.fileName }}"
vault.hashicorp.com/agent-inject-template-google-application-credentials: |
{{`{{- with secret "`}}{{ .Values.vaultAgent.gcpServiceAccount.secretPath }}{{`" -}}
{{- index .Data.data "`}}{{ .Values.vaultAgent.gcpServiceAccount.secretKey }}{{`" -}}
{{- end }}`}}
{{- end }}
# Fix for Vault + Istio compatibility - exclude Vault traffic from Istio proxy
{{- with .Values.istio.sidecar.excludeOutboundPorts }}
traffic.sidecar.istio.io/excludeOutboundPorts: "{{ join "," . }}"
{{- end }}
{{- with .Values.istio.sidecar.excludeOutboundIPRanges }}
traffic.sidecar.istio.io/excludeOutboundIPRanges: "{{ . }}"
{{- end }}
{{- with .Values.istio.sidecar.includeOutboundIPRanges }}
traffic.sidecar.istio.io/includeOutboundIPRanges: "{{ . }}"
{{- end }}
{{- end }}
labels:
{{- include "sebastian.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: "main"
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- if .Values.server.hostNetwork }}
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
{{- end }}
{{- if or .Values.server.image.is_private .Values.setup.image.is_private }}
imagePullSecrets:
{{- if .Values.server.image.is_private }}
- name: "{{ .Values.server.image.credentials }}"
{{- end }}
{{- if and .Values.setup.image.is_private (ne .Values.setup.image.credentials .Values.server.image.credentials) }}
- name: "{{ .Values.setup.image.credentials }}"
{{- end }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
volumes:
{{- /* Only mount storage volume when using local storage (S3 not enabled) */}}
{{- if and .Values.server.persistence.enabled (not .Values.config.storage.providers.s3.enabled) }}
- name: storage-volume
{{- if .Values.server.persistence.existingClaim }}
persistentVolumeClaim:
claimName: {{ .Values.server.persistence.existingClaim }}
{{- else }}
persistentVolumeClaim:
claimName: {{ include "sebastian.fullname" . }}-storage
{{- end }}
{{- end }}
{{- if or .Values.setup.runMigrations .Values.setup.runSeeds }}
initContainers:
- name: wait-for-bootstrap
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ $waitForBootstrapImageName }}:{{ $waitForBootstrapImageTag }}"
imagePullPolicy: {{ $waitForBootstrapPullPolicy }}
command: ["/bin/sh", "-ec"]
args:
- |
if [ "${SETUP_RUN_SEEDS}" = "true" ]; then
if [ "${APPLIANCE_BOOTSTRAP_ENABLED}" = "true" ]; then
echo "Waiting for appliance bootstrap migrations to finish..."
until PGPASSWORD="${DB_PASSWORD_ADMIN}" psql -h "${DB_HOST_ADMIN}" -p "${DB_PORT_ADMIN}" -U "${DB_USER_ADMIN}" -d "${DB_NAME_SERVER}" -Atqc "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users');" 2>/dev/null | grep -q '^t$'; do
sleep 2
done
else
echo "Waiting for bootstrap seeds to create initial data..."
until PGPASSWORD="${DB_PASSWORD_ADMIN}" psql -h "${DB_HOST_ADMIN}" -p "${DB_PORT_ADMIN}" -U "${DB_USER_ADMIN}" -d "${DB_NAME_SERVER}" -Atqc "SELECT EXISTS (SELECT 1 FROM users LIMIT 1);" 2>/dev/null | grep -q '^t$'; do
sleep 2
done
fi
elif [ "${SETUP_RUN_MIGRATIONS}" = "true" ]; then
echo "Waiting for bootstrap job to create application databases..."
until PGPASSWORD="${DB_PASSWORD_ADMIN}" psql -h "${DB_HOST_ADMIN}" -p "${DB_PORT_ADMIN}" -U "${DB_USER_ADMIN}" -d postgres -Atqc "SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname='${DB_NAME_SERVER}');" 2>/dev/null | grep -q '^t$'; do
sleep 2
done
fi
env:
- name: SETUP_RUN_MIGRATIONS
value: {{ ternary "true" "false" .Values.setup.runMigrations | quote }}
- name: SETUP_RUN_SEEDS
value: {{ ternary "true" "false" .Values.setup.runSeeds | quote }}
- name: DB_NAME_SERVER
value: "server"
- name: APPLIANCE_BOOTSTRAP_ENABLED
value: {{ ternary "true" "false" .Values.setup.applianceBootstrap.enabled | quote }}
- name: DB_USER_ADMIN
value: "postgres"
{{- if .Values.db.enabled }}
- name: DB_HOST_ADMIN
value: "db.{{ include "sebastian.namespace" . }}.svc.cluster.local"
- name: DB_PORT_ADMIN
value: "5432"
- name: DB_PASSWORD_ADMIN
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_SUPERUSER
{{- else }}
- name: DB_HOST_ADMIN
value: "{{ .Values.config.db.adminHost | default .Values.config.db.host }}"
- name: DB_PORT_ADMIN
value: "{{ .Values.config.db.adminPort | default .Values.config.db.port }}"
- name: DB_PASSWORD_ADMIN
{{- if and .Values.config.db.server_password_admin_secret.name .Values.config.db.server_password_admin_secret.key }}
valueFrom:
secretKeyRef:
name: {{ .Values.config.db.server_password_admin_secret.name | quote }}
key: {{ .Values.config.db.server_password_admin_secret.key | quote }}
{{- else }}
value: {{ .Values.config.db.server_admin_password | quote }}
{{- end }}
{{- end }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.server.image.name }}:{{ .Values.server.image.tag }}"
command: ["./entrypoint.sh"]
imagePullPolicy: {{ .Values.server.pullPolicy }}
env:
# ----------- EDITION ----------------
- name: NEXT_PUBLIC_EDITION
value: "{{ .Values.edition | default "community" }}"
- name: EDITION
value: "{{ .Values.edition | default "community" }}"
# ----------- APP ----------------
- name: VERSION
value: "{{ .Values.version }}"
- name: APP_VERSION
value: "{{ .Values.version }}"
- name: NEXT_PUBLIC_APP_VERSION
value: "{{ .Values.version }}"
- name: APP_BUILD_SHA
value: "{{ (default dict .Values.build).sha | default .Values.server.image.tag }}"
- name: NEXT_PUBLIC_APP_BUILD_SHA
value: "{{ (default dict .Values.build).sha | default .Values.server.image.tag }}"
- name: APP_NAME
value: "{{ .Values.nameOverride }}"
- name: APP_ENV
value: "{{ .Values.env }}"
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: HOST
value: "{{ include "sebastian.resolveHost" . }}"
- name: VERIFY_EMAIL_ENABLED
value: "{{ .Values.server.verify_email}}"
- name: SERVER_ACTIONS_BODY_LIMIT
value: "{{ .Values.server.serverActionsBodyLimit | default "200mb" }}"
- name: SEARCH_INDEX_LIVE
value: {{ ternary "true" "false" (.Values.server.searchIndexLive | default false) | quote }}
- name: RATE_LIMIT_ENFORCE
value: "{{ (default dict .Values.rateLimit).enforce | default "false" }}"
{{- if and .Values.vaultAgent.enabled .Values.vaultAgent.gcpServiceAccount.secretPath }}
- name: GOOGLE_APPLICATION_CREDENTIALS
value: "/vault/secrets/{{ .Values.vaultAgent.gcpServiceAccount.fileName }}"
{{- end }}
{{- with .Values.chatProvider }}
{{- if .aiChatProvider }}
- name: AI_CHAT_PROVIDER
value: {{ .aiChatProvider | quote }}
{{- end }}
{{- if .vertexProjectId }}
- name: VERTEX_PROJECT_ID
value: {{ .vertexProjectId | quote }}
{{- end }}
{{- if .vertexLocation }}
- name: VERTEX_LOCATION
value: {{ .vertexLocation | quote }}
{{- end }}
{{- if .vertexChatModel }}
- name: VERTEX_CHAT_MODEL
value: {{ .vertexChatModel | quote }}
{{- end }}
{{- if .vertexOpenapiBaseUrl }}
- name: VERTEX_OPENAPI_BASE_URL
value: {{ .vertexOpenapiBaseUrl | quote }}
{{- end }}
{{- end }}
# ----------- AI Document Assist ----------------
- name: AI_DOCUMENT_API_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: AI_DOCUMENT_API_KEY
# ----------- Collaborative Doc Persistence ----------------
- name: COLLAB_PERSIST_API_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: COLLAB_PERSIST_API_KEY
# ----------- HOCUSPOCUS JWT ----------------
- name: HOCUSPOCUS_JWT_SECRET
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: HOCUSPOCUS_JWT_SECRET
# ----------- TEMPORAL ----------------
- name: TEMPORAL_ADDRESS
value: "{{ .Values.temporal.address }}"
- name: TEMPORAL_NAMESPACE
value: "{{ .Values.temporal.namespace }}"
- name: TEMPORAL_PORTAL_DOMAIN_TASK_QUEUE
value: "{{ .Values.temporal.portalDomainTaskQueue }}"
# ----------- REDIS ----------------
{{- if .Values.redis.enabled }}
- name: REDIS_HOST
value: "redis.{{ include "sebastian.namespace" . }}.svc.cluster.local"
- name: REDIS_PORT
value: "6379"
- name: REDIS_DB
value: "0"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
{{- else }}
- name: REDIS_HOST
value: "{{ .Values.config.redis.host }}.{{ include "sebastian.namespace" . }}.svc.cluster.local"
- name: REDIS_PORT
value: "{{ .Values.config.redis.port }}"
- name: REDIS_DB
value: "{{ .Values.config.redis.db }}"
- name: REDIS_PASSWORD
{{- if and .Values.config.redis.passwordSecret.name .Values.config.redis.passwordSecret.key }}
valueFrom:
secretKeyRef:
name: {{ .Values.config.redis.passwordSecret.name | quote }}
key: {{ .Values.config.redis.passwordSecret.key | quote }}
{{- else }}
value: {{ .Values.config.redis.password | quote }}
{{- end }}
{{- end }}
# ----------- OBSERVABILITY (OpenTelemetry) ----------------
# When enabled, the app's OTEL SDK (http+pg+redis instrumentation) exports
# traces to OTLP_ENDPOINT. In production this points at the Alloy collector,
# which fans traces out to Tempo (storage) and the metrics generator
# (traces_spanmetrics_* / traces_service_graph_*). See nm-kube-config/alloy
# and nm-kube-config/tempo-metrics-generator.
{{- if (default dict .Values.observability).enabled }}
- name: ALGA_OBSERVABILITY
value: "true"
- name: OTLP_ENDPOINT
value: "{{ .Values.observability.otlpEndpoint }}"
{{- with (default dict .Values.observability).deploymentId }}
- name: DEPLOYMENT_ID
value: "{{ . }}"
{{- end }}
{{- end }}
# ----------- HOCUSPOCUS ----------------
{{- if .Values.hocuspocus.enabled }}
- name: REQUIRE_HOCUSPOCUS
value: "true"
- name: HOCUSPOCUS_HOST
value: "hocuspocus.{{ include "sebastian.namespace" . }}.svc.cluster.local"
- name: HOCUSPOCUS_PORT
value: "{{ .Values.hocuspocus.service.port | default 1234 }}"
- name: NEXT_PUBLIC_HOCUSPOCUS_URL
value: "wss://{{ include "sebastian.resolveHost" . }}/hocuspocus"
{{- end }}
# ----------- DB ----------------
{{- if .Values.db.enabled }}
- name: DB_TYPE
value: "postgres"
- name: DB_HOST
value: "{{ include "sebastian.runtimeDbHost" . }}"
- name: DB_USER_SERVER
value: "app_user"
- name: DB_PORT
value: "{{ include "sebastian.runtimeDbPort" . }}"
- name: DB_NAME_SERVER
value: "server"
- name: DB_USER_ADMIN
value: "postgres"
- name: DB_PASSWORD_ADMIN
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_SUPERUSER
- name: DB_PASSWORD_SUPERUSER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_SUPERUSER
- name: DB_PASSWORD_SERVER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_SERVER
{{- else }}
- name: DB_TYPE
value: "postgres"
- name: DB_HOST
value: "{{ .Values.config.db.host }}"
- name: DB_USER_SERVER
value: "{{ .Values.config.db.user }}"
- name: DB_USER_ADMIN
value: "postgres"
- name: DB_PORT
value: "{{ .Values.config.db.port }}"
- name: DB_NAME_SERVER
value: "{{ .Values.config.db.server_database }}"
- name: DB_PASSWORD_ADMIN
{{- with .Values.config.db.server_password_admin_secret }}
valueFrom:
secretKeyRef:
name: {{ .name | quote }}
key: {{ .key | quote }}
{{- else }}
value: {{ .Values.config.db.server_admin_password | quote }}
{{- end }}
- name: DB_PASSWORD_SERVER
{{- with .Values.config.db.server_password_secret }}
valueFrom:
secretKeyRef:
name: {{ .name | quote }}
key: {{ .key | quote }}
{{- else }}
value: {{ .Values.config.db.server_password | quote }}
{{- end }}
{{- end }}
# ----------- STORAGE ----------------
# Local Storage Provider (Community Edition)
{{- if not .Values.config.storage.providers.s3.enabled }}
- name: STORAGE_DEFAULT_PROVIDER
value: "local"
- name: STORAGE_LOCAL_BASE_PATH
value: "{{ .Values.config.storage.providers.local.base_path | default "/data/files" }}"
- name: STORAGE_LOCAL_MAX_FILE_SIZE
value: "{{ .Values.config.storage.providers.local.max_file_size | default 104857600 }}"
- name: STORAGE_LOCAL_ALLOWED_MIME_TYPES
value: "{{ join "," .Values.config.storage.providers.local.allowed_mime_types | default "*/*" }}"
- name: STORAGE_LOCAL_RETENTION_DAYS
value: "{{ .Values.config.storage.providers.local.retention_days | default "30" }}"
{{- end }}
# S3 Storage Provider (Enterprise Edition)
{{- if .Values.config.storage.providers.s3.enabled }}
- name: STORAGE_DEFAULT_PROVIDER
value: "s3"
- name: STORAGE_S3_REGION
value: "{{ .Values.config.storage.providers.s3.region }}"
- name: STORAGE_S3_BUCKET
value: "{{ .Values.config.storage.providers.s3.bucket }}"
{{- if .Values.config.storage.providers.s3.bundle_bucket }}
- name: STORAGE_S3_BUNDLE_BUCKET
value: "{{ .Values.config.storage.providers.s3.bundle_bucket }}"
{{- end }}
- name: STORAGE_S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.config.storage.providers.s3.secretRef.name | default "storage-credentials" | quote }}
key: {{ .Values.config.storage.providers.s3.secretRef.accessKeyKey | default "S3_ACCESS_KEY" | quote }}
- name: STORAGE_S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.config.storage.providers.s3.secretRef.name | default "storage-credentials" | quote }}
key: {{ .Values.config.storage.providers.s3.secretRef.secretKeyKey | default "S3_SECRET_KEY" | quote }}
{{- if .Values.config.storage.providers.s3.endpoint }}
- name: STORAGE_S3_ENDPOINT
value: "{{ .Values.config.storage.providers.s3.endpoint }}"
{{- end }}
- name: STORAGE_S3_MAX_FILE_SIZE
value: "{{ .Values.config.storage.providers.s3.max_file_size | default "104857600" }}"
- name: STORAGE_S3_ALLOWED_MIME_TYPES
value: "{{ join "," .Values.config.storage.providers.s3.allowed_mime_types | default "*/*" }}"
- name: STORAGE_S3_RETENTION_DAYS
value: "{{ .Values.config.storage.providers.s3.retention_days | default "30" }}"
{{- end }}
# ----------- STORAGE UPLOAD ----------------
- name: STORAGE_UPLOAD_TEMP_DIR
value: "{{ .Values.config.storage.upload.temp_dir }}"
- name: STORAGE_UPLOAD_MAX_CONCURRENT
value: "{{ .Values.config.storage.upload.max_concurrent }}"
- name: STORAGE_UPLOAD_CHUNK_SIZE
value: "{{ .Values.config.storage.upload.chunk_size }}"
# ----------- STORAGE BACKUP ----------------
{{- if .Values.config.storage.backup.enabled }}
- name: STORAGE_BACKUP_ENABLED
value: "true"
- name: STORAGE_BACKUP_SCHEDULE
value: "{{ .Values.config.storage.backup.schedule }}"
- name: STORAGE_BACKUP_RETENTION_DAYS
value: "{{ .Values.config.storage.backup.retention.days }}"
- name: STORAGE_BACKUP_RETENTION_COPIES
value: "{{ .Values.config.storage.backup.retention.copies }}"
{{- end }}
# ----------- LOGGING ----------------
- name: LOG_LEVEL
value: "{{ .Values.logging.level }}"
- name: LOG_IS_FORMAT_JSON
value: "{{ .Values.logging.is_format_json }}"
- name: LOG_IS_FULL_DETAILS
value: "{{ .Values.logging.is_full_details }}"
- name: LOG_ENABLED_FILE_LOGGING
value: "{{ .Values.logging.file.enabled }}"
- name: LOG_DIR_PATH
value: "{{ .Values.logging.file.path }}"
- name: LOG_ENABLED_EXTERNAL_LOGGING
value: "{{ .Values.logging.external.enabled }}"
- name: LOG_EXTERNAL_HTTP_HOST
value: "{{ .Values.logging.external.host }}"
- name: LOG_EXTERNAL_HTTP_PORT
value: "{{ .Values.logging.external.port }}"
- name: LOG_EXTERNAL_HTTP_PATH
value: "{{ .Values.logging.external.path }}"
- name: LOG_EXTERNAL_HTTP_LEVEL
value: "{{ .Values.logging.external.level }}"
- name: LOG_EXTERNAL_HTTP_TOKEN
value: "{{ .Values.logging.external.token }}"
# ----------- HOCUSPOCUS ----------------
{{- if .Values.hocuspocus.enabled }}
- name: HOCUSPOCUS_URL
value: "ws://hocuspocus.{{ include "sebastian.namespace" . }}.svc.cluster.local:{{ .Values.hocuspocus.service.port }}"
{{- end }}
# ----------- EMAIL ----------------
- name: EMAIL_ENABLE
value: "{{ .Values.email.enabled }}"
- name: EMAIL_FROM
value: "{{ .Values.email.from }}"
- name: EMAIL_HOST
value: "{{ .Values.email.host }}"
- name: EMAIL_PORT
value: "{{ .Values.email.port }}"
- name: EMAIL_USERNAME
value: "{{ .Values.email.user }}"
- name: EMAIL_PASSWORD
value: "{{ .Values.email.password }}"
# Optional: Explicit provider type. If omitted, factory auto-detects 'resend' by RESEND_API_KEY
{{- if .Values.email.provider }}
- name: EMAIL_PROVIDER_TYPE
value: "{{ .Values.email.provider }}"
{{- end }}
# RESEND configuration (preferred for system emails). Use secret if available, else value.
{{- if and .Values.email.resendApiKeySecret.name .Values.email.resendApiKeySecret.key }}
- name: RESEND_API_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.email.resendApiKeySecret.name | quote }}
key: {{ .Values.email.resendApiKeySecret.key | quote }}
{{- else if .Values.email.resendApiKey }}
- name: RESEND_API_KEY
value: "{{ .Values.email.resendApiKey }}"
{{- end }}
{{- if .Values.email.resendBaseUrl }}
- name: RESEND_BASE_URL
value: "{{ .Values.email.resendBaseUrl }}"
{{- end }}
- name: INBOUND_EMAIL_IN_APP_PROCESSING_ENABLED
value: "yes"
# ----------- LLM ----------------
- name: OPENAI_API_KEY
value: "{{ .Values.config.llm.openai }}"
- name: ANTHROPIC_API_KEY
value: "{{ .Values.config.llm.anthropic }}"
# ----------- CRYPTO ----------------
- name: CRYPTO_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: CRYPTR_KEY
- name: SALT_BYTES
value: "{{ .Values.crypto.salt_bytes }}"
- name: ITERATIONS
value: "{{ .Values.crypto.iteration }}"
- name: KEY_LENGTH
value: "{{ .Values.crypto.key_length }}"
- name: ALGORITHM
value: "{{ .Values.crypto.algorithm }}"
- name: ALGA_AUTH_KEY
valueFrom:
secretKeyRef:
name: alga-psa-shared
key: ALGA_AUTH_KEY
- name: SECRET_KEY
value: "{{ .Values.crypto.secret_key }}"
# ----------- TOKEN ----------------
- name: TOKEN_SECRET_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: TOKEN_SECRET_KEY
- name: TOKEN_EXPIRE
value: "{{ .Values.token.expire }}"
# ----------- AUTH ----------------
{{- $rawAppUrl := .Values.appUrl | default (include "sebastian.resolveHost" .) }}
{{- $publicAppUrl := hasPrefix "http" $rawAppUrl | ternary $rawAppUrl (printf "https://%s" $rawAppUrl) }}
- name: NEXTAUTH_URL
value: "{{ $publicAppUrl }}"
# Public base URL for links in emails and client-side code
- name: NEXT_PUBLIC_BASE_URL
value: "{{ $publicAppUrl }}"
# App URL for Stripe redirects (can differ from host for blue/green testing)
- name: NEXT_PUBLIC_APP_URL
value: "{{ $publicAppUrl }}"
- name: NEXTAUTH_SECRET
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: NEXTAUTH_SECRET
{{- if .Values.setup.applianceBootstrap.enabled }}
# Lowercase alias (appliance bootstrap): getSecret()/EnvSecretProvider
# resolves secrets by lowercase name ahead of the uppercase fallback,
# so the server verifies admin password hashes with the same pepper the
# bootstrap job used to create them (see helm/templates/jobs.yaml).
- name: nextauth_secret
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: NEXTAUTH_SECRET
{{- end }}
- name: IMAP_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: IMAP_WEBHOOK_SECRET
- name: NEXTAUTH_SESSION_EXPIRES
value: "{{ .Values.auth.nextauth_session_expires }}"
- name: AUTH_TRUST_HOST
value: "true"
{{- if .Values.server.teamsIframeSessionCookie }}
# Relax session cookie SameSite so the Alga PSA tab can read the
# session from inside the Microsoft Teams iframe. Off by default;
# enable only on deployments that host the Teams personal tab.
- name: TEAMS_IFRAME_SESSION_COOKIE
value: "true"
{{- end }}
# ----------- EXTENSIONS ----------------
{{- with .Values.config.extensions.domainRoot }}
- name: EXT_DOMAIN_ROOT
value: "{{ . }}"
{{- end }}
# ----------- RUNNER / EXTENSION EXECUTION -----------
{{- with .Values.runner }}
{{- $runnerBaseUrl := .baseUrl | default (printf "http://runner-exec.%s.svc.cluster.local" $.Release.Namespace) }}
- name: RUNNER_BASE_URL
value: "{{ $runnerBaseUrl }}"
{{- if and .serviceTokenSecret (hasKey .serviceTokenSecret "name") (hasKey .serviceTokenSecret "key") (.serviceTokenSecret.name) (.serviceTokenSecret.key) }}
- name: RUNNER_SERVICE_TOKEN
valueFrom:
secretKeyRef:
name: {{ .serviceTokenSecret.name | quote }}
key: {{ .serviceTokenSecret.key | quote }}
{{- else if .serviceToken }}
- name: RUNNER_SERVICE_TOKEN
value: "{{ .serviceToken }}"
{{- end }}
{{- if .debugStream }}
{{- if and .debugStream.redisUrlSecret (hasKey .debugStream.redisUrlSecret "name") (hasKey .debugStream.redisUrlSecret "key") (.debugStream.redisUrlSecret.name) (.debugStream.redisUrlSecret.key) }}
- name: RUNNER_DEBUG_REDIS_URL
valueFrom:
secretKeyRef:
name: {{ .debugStream.redisUrlSecret.name | quote }}
key: {{ .debugStream.redisUrlSecret.key | quote }}
{{- else if .debugStream.redisUrl }}
- name: RUNNER_DEBUG_REDIS_URL
value: "{{ .debugStream.redisUrl }}"
{{- end }}
{{- if .debugStream.streamPrefix }}
- name: RUNNER_DEBUG_REDIS_STREAM_PREFIX
value: "{{ .debugStream.streamPrefix }}"
{{- end }}
{{- if .debugStream.maxLen }}
- name: RUNNER_DEBUG_REDIS_MAXLEN
value: "{{ .debugStream.maxLen }}"
{{- end }}
{{- end }}
{{- end }}
# ----------- GOOGLE AUTH ----------------
{{- with include "sebastian.googleOAuthEnv" . }}
{{ . | nindent 10 }}
{{- end }}
# ----------- GMAIL INTEGRATION ----------------
{{- if (default dict .Values.gmail_integration).enabled }}
- name: GOOGLE_CLIENT_ID
value: "{{ .Values.gmail_integration.client_id }}"
- name: GOOGLE_CLIENT_SECRET
value: "{{ .Values.gmail_integration.client_secret }}"
- name: GOOGLE_PROJECT_ID
value: "{{ .Values.gmail_integration.project_id }}"
{{- if .Values.gmail_integration.redirect_uri }}
- name: GOOGLE_REDIRECT_URI
value: "{{ .Values.gmail_integration.redirect_uri }}"
{{- end }}
{{- end }}
# ----------- NM-STORE INTEGRATION ----------------
{{- if .Values.nm_store_integration }}
{{- if .Values.nm_store_integration.url }}
- name: NM_STORE_URL
value: "{{ .Values.nm_store_integration.url }}"
{{- end }}
{{- if .Values.nm_store_integration.use_shared_secret }}
- name: TEMPORAL_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: nm-store-db-secret
key: TEMPORAL_WEBHOOK_SECRET
- name: ALGA_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: nm-store-db-secret
key: ALGA_WEBHOOK_SECRET
{{- end }}
{{- end }}
# ----------- MICROSOFT INTEGRATION ----------------
{{- if (default dict .Values.microsoft_integration).enabled }}
{{- if .Values.microsoft_integration.client_id }}
- name: MICROSOFT_CLIENT_ID
value: "{{ .Values.microsoft_integration.client_id }}"
{{- end }}
{{- if .Values.microsoft_integration.client_secret }}
- name: MICROSOFT_CLIENT_SECRET
value: "{{ .Values.microsoft_integration.client_secret }}"
{{- end }}
{{- if .Values.microsoft_integration.tenant_id }}
- name: MICROSOFT_TENANT_ID
value: "{{ .Values.microsoft_integration.tenant_id }}"
{{- end }}
{{- if .Values.microsoft_integration.redirect_uri }}
- name: MICROSOFT_REDIRECT_URI
value: "{{ .Values.microsoft_integration.redirect_uri }}"
{{- end }}
{{- with include "sebastian.microsoftOAuthEnv" . }}
{{ . | nindent 10 }}
{{- end }}
{{- end }}
# ----------- XERO INTEGRATION ----------------
{{- if (default dict .Values.xero_integration).enabled }}
{{- if .Values.xero_integration.client_id }}
- name: XERO_CLIENT_ID
value: "{{ .Values.xero_integration.client_id }}"
{{- end }}
# XERO_CLIENT_SECRET is resolved via the secret provider chain:
# 1. Vault (if configured in secrets_provider.readChain)
# 2. Filesystem secrets (Docker secrets at /run/secrets/xero_client_secret)
# 3. Environment variable XERO_CLIENT_SECRET
# The application's secret provider will automatically resolve it.
# Note: XERO_REDIRECT_URI is now derived from APP_BASE_URL in the application code
{{- end }}
# ----------- NINJAONE INTEGRATION ----------------
{{- if (default dict .Values.ninjaone_integration).enabled }}
{{- if .Values.ninjaone_integration.client_id }}
- name: NINJAONE_CLIENT_ID
value: "{{ .Values.ninjaone_integration.client_id }}"
{{- end }}
{{- if .Values.ninjaone_integration.client_secret }}
- name: NINJAONE_CLIENT_SECRET
value: "{{ .Values.ninjaone_integration.client_secret }}"
{{- end }}
{{- if .Values.ninjaone_integration.default_region }}
- name: NINJAONE_DEFAULT_REGION
value: "{{ .Values.ninjaone_integration.default_region }}"
{{- end }}
{{- with include "sebastian.ninjaonetOAuthEnv" . }}
{{ . | nindent 10 }}
{{- end }}
{{- end }}
# ----------- STRIPE INTEGRATION ----------------
# Sensitive keys (read from secrets via secret provider)
{{- if .Values.secrets.stripe_secret_key }}
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: stripe_secret_key
{{- end }}
{{- if .Values.secrets.stripe_publishable_key }}
- name: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: stripe_publishable_key
{{- end }}
{{- if .Values.secrets.stripe_webhook_secret }}
- name: STRIPE_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: stripe_webhook_secret
{{- end }}
# ----------- APPLE IAP INTEGRATION ----------------
{{- if .Values.secrets.APPLE_IAP_KEY_ID }}
- name: APPLE_IAP_KEY_ID
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_IAP_KEY_ID
{{- end }}
{{- if .Values.secrets.APPLE_IAP_ISSUER_ID }}
- name: APPLE_IAP_ISSUER_ID
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_IAP_ISSUER_ID
{{- end }}
{{- if .Values.secrets.APPLE_IAP_BUNDLE_ID }}
- name: APPLE_IAP_BUNDLE_ID
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_IAP_BUNDLE_ID
{{- end }}
{{- if .Values.secrets.APPLE_IAP_PRIVATE_KEY }}
- name: APPLE_IAP_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_IAP_PRIVATE_KEY
{{- end }}
{{- if .Values.secrets.APPLE_IAP_ENVIRONMENT }}
- name: APPLE_IAP_ENVIRONMENT
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_IAP_ENVIRONMENT
{{- end }}
# ----------- SIGN IN WITH APPLE ----------------
{{- if .Values.secrets.APPLE_SIGN_IN_BUNDLE_ID }}
- name: APPLE_SIGN_IN_BUNDLE_ID
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_SIGN_IN_BUNDLE_ID
{{- end }}
{{- if .Values.secrets.APPLE_SIGN_IN_TEAM_ID }}
- name: APPLE_SIGN_IN_TEAM_ID
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_SIGN_IN_TEAM_ID
{{- end }}
{{- if .Values.secrets.APPLE_SIGN_IN_KEY_ID }}
- name: APPLE_SIGN_IN_KEY_ID
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_SIGN_IN_KEY_ID
{{- end }}
{{- if .Values.secrets.APPLE_SIGN_IN_PRIVATE_KEY }}
- name: APPLE_SIGN_IN_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_SIGN_IN_PRIVATE_KEY
{{- end }}
{{- if .Values.secrets.APPLE_SIGN_IN_ENCRYPTION_KEY }}
- name: APPLE_SIGN_IN_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: APPLE_SIGN_IN_ENCRYPTION_KEY
{{- end }}
# Non-sensitive Stripe configuration
{{- if .Values.stripe }}
{{- if .Values.stripe.master_billing_tenant_id_secret }}
- name: MASTER_BILLING_TENANT_ID
valueFrom:
secretKeyRef:
name: {{ .Values.stripe.master_billing_tenant_id_secret.name | default "alga-psa-shared" }}
key: {{ .Values.stripe.master_billing_tenant_id_secret.key | default "MASTER_BILLING_TENANT_ID" }}
{{- else if .Values.stripe.master_billing_tenant_id }}
- name: MASTER_BILLING_TENANT_ID
value: "{{ .Values.stripe.master_billing_tenant_id }}"
{{- end }}
{{- if .Values.stripe.pro_price_id }}
- name: STRIPE_PRO_PRICE_ID
value: "{{ .Values.stripe.pro_price_id }}"
{{- end }}
{{- if .Values.stripe.premium_base_price_id }}
- name: STRIPE_PREMIUM_BASE_PRICE_ID
value: "{{ .Values.stripe.premium_base_price_id }}"
{{- end }}
{{- if .Values.stripe.premium_user_price_id }}
- name: STRIPE_PREMIUM_USER_PRICE_ID
value: "{{ .Values.stripe.premium_user_price_id }}"
{{- end }}
{{- if .Values.stripe.pro_annual_price_id }}
- name: STRIPE_PRO_ANNUAL_PRICE_ID
value: "{{ .Values.stripe.pro_annual_price_id }}"
{{- end }}
{{- if .Values.stripe.premium_base_annual_price_id }}
- name: STRIPE_PREMIUM_BASE_ANNUAL_PRICE_ID
value: "{{ .Values.stripe.premium_base_annual_price_id }}"
{{- end }}
{{- if .Values.stripe.premium_user_annual_price_id }}
- name: STRIPE_PREMIUM_USER_ANNUAL_PRICE_ID
value: "{{ .Values.stripe.premium_user_annual_price_id }}"
{{- end }}
{{- if .Values.stripe.early_adopters_base_price_id }}
- name: STRIPE_EARLY_ADOPTERS_BASE_PRICE_ID
value: "{{ .Values.stripe.early_adopters_base_price_id }}"
{{- end }}
{{- if .Values.stripe.early_adopters_user_price_id }}
- name: STRIPE_EARLY_ADOPTERS_USER_PRICE_ID
value: "{{ .Values.stripe.early_adopters_user_price_id }}"
{{- end }}
{{- if .Values.stripe.early_adopters_base_annual_price_id }}
- name: STRIPE_EARLY_ADOPTERS_BASE_ANNUAL_PRICE_ID
value: "{{ .Values.stripe.early_adopters_base_annual_price_id }}"
{{- end }}
{{- if .Values.stripe.early_adopters_user_annual_price_id }}
- name: STRIPE_EARLY_ADOPTERS_USER_ANNUAL_PRICE_ID
value: "{{ .Values.stripe.early_adopters_user_annual_price_id }}"
{{- end }}
{{- end }}
# ----------- IPINFO INTEGRATION ----------------
{{- if .Values.secrets.IPINFO_API_TOKEN }}
- name: IPINFO_API_TOKEN
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: IPINFO_API_TOKEN
{{- end }}
# ----------- MICROSOFT TEAMS BOT ----------------
{{- if .Values.secrets.TEAMS_BOT_APP_ID }}
- name: TEAMS_BOT_APP_ID
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: TEAMS_BOT_APP_ID
{{- end }}
{{- if .Values.secrets.TEAMS_BOT_APP_TENANT_ID }}
- name: TEAMS_BOT_APP_TENANT_ID
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: TEAMS_BOT_APP_TENANT_ID
{{- end }}
{{- if .Values.secrets.TEAMS_BOT_APP_PASSWORD }}
- name: TEAMS_BOT_APP_PASSWORD
valueFrom:
secretKeyRef:
name: "{{include "sebastian.fullname" .}}-secrets"
key: TEAMS_BOT_APP_PASSWORD
{{- end }}
# ----------- SECRET PROVIDER ----------------
# Composite secret provider configuration
- name: SECRET_READ_CHAIN
value: "{{ .Values.secrets_provider.readChain | default "env,filesystem" }}"
- name: SECRET_WRITE_PROVIDER
value: "{{ .Values.secrets_provider.writeProvider | default "filesystem" }}"
{{- if .Values.secrets_provider.envPrefix }}
- name: SECRET_ENV_PREFIX
value: "{{ .Values.secrets_provider.envPrefix }}"
{{- end }}
# Vault configuration (only if vault is used)
{{- if or (contains "vault" .Values.secrets_provider.readChain) (eq .Values.secrets_provider.writeProvider "vault") }}
{{- if .Values.secrets_provider.vault.addr }}
- name: VAULT_ADDR
value: "{{ .Values.secrets_provider.vault.addr }}"
{{- end }}
{{- if or .Values.secrets_provider.vault.token (or (contains "vault" .Values.secrets_provider.readChain) (eq .Values.secrets_provider.writeProvider "vault")) }}
- name: VAULT_TOKEN
{{- if .Values.secrets_provider.vault.token }}
value: "{{ .Values.secrets_provider.vault.token }}"
{{- else }}
valueFrom:
secretKeyRef:
name: vault-credentials
key: VAULT_TOKEN
{{- end }}
{{- end }}
{{- if .Values.secrets_provider.vault.appSecretPath }}
- name: VAULT_APP_SECRET_PATH
value: "{{ .Values.secrets_provider.vault.appSecretPath }}"
{{- end }}
{{- if .Values.secrets_provider.vault.tenantSecretPathTemplate }}
- name: VAULT_TENANT_SECRET_PATH_TEMPLATE
value: "{{ .Values.secrets_provider.vault.tenantSecretPathTemplate }}"
{{- end }}
{{- end }}
{{- if and .Values.server.persistence.enabled (not .Values.config.storage.providers.s3.enabled) }}
volumeMounts:
- name: storage-volume
mountPath: {{ .Values.server.persistence.mountPath }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.server.service.port }}
protocol: TCP
{{- if and .Values.server.probes .Values.server.probes.startup .Values.server.probes.startup.enabled }}
startupProbe:
httpGet:
path: {{ .Values.server.probes.startup.httpGet.path | default "/auth/signin" }}
port: {{ .Values.server.probes.startup.httpGet.port | default .Values.server.service.port }}
initialDelaySeconds: {{ .Values.server.probes.startup.initialDelaySeconds | default 10 }}
periodSeconds: {{ .Values.server.probes.startup.periodSeconds | default 10 }}
timeoutSeconds: {{ .Values.server.probes.startup.timeoutSeconds | default 10 }}
failureThreshold: {{ .Values.server.probes.startup.failureThreshold | default 30 }}
successThreshold: {{ .Values.server.probes.startup.successThreshold | default 1 }}
{{- end }}
{{- if and .Values.server.probes .Values.server.probes.liveness .Values.server.probes.liveness.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.server.probes.liveness.httpGet.path | default "/auth/signin" }}
port: {{ .Values.server.probes.liveness.httpGet.port | default .Values.server.service.port }}
initialDelaySeconds: {{ .Values.server.probes.liveness.initialDelaySeconds | default 40 }}
periodSeconds: {{ .Values.server.probes.liveness.periodSeconds | default 30 }}
timeoutSeconds: {{ .Values.server.probes.liveness.timeoutSeconds | default 10 }}
failureThreshold: {{ .Values.server.probes.liveness.failureThreshold | default 3 }}
successThreshold: {{ .Values.server.probes.liveness.successThreshold | default 1 }}
{{- end }}
{{- if and .Values.server.probes .Values.server.probes.readiness .Values.server.probes.readiness.enabled }}
readinessProbe:
httpGet:
path: {{ .Values.server.probes.readiness.httpGet.path | default "/auth/signin" }}
port: {{ .Values.server.probes.readiness.httpGet.port | default .Values.server.service.port }}
initialDelaySeconds: {{ .Values.server.probes.readiness.initialDelaySeconds | default 30 }}
periodSeconds: {{ .Values.server.probes.readiness.periodSeconds | default 15 }}
timeoutSeconds: {{ .Values.server.probes.readiness.timeoutSeconds | default 10 }}
failureThreshold: {{ .Values.server.probes.readiness.failureThreshold | default 3 }}
successThreshold: {{ .Values.server.probes.readiness.successThreshold | default 1 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}