Use full secret paths.

This commit is contained in:
2025-08-04 13:57:52 -07:00
parent 22537da98e
commit 5ca8c010e5
17 changed files with 95 additions and 47 deletions

6
.gitignore vendored
View File

@@ -10,6 +10,6 @@ CLAUDE.md
# Test directory # Test directory
/test/tmp test/tmp
/test/test-cloud/* test/test-cloud/*
!/test/test-cloud/README.md !test/test-cloud/README.md

View File

@@ -149,9 +149,58 @@ Apps that rely on PostgreSQL or MySQL databases typically need a database initia
Examples of apps with db-init jobs: `gitea`, `codimd`, `immich`, `openproject` Examples of apps with db-init jobs: `gitea`, `codimd`, `immich`, `openproject`
##### Database URL Configuration
**Important:** When apps require database URLs with embedded credentials, always use a separate `dbUrl` secret instead of trying to construct the URL with environment variable substitution in Kustomize templates.
**Wrong** (Kustomize cannot process runtime env var substitution):
```yaml
- name: DB_URL
value: "postgresql://user:$(DB_PASSWORD)@host/db"
```
**Correct** (Use a dedicated secret):
```yaml
- name: DB_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: apps.appname.dbUrl
```
Add `apps.appname.dbUrl` to the manifest's `requiredSecrets` and the `wild-app-add` script will generate the complete URL with embedded credentials.
##### Security Context Requirements
Pods must comply with Pod Security Standards. All pods should include proper security contexts to avoid deployment warnings:
```yaml
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999 # Use appropriate non-root user ID
runAsGroup: 999 # Use appropriate group ID
seccompProfile:
type: RuntimeDefault
containers:
- name: container-name
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false # Set to true when possible
```
For PostgreSQL init jobs, use `runAsUser: 999` (postgres user). For other database types, use the appropriate non-root user ID for that database container.
#### Secrets #### Secrets
Secrets are managed in the `secrets.yaml` file in the Wild Cloud home directory. The app's `manifest.yaml` should list any required secrets under `requiredSecrets`. When the app is added, default secret values will be generated and stored in the `secrets.yaml` file. Secrets are always stored and referenced in the `apps.<app-name>.<secret-name>` yaml path. When `wild-app-deploy` is run, a Secret resource will be created in the Kubernetes cluster with the name `<app-name>-secrets`, containing all secrets defined in the manifest's `requiredSecrets` key. These secrets can then be referenced in the app's Kustomize files using a `secretKeyRef`. For example, to mount a secret in an environment variable, you would use: Secrets are managed in the `secrets.yaml` file in the Wild Cloud home directory. The app's `manifest.yaml` should list any required secrets under `requiredSecrets`. When the app is added, default secret values will be generated and stored in the `secrets.yaml` file. Secrets are always stored and referenced in the `apps.<app-name>.<secret-name>` yaml path. When `wild-app-deploy` is run, a Secret resource will be created in the Kubernetes cluster with the name `<app-name>-secrets`, containing all secrets defined in the manifest's `requiredSecrets` key. These secrets can then be referenced in the app's Kustomize files using a `secretKeyRef`.
**Important:** Always use the full dotted path from the manifest as the secret key, not just the last segment. For example, to mount a secret in an environment variable, you would use:
```yaml ```yaml
env: env:
@@ -159,9 +208,11 @@ env:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: dbPassword key: apps.immich.dbPassword # Use full dotted path, not just "dbPassword"
``` ```
This approach prevents naming conflicts between apps and makes secret keys more descriptive and consistent with the `secrets.yaml` structure.
`secrets.yaml` files should not be checked in to a git repository and are ignored by default in Wild Cloud home directories. Checked in kustomize files should only reference secrets, not compile them. `secrets.yaml` files should not be checked in to a git repository and are ignored by default in Wild Cloud home directories. Checked in kustomize files should only reference secrets, not compile them.
## App Lifecycle ## App Lifecycle

View File

@@ -27,7 +27,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: mysql-secrets name: mysql-secrets
key: rootPassword key: apps.mysql.rootPassword
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.ghost.dbHost }}" value: "{{ .apps.ghost.dbHost }}"
- name: DB_PORT - name: DB_PORT
@@ -40,5 +40,5 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ghost-secrets name: ghost-secrets
key: dbPassword key: apps.ghost.dbPassword
restartPolicy: OnFailure restartPolicy: OnFailure

View File

@@ -39,7 +39,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ghost-secrets name: ghost-secrets
key: dbPassword key: apps.ghost.dbPassword
- name: GHOST_HOST - name: GHOST_HOST
value: {{ .apps.ghost.domain }} value: {{ .apps.ghost.domain }}
- name: GHOST_PORT_NUMBER - name: GHOST_PORT_NUMBER
@@ -50,7 +50,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ghost-secrets name: ghost-secrets
key: adminPassword key: apps.ghost.adminPassword
- name: GHOST_EMAIL - name: GHOST_EMAIL
value: {{ .apps.ghost.adminEmail }} value: {{ .apps.ghost.adminEmail }}
- name: GHOST_BLOG_TITLE - name: GHOST_BLOG_TITLE
@@ -75,7 +75,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ghost-secrets name: ghost-secrets
key: smtpPassword key: apps.ghost.smtpPassword
- name: GHOST_SMTP_FROM_ADDRESS - name: GHOST_SMTP_FROM_ADDRESS
value: {{ .apps.ghost.smtp.from }} value: {{ .apps.ghost.smtp.from }}
resources: resources:

View File

@@ -36,7 +36,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: postgres-secrets name: postgres-secrets
key: password key: apps.postgres.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.gitea.dbHost }}" value: "{{ .apps.gitea.dbHost }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
@@ -47,5 +47,5 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: dbPassword key: apps.gitea.dbPassword
restartPolicy: OnFailure restartPolicy: OnFailure

View File

@@ -33,27 +33,27 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: adminPassword key: apps.gitea.adminPassword
- name: GITEA__security__SECRET_KEY - name: GITEA__security__SECRET_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: secretKey key: apps.gitea.secretKey
- name: GITEA__security__INTERNAL_TOKEN - name: GITEA__security__INTERNAL_TOKEN
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: jwtSecret key: apps.gitea.jwtSecret
- name: GITEA__database__PASSWD - name: GITEA__database__PASSWD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: dbPassword key: apps.gitea.dbPassword
- name: GITEA__mailer__PASSWD - name: GITEA__mailer__PASSWD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: smtpPassword key: apps.gitea.smtpPassword
ports: ports:
- name: ssh - name: ssh
containerPort: 2222 containerPort: 2222

View File

@@ -53,7 +53,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: postgres-secrets name: postgres-secrets
key: password key: apps.postgres.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.immich.dbHostname }}" value: "{{ .apps.immich.dbHostname }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
@@ -64,5 +64,5 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: dbPassword key: apps.immich.dbPassword
restartPolicy: OnFailure restartPolicy: OnFailure

View File

@@ -33,7 +33,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: dbPassword key: apps.immich.dbPassword
- name: TZ - name: TZ
value: "{{ .apps.immich.timezone }}" value: "{{ .apps.immich.timezone }}"
- name: IMMICH_WORKERS_EXCLUDE - name: IMMICH_WORKERS_EXCLUDE

View File

@@ -36,7 +36,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: dbPassword key: apps.immich.dbPassword
- name: TZ - name: TZ
value: "{{ .apps.immich.timezone }}" value: "{{ .apps.immich.timezone }}"
- name: IMMICH_WORKERS_EXCLUDE - name: IMMICH_WORKERS_EXCLUDE

View File

@@ -82,14 +82,14 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: mysql-secrets name: mysql-secrets
key: rootPassword key: apps.mysql.rootPassword
- name: MYSQL_USER - name: MYSQL_USER
value: {{ .apps.mysql.user }} value: {{ .apps.mysql.user }}
- name: MYSQL_PASSWORD - name: MYSQL_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: mysql-secrets name: mysql-secrets
key: password key: apps.mysql.password
- name: MYSQL_DATABASE - name: MYSQL_DATABASE
value: {{ .apps.mysql.dbName }} value: {{ .apps.mysql.dbName }}
- name: MYSQL_PORT - name: MYSQL_PORT

View File

@@ -36,7 +36,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: postgres-secrets name: postgres-secrets
key: password key: apps.postgres.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.openproject.dbHostname }}" value: "{{ .apps.openproject.dbHostname }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
@@ -47,5 +47,5 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: dbPassword key: apps.openproject.dbPassword
restartPolicy: OnFailure restartPolicy: OnFailure

View File

@@ -62,12 +62,12 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: dbPassword key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: adminPassword key: apps.openproject.adminPassword
resources: resources:
limits: limits:
memory: 200Mi memory: 200Mi
@@ -106,12 +106,12 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: dbPassword key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: adminPassword key: apps.openproject.adminPassword
resources: resources:
limits: limits:
memory: 512Mi memory: 512Mi

View File

@@ -84,12 +84,12 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: dbPassword key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: adminPassword key: apps.openproject.adminPassword
args: args:
- /app/docker/prod/wait-for-db - /app/docker/prod/wait-for-db
resources: resources:
@@ -127,12 +127,12 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: dbPassword key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: adminPassword key: apps.openproject.adminPassword
args: args:
- /app/docker/prod/web - /app/docker/prod/web
volumeMounts: volumeMounts:

View File

@@ -84,12 +84,12 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: dbPassword key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: adminPassword key: apps.openproject.adminPassword
args: args:
- bash - bash
- /app/docker/prod/wait-for-db - /app/docker/prod/wait-for-db
@@ -132,7 +132,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: openproject-secrets name: openproject-secrets
key: dbPassword key: apps.openproject.dbPassword
- name: "OPENPROJECT_GOOD_JOB_QUEUES" - name: "OPENPROJECT_GOOD_JOB_QUEUES"
value: "" value: ""
volumeMounts: volumeMounts:

View File

@@ -44,7 +44,7 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: postgres-secrets name: postgres-secrets
key: password key: apps.postgres.password
volumeMounts: volumeMounts:
- name: postgres-data - name: postgres-data
mountPath: /var/lib/postgresql/data mountPath: /var/lib/postgresql/data

View File

@@ -73,5 +73,5 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: postgres-secrets name: postgres-secrets
key: password key: apps.postgres.password
restartPolicy: Never restartPolicy: Never

View File

@@ -79,27 +79,22 @@ deploy_secrets() {
echo "Deploying secrets for app '${app_name}' in namespace '${namespace}'" echo "Deploying secrets for app '${app_name}' in namespace '${namespace}'"
# Create secret data # Gather data for app secret
local secret_data="" local secret_data=""
while IFS= read -r secret_path; do while IFS= read -r secret_path; do
# Get the secret value using full path
secret_value=$(yq eval ".${secret_path} // \"\"" "${SECRETS_FILE}") secret_value=$(yq eval ".${secret_path} // \"\"" "${SECRETS_FILE}")
# Extract just the key name for the Kubernetes secret (handle dotted paths)
secret_key="${secret_path##*.}"
if [ -n "${secret_value}" ] && [ "${secret_value}" != "null" ]; then if [ -n "${secret_value}" ] && [ "${secret_value}" != "null" ]; then
if [[ "${secret_value}" == CHANGE_ME_* ]]; then if [[ "${secret_value}" == CHANGE_ME_* ]]; then
echo "Warning: Secret '${secret_path}' for app '${app_name}' still has dummy value: ${secret_value}" echo "Warning: Secret '${secret_path}' for app '${app_name}' still has dummy value: ${secret_value}"
fi fi
secret_data="${secret_data} --from-literal=${secret_key}=${secret_value}" secret_data="${secret_data} --from-literal=${secret_path}=${secret_value}"
else else
echo "Error: Required secret '${secret_path}' not found in ${SECRETS_FILE} for app '${app_name}'" echo "Error: Required secret '${secret_path}' not found in ${SECRETS_FILE} for app '${app_name}'"
exit 1 exit 1
fi fi
done < <(yq eval '.requiredSecrets[]' "${manifest_file}") done < <(yq eval '.requiredSecrets[]' "${manifest_file}")
# Create the secret if we have data # Create/update app secret in cluster
if [ -n "${secret_data}" ]; then if [ -n "${secret_data}" ]; then
echo "Creating/updating secret '${app_name}-secrets' in namespace '${namespace}'" echo "Creating/updating secret '${app_name}-secrets' in namespace '${namespace}'"
if [ "${DRY_RUN:-}" = "--dry-run=client" ]; then if [ "${DRY_RUN:-}" = "--dry-run=client" ]; then
@@ -112,9 +107,11 @@ deploy_secrets() {
fi fi
} }
# Step 1: Create namespaces first (dependencies and main app) # Step 1: Create namespaces first
echo "Creating namespaces..." echo "Creating namespaces..."
MANIFEST_FILE="apps/${APP_NAME}/manifest.yaml" MANIFEST_FILE="apps/${APP_NAME}/manifest.yaml"
# Create dependency namespaces.
if [ -f "${MANIFEST_FILE}" ]; then if [ -f "${MANIFEST_FILE}" ]; then
if yq eval '.requires' "${MANIFEST_FILE}" | grep -q -v '^null$'; then if yq eval '.requires' "${MANIFEST_FILE}" | grep -q -v '^null$'; then
yq eval '.requires[].name' "${MANIFEST_FILE}" | while read -r required_app; do yq eval '.requires[].name' "${MANIFEST_FILE}" | while read -r required_app; do