Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7ecd83d
DEVOPS-3406 - helm: declared secretsMode with ESO ExternalSecret support
raul-gherman-modaoperandi Sep 7, 2026
7046be8
DEVOPS-3406 - add legacy shim for secretsMode handling and validation
raul-gherman-modaoperandi Sep 8, 2026
8d3c3f6
DEVOPS-3406 - update secretsMode handling and validation for inline mode
raul-gherman-modaoperandi Sep 8, 2026
f0cb746
DEVOPS-3406 - add validation and legacy shim for migration Job secret…
raul-gherman-modaoperandi Sep 8, 2026
d439353
DEVOPS-3406 - add createNamespace option and template for Namespace r…
raul-gherman-modaoperandi Sep 8, 2026
5a5e775
DEVOPS-3406 - add createNamespace option and template for Namespace r…
raul-gherman-modaoperandi Sep 8, 2026
6a7e2e7
DEVOPS-3406 - enhance migration Job and ExternalSecret handling with …
raul-gherman-modaoperandi Sep 8, 2026
e0e6e15
DEVOPS-3406 - address Copilot review comments (valid)
raul-gherman-modaoperandi Sep 8, 2026
42813d6
DEVOPS-3406 - address Copilot review comments (valid) [1]
raul-gherman-modaoperandi Sep 8, 2026
1110614
DEVOPS-3406 - address Copilot review comments (valid) [2]
raul-gherman-modaoperandi Sep 8, 2026
39b7c6e
DEVOPS-3406 - address Copilot review comments (valid) [3]
raul-gherman-modaoperandi Sep 8, 2026
96642f8
DEVOPS-3406 - address Copilot review comments (valid) [4]
raul-gherman-modaoperandi Sep 8, 2026
2f66319
DEVOPS-3406 - address Copilot review comments (valid) [5]
raul-gherman-modaoperandi Sep 8, 2026
a735f66
DEVOPS-3406 - address Copilot review comments (valid) [6]
raul-gherman-modaoperandi Sep 8, 2026
cb57d91
DEVOPS-3406 - address Copilot review comments (valid) [7]
raul-gherman-modaoperandi Sep 8, 2026
a5f6f16
DEVOPS-3406 - address Copilot review comments (valid) [8]
raul-gherman-modaoperandi Sep 8, 2026
aa0d432
Update assertions for openwork-ee-secret in tests
raul-gherman-modaoperandi Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions packaging/helm/openwork-ee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,20 +284,86 @@ Published self-host planning pages:

## Secrets

The chart can create an Opaque Secret from `secret.values`, or consume an existing Secret:
The deployment declares how it manages secrets with a single key:

```yaml
secret:
secretsMode: inline # inline | existingSecret | externalSecrets
```

- `inline` (default): the chart renders an Opaque Secret from `secret.values`.
Local evaluation only — the values live wherever the values file lives, so
never commit real credentials.
- `existingSecret`: workloads consume a pre-created Secret named by
`secret.existingSecret` (requires `secret.create: false`); the chart renders
no secret resource.
- `externalSecrets`: the chart renders an
[External Secrets Operator](https://external-secrets.io/) `ExternalSecret`
that materializes the workload Secret from an external provider — the
GitOps/ArgoCD-safe path, where git holds only store references and remote
key paths (requires `secret.create: false`).

Any `secret.keys` override applies in every mode, since all three resolve the
workload Secret through the same names. The mode combinations are enforced at
render time: an unknown `secretsMode`, a missing `secret.existingSecret` in
`existingSecret` mode, `secret.existingSecret` set in any other mode, and
`secret.create: true` outside `inline` mode all fail the render.

### existingSecret

```yaml
secret:
secretsMode: existingSecret
create: false
existingSecret: openwork-ee-secrets
```

The existing Secret must contain the keys listed under `secret.keys`, especially:
The existing Secret must be created in the `namespace` where the chart deploys
(default `openwork`) before the workloads start, and must contain the keys
listed under `secret.keys`, especially:

- `DATABASE_URL`
- `BETTER_AUTH_SECRET`
- `DEN_DB_ENCRYPTION_KEY`

### externalSecrets (GitOps / ArgoCD)

For GitOps flows (ArgoCD runs `helm template`, so anything in values lands in
git and in rendered manifests), use ESO mode. The chart renders an
`ExternalSecret` that materializes the same-named workload Secret from your
provider in-cluster:

The chart renders `spec.data` — the oldest stable ESO shape, unchanged since
`external-secrets.io/v1beta1` — pulling every `secret.keys.*` entry from
`<pathPrefix>/<KEY_NAME>` in the provider. The key list is generated from
`secret.keys`, so it can never drift from what the workloads consume:

```yaml
secret:
secretsMode: externalSecrets
create: false
externalSecrets:
secretStoreRef:
# References an existing (Cluster)SecretStore; for AWS Secrets Manager the
# store itself carries spec.provider.aws (region, auth), the chart only
# points at it by name.
name: external-secrets
kind: ClusterSecretStore
refreshInterval: 5m
# Every secret.keys.* value must exist as a JSON property under this trunk,
# e.g. eks/openwork/prod/den/DATABASE_URL.
pathPrefix: "eks/openwork/prod/den"
```

Every property the workloads consume must exist in your provider under
`pathPrefix`, named like `secret.keys.*` values (`DATABASE_URL`,
`BETTER_AUTH_SECRET`, ...) — the chart pulls each key by name and cannot
invent missing ones. `target.deletionPolicy` defaults to `Retain`, so
uninstalling the release keeps the materialized Secret. ESO must be installed
on the destination cluster with a `SecretStore`/`ClusterSecretStore`; the
chart selects `external-secrets.io/v1` or `v1beta1` from cluster capabilities
and fails loudly at sync time if the CRDs are missing.

Set optional `DATABASE_REDIS_URL` to enable Den API Redis-backed session and query caching. Set `DAYTONA_API_KEY` when `config.provisioner.mode` is `daytona`. Set `POLAR_ACCESS_TOKEN` when Polar feature gating is enabled. Set `OPENROUTER_MANAGEMENT_API_KEY` when enabling OpenWork Models management.

Redis cache examples:
Expand Down
102 changes: 99 additions & 3 deletions packaging/helm/openwork-ee/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- /*
Dedupe in both directions: contains handles release names that already
include the chart name (my-openwork-ee); hasPrefix handles release names
that prefix the chart name (release "openwork", chart "openwork-ee"), which
would otherwise produce doubled names like openwork-openwork-ee-secret.
*/ -}}
{{- if or (contains $name .Release.Name) (hasPrefix .Release.Name $name) -}}
Comment thread
raul-gherman-modaoperandi marked this conversation as resolved.
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
Expand Down Expand Up @@ -59,11 +65,101 @@ app.kubernetes.io/component: {{ .component }}
{{- end -}}
{{- end -}}

{{/*
Returns the workload Secret name as a quoted string: existingSecret values
are user-supplied and may look like YAML scalars (true, 1234), which would
otherwise render non-string manifest fields and fail at apply time.
Consumers that need the bare name trim the quotes.
*/}}
{{- define "openwork-ee.secretName" -}}
{{- if eq .Values.secret.secretsMode "existingSecret" -}}
{{- .Values.secret.existingSecret | toString | trim | quote -}}
{{- else -}}
{{- printf "%s-secret" (include "openwork-ee.fullname" .) | quote -}}
{{- end -}}
{{- end -}}

{{/* Bare (unquoted) Secret name for contexts that need it (e.g. lookup). */}}
{{- define "openwork-ee.secretNameRaw" -}}
{{- include "openwork-ee.secretName" . | trimAll "\"" -}}
{{- end -}}

{{/* Bare (unquoted) namespace name for contexts that need it. */}}
{{- define "openwork-ee.namespaceRaw" -}}
{{- include "openwork-ee.namespace" . | trimAll "\"" -}}
{{- end -}}

{{- define "openwork-ee.secretsMode.validate" -}}
{{- if not (has .Values.secret.secretsMode (list "inline" "existingSecret" "externalSecrets")) -}}
{{- fail "secretsMode must be one of inline, existingSecret, externalSecrets" -}}
{{- end -}}
{{- if eq .Values.secret.secretsMode "existingSecret" -}}
{{- if not (.Values.secret.existingSecret | toString | trim) -}}
{{- fail "secret.existingSecret is required when secretsMode=existingSecret" -}}
{{- end -}}
{{- end -}}
{{- if ne .Values.secret.secretsMode "existingSecret" -}}
{{- if .Values.secret.existingSecret -}}
{{- .Values.secret.existingSecret -}}
{{- fail "secret.existingSecret is only allowed when secretsMode=existingSecret" -}}
{{- end -}}
{{- end -}}
{{- if ne .Values.secret.secretsMode "inline" -}}
{{- if .Values.secret.create -}}
{{- fail "secret.create must be false when secretsMode is not inline" -}}
{{- end -}}
{{- end -}}
{{- if and (eq .Values.secret.secretsMode "inline") (not .Values.secret.create) -}}
{{- /* Legacy migration shim: values files from before secretsMode shipped that
set create=false with untouched placeholder values meant "no inline
secrets" — treat that as existingSecret mode. Real-looking values with
create=false are incoherent and must fail, not be silently rerouted. */ -}}
{{- $dsn := .Values.secret.values.databaseUrl | toString -}}
{{- $auth := .Values.secret.values.betterAuthSecret | toString -}}
{{- $enc := .Values.secret.values.denDbEncryptionKey | toString -}}
{{- /*
Reroute only when ALL three required values are still placeholders: a
partially-filled inline block means someone set real values and create=false
is incoherent — fail rather than silently ignoring their real values.
*/ -}}
{{- $dsnIsPlaceholder := or (contains "change-me@" $dsn) (contains "******" $dsn) -}}
{{- if and (hasPrefix "CHANGE_ME" $auth) (hasPrefix "CHANGE_ME" $enc) $dsnIsPlaceholder -}}
{{- $_ := set .Values.secret "secretsMode" "existingSecret" -}}
{{- if not (.Values.secret.existingSecret | toString | trim) -}}
{{- $_ := set .Values.secret "existingSecret" (include "openwork-ee.fullname" . | printf "%s-secret") -}}
{{- end -}}
{{- else -}}
{{- include "openwork-ee.fullname" . }}-secret
{{- fail "secret.create must be true when secretsMode=inline (set secretsMode=existingSecret or externalSecrets to source secrets externally)" -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{- define "openwork-ee.externalSecrets.apiVersion" -}}
{{- if .Capabilities.APIVersions.Has "external-secrets.io/v1" -}}
external-secrets.io/v1
{{- else -}}
external-secrets.io/v1beta1
{{- end -}}
{{- end -}}

{{- define "openwork-ee.externalSecrets.validate" -}}
{{- if eq .Values.secret.secretsMode "externalSecrets" -}}
{{- $storeName := "" -}}
{{- if .Values.externalSecrets.secretStoreRef -}}
{{- $storeName = .Values.externalSecrets.secretStoreRef.name | toString | trim -}}
{{- end -}}
{{- if not $storeName -}}
{{- fail "externalSecrets.secretStoreRef.name is required when secretsMode=externalSecrets" -}}
{{- end -}}
{{- $storeKind := "" -}}
{{- if .Values.externalSecrets.secretStoreRef -}}
{{- $storeKind = .Values.externalSecrets.secretStoreRef.kind | toString -}}
{{- end -}}
{{- if not (has $storeKind (list "SecretStore" "ClusterSecretStore")) -}}
{{- fail "externalSecrets.secretStoreRef.kind must be SecretStore or ClusterSecretStore" -}}
{{- end -}}
{{- if not (.Values.externalSecrets.pathPrefix | toString | trim) -}}
{{- fail "externalSecrets.pathPrefix is required when secretsMode=externalSecrets" -}}
{{- end -}}
{{- end -}}
{{- end -}}

Expand Down
1 change: 1 addition & 0 deletions packaging/helm/openwork-ee/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{- include "openwork-ee.secretsMode.validate" . }}
apiVersion: v1
kind: ConfigMap
metadata:
Expand Down
43 changes: 43 additions & 0 deletions packaging/helm/openwork-ee/templates/externalsecret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{{- include "openwork-ee.secretsMode.validate" . }}
{{- if eq .Values.secret.secretsMode "externalSecrets" }}
{{- include "openwork-ee.externalSecrets.validate" . }}
apiVersion: {{ include "openwork-ee.externalSecrets.apiVersion" . }}
kind: ExternalSecret
metadata:
name: {{ include "openwork-ee.secretName" . }}
namespace: {{ include "openwork-ee.namespace" . }}
labels:
{{- include "openwork-ee.labels" . | nindent 4 }}
{{- /*
When the migration Job runs as a pre-install/pre-upgrade hook, the
ExternalSecret must apply first so ESO can start materializing the Secret
the Job consumes. Weight -10 precedes the Job's -5. ArgoCD maps these to
PreSync the same way, so GitOps installs get the same ordering.
*/}}
{{- if .Values.migrations.hook }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": before-hook-creation
{{- end }}
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval | quote }}
secretStoreRef:
name: {{ .Values.externalSecrets.secretStoreRef.name | toString | trim | quote }}
kind: {{ .Values.externalSecrets.secretStoreRef.kind }}
target:
name: {{ include "openwork-ee.secretName" . }}
creationPolicy: {{ .Values.externalSecrets.target.creationPolicy | default "Owner" }}
deletionPolicy: {{ .Values.externalSecrets.target.deletionPolicy | default "Retain" }}
data:
{{- $prefix := .Values.externalSecrets.pathPrefix | toString | trim | trimSuffix "/" }}
{{- range $name := keys .Values.secret.keys | sortAlpha }}
{{- $envKey := index $.Values.secret.keys $name }}
- secretKey: {{ $envKey | quote }}
remoteRef:
key: {{ printf "%s/%s" $prefix $envKey | quote }}
conversionStrategy: {{ $.Values.externalSecrets.conversionStrategy }}
decodingStrategy: {{ $.Values.externalSecrets.decodingStrategy }}
metadataPolicy: {{ $.Values.externalSecrets.metadataPolicy }}
{{- end }}
{{- end }}
33 changes: 29 additions & 4 deletions packaging/helm/openwork-ee/templates/migration-job.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{{- include "openwork-ee.customCa.validate" . }}
{{- /* Runs the legacy create=false shim before any secretsMode check below. */}}
{{- include "openwork-ee.secretsMode.validate" . }}
{{- if .Values.migrations.enabled }}
apiVersion: batch/v1
kind: Job
Expand Down Expand Up @@ -28,6 +30,9 @@ spec:
{{- end }}
spec:
restartPolicy: Never
{{- if ne .Values.secret.secretsMode "inline" }}
serviceAccountName: {{ include "openwork-ee.fullname" . }}-migrate
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
Expand All @@ -36,6 +41,26 @@ spec:
volumes:
{{- include "openwork-ee.customCa.volume" . | nindent 8 }}
{{- end }}
{{- /*
When the Secret is materialized asynchronously (ESO or out-of-band
creation), the Job must not fail on a missing Secret before it exists.
Block in an init container until it appears instead of erroring on the
env secretKeyRef.
*/}}
{{- if ne .Values.secret.secretsMode "inline" }}
initContainers:
- name: wait-for-secret
image: "{{ .Values.migrations.kubectlImage.repository }}:{{ .Values.migrations.kubectlImage.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
Comment on lines +52 to +54
command:
- sh
- -c
- |
until kubectl get secret {{ include "openwork-ee.secretNameRaw" . }} -n {{ include "openwork-ee.namespaceRaw" . }} > /dev/null 2>&1; do
echo "waiting for secret {{ include "openwork-ee.secretNameRaw" . }}..."
sleep 3
done
{{- end }}
containers:
- name: migrate
image: "{{ .Values.denApi.image.repository }}:{{ default .Values.image.tag .Values.denApi.image.tag }}"
Expand All @@ -52,22 +77,22 @@ spec:
- name: DB_MODE
value: {{ .Values.config.databaseMode | quote }}
- name: DATABASE_URL
{{- if .Values.secret.create }}
{{- if eq .Values.secret.secretsMode "inline" }}
value: {{ .Values.secret.values.databaseUrl | quote }}
Comment thread
raul-gherman-modaoperandi marked this conversation as resolved.
{{- else }}
valueFrom:
secretKeyRef:
name: {{ include "openwork-ee.secretName" . }}
key: {{ .Values.secret.keys.databaseUrl }}
key: {{ .Values.secret.keys.databaseUrl | quote }}
{{- end }}
- name: DEN_DB_ENCRYPTION_KEY
{{- if .Values.secret.create }}
{{- if eq .Values.secret.secretsMode "inline" }}
value: {{ .Values.secret.values.denDbEncryptionKey | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ include "openwork-ee.secretName" . }}
key: {{ .Values.secret.keys.denDbEncryptionKey }}
key: {{ .Values.secret.keys.denDbEncryptionKey | quote }}
{{- end }}
{{- if .Values.customCa.enabled }}
{{- include "openwork-ee.customCa.env" . | nindent 12 }}
Expand Down
66 changes: 66 additions & 0 deletions packaging/helm/openwork-ee/templates/migration-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{{- /*
RBAC for the migration Job's wait-for-secret init container: the Job polls
for an asynchronously materialized Secret (ESO / out-of-band creation)
instead of failing on the env secretKeyRef. Rendered only outside inline
mode. In hook mode these must be hooks with an earlier weight than the Job
(-6 precedes the Job's -5) so they exist before it starts.
*/}}
{{- /* Runs the legacy create=false shim before the secretsMode check below. */}}
{{- include "openwork-ee.secretsMode.validate" . }}
{{- if and .Values.migrations.enabled (ne .Values.secret.secretsMode "inline") }}
{{- $asHook := .Values.migrations.hook }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "openwork-ee.fullname" . }}-migrate
namespace: {{ include "openwork-ee.namespace" . }}
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "migration") | nindent 4 }}
{{- if $asHook }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-6"
"helm.sh/hook-delete-policy": before-hook-creation
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "openwork-ee.fullname" . }}-migrate
namespace: {{ include "openwork-ee.namespace" . }}
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "migration") | nindent 4 }}
{{- if $asHook }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-6"
"helm.sh/hook-delete-policy": before-hook-creation
{{- end }}
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: [{{ include "openwork-ee.secretName" . }}]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "openwork-ee.fullname" . }}-migrate
namespace: {{ include "openwork-ee.namespace" . }}
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "migration") | nindent 4 }}
{{- if $asHook }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-6"
"helm.sh/hook-delete-policy": before-hook-creation
{{- end }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "openwork-ee.fullname" . }}-migrate
subjects:
- kind: ServiceAccount
name: {{ include "openwork-ee.fullname" . }}-migrate
namespace: {{ include "openwork-ee.namespace" . }}
{{- end }}
Loading