{{- 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 }}