Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 77 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 @@ -60,13 +66,81 @@ app.kubernetes.io/component: {{ .component }}
{{- end -}}

{{- define "openwork-ee.secretName" -}}
{{- if .Values.secret.existingSecret -}}
{{- .Values.secret.existingSecret -}}
{{- if eq .Values.secret.secretsMode "existingSecret" -}}
{{- .Values.secret.existingSecret | toString | trim -}}
{{- else -}}
{{- include "openwork-ee.fullname" . }}-secret
{{- end -}}
{{- 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 -}}
{{- 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 -}}
{{- if or (hasPrefix "CHANGE_ME" $auth) (hasPrefix "CHANGE_ME" $enc) (contains "change-me@" $dsn) (contains "******" $dsn) -}}
{{- $_ := 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 -}}
{{- 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 -}}

{{- define "openwork-ee.denApiServiceName" -}}
{{ include "openwork-ee.fullname" . }}-den-api
{{- 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 }}
remoteRef:
key: {{ printf "%s/%s" $prefix $envKey | quote }}
conversionStrategy: {{ $.Values.externalSecrets.conversionStrategy }}
decodingStrategy: {{ $.Values.externalSecrets.decodingStrategy }}
metadataPolicy: {{ $.Values.externalSecrets.metadataPolicy }}
{{- end }}
{{- end }}
29 changes: 27 additions & 2 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.secretName" . }} -n {{ include "openwork-ee.namespace" . }} > /dev/null 2>&1; do
echo "waiting for secret {{ include "openwork-ee.secretName" . }}..."
sleep 3
done
{{- end }}
containers:
- name: migrate
image: "{{ .Values.denApi.image.repository }}:{{ default .Values.image.tag .Values.denApi.image.tag }}"
Expand All @@ -52,7 +77,7 @@ 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:
Expand All @@ -61,7 +86,7 @@ spec:
key: {{ .Values.secret.keys.databaseUrl }}
{{- end }}
- name: DEN_DB_ENCRYPTION_KEY
{{- if .Values.secret.create }}
{{- if eq .Values.secret.secretsMode "inline" }}
value: {{ .Values.secret.values.denDbEncryptionKey | quote }}
{{- else }}
valueFrom:
Expand Down
64 changes: 64 additions & 0 deletions packaging/helm/openwork-ee/templates/migration-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{{- /*
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.
*/}}
{{- 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" . | quote }}]
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 }}
16 changes: 16 additions & 0 deletions packaging/helm/openwork-ee/templates/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{- /*
lookup returns empty under helm template / ArgoCD repo-server rendering, so
GitOps flows still get the Namespace manifest; a live helm install/upgrade
skips it when the namespace already exists, avoiding AlreadyExists failures
against pre-provisioned namespaces.
*/ -}}
{{- $namespaceName := include "openwork-ee.namespace" . | trimAll "\"" -}}
{{- $existing := lookup "v1" "Namespace" "" $namespaceName -}}
{{- if and .Values.createNamespace (not $existing) }}
apiVersion: v1
kind: Namespace
metadata:
name: {{ include "openwork-ee.namespace" . }}
labels:
{{- include "openwork-ee.labels" . | nindent 4 }}
Comment on lines +12 to +15
{{- end }}
3 changes: 2 additions & 1 deletion packaging/helm/openwork-ee/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{- if .Values.secret.create }}
{{- include "openwork-ee.secretsMode.validate" . }}
{{- if eq .Values.secret.secretsMode "inline" }}
apiVersion: v1
kind: Secret
metadata:
Expand Down
Loading