diff --git a/.gitignore b/.gitignore index 7d9aeaa..df48dbf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,6 @@ CLAUDE.md # Test directory -/test/tmp -/test/test-cloud/* -!/test/test-cloud/README.md +test/tmp +test/test-cloud/* +!test/test-cloud/README.md diff --git a/apps/README.md b/apps/README.md index 558a7d5..94a5b30 100644 --- a/apps/README.md +++ b/apps/README.md @@ -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` +##### 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 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..` yaml path. When `wild-app-deploy` is run, a Secret resource will be created in the Kubernetes cluster with the 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..` yaml path. When `wild-app-deploy` is run, a Secret resource will be created in the Kubernetes cluster with the 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 env: @@ -159,9 +208,11 @@ env: valueFrom: secretKeyRef: 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. ## App Lifecycle diff --git a/apps/ghost/db-init-job.yaml b/apps/ghost/db-init-job.yaml index cebe472..066cfe5 100644 --- a/apps/ghost/db-init-job.yaml +++ b/apps/ghost/db-init-job.yaml @@ -27,7 +27,7 @@ spec: valueFrom: secretKeyRef: name: mysql-secrets - key: rootPassword + key: apps.mysql.rootPassword - name: DB_HOSTNAME value: "{{ .apps.ghost.dbHost }}" - name: DB_PORT @@ -40,5 +40,5 @@ spec: valueFrom: secretKeyRef: name: ghost-secrets - key: dbPassword + key: apps.ghost.dbPassword restartPolicy: OnFailure \ No newline at end of file diff --git a/apps/ghost/deployment.yaml b/apps/ghost/deployment.yaml index 39c52b4..3679adb 100644 --- a/apps/ghost/deployment.yaml +++ b/apps/ghost/deployment.yaml @@ -39,7 +39,7 @@ spec: valueFrom: secretKeyRef: name: ghost-secrets - key: dbPassword + key: apps.ghost.dbPassword - name: GHOST_HOST value: {{ .apps.ghost.domain }} - name: GHOST_PORT_NUMBER @@ -50,7 +50,7 @@ spec: valueFrom: secretKeyRef: name: ghost-secrets - key: adminPassword + key: apps.ghost.adminPassword - name: GHOST_EMAIL value: {{ .apps.ghost.adminEmail }} - name: GHOST_BLOG_TITLE @@ -75,7 +75,7 @@ spec: valueFrom: secretKeyRef: name: ghost-secrets - key: smtpPassword + key: apps.ghost.smtpPassword - name: GHOST_SMTP_FROM_ADDRESS value: {{ .apps.ghost.smtp.from }} resources: diff --git a/apps/gitea/db-init-job.yaml b/apps/gitea/db-init-job.yaml index dcd535c..d9a3454 100644 --- a/apps/gitea/db-init-job.yaml +++ b/apps/gitea/db-init-job.yaml @@ -36,7 +36,7 @@ spec: valueFrom: secretKeyRef: name: postgres-secrets - key: password + key: apps.postgres.password - name: DB_HOSTNAME value: "{{ .apps.gitea.dbHost }}" - name: DB_DATABASE_NAME @@ -47,5 +47,5 @@ spec: valueFrom: secretKeyRef: name: gitea-secrets - key: dbPassword + key: apps.gitea.dbPassword restartPolicy: OnFailure \ No newline at end of file diff --git a/apps/gitea/deployment.yaml b/apps/gitea/deployment.yaml index 991b312..906afb9 100644 --- a/apps/gitea/deployment.yaml +++ b/apps/gitea/deployment.yaml @@ -33,27 +33,27 @@ spec: valueFrom: secretKeyRef: name: gitea-secrets - key: adminPassword + key: apps.gitea.adminPassword - name: GITEA__security__SECRET_KEY valueFrom: secretKeyRef: name: gitea-secrets - key: secretKey + key: apps.gitea.secretKey - name: GITEA__security__INTERNAL_TOKEN valueFrom: secretKeyRef: name: gitea-secrets - key: jwtSecret + key: apps.gitea.jwtSecret - name: GITEA__database__PASSWD valueFrom: secretKeyRef: name: gitea-secrets - key: dbPassword + key: apps.gitea.dbPassword - name: GITEA__mailer__PASSWD valueFrom: secretKeyRef: name: gitea-secrets - key: smtpPassword + key: apps.gitea.smtpPassword ports: - name: ssh containerPort: 2222 diff --git a/apps/immich/db-init-job.yaml b/apps/immich/db-init-job.yaml index 888a3cf..630a5df 100644 --- a/apps/immich/db-init-job.yaml +++ b/apps/immich/db-init-job.yaml @@ -53,7 +53,7 @@ spec: valueFrom: secretKeyRef: name: postgres-secrets - key: password + key: apps.postgres.password - name: DB_HOSTNAME value: "{{ .apps.immich.dbHostname }}" - name: DB_DATABASE_NAME @@ -64,5 +64,5 @@ spec: valueFrom: secretKeyRef: name: immich-secrets - key: dbPassword + key: apps.immich.dbPassword restartPolicy: OnFailure diff --git a/apps/immich/deployment-microservices.yaml b/apps/immich/deployment-microservices.yaml index 8630cbd..220280c 100644 --- a/apps/immich/deployment-microservices.yaml +++ b/apps/immich/deployment-microservices.yaml @@ -33,7 +33,7 @@ spec: valueFrom: secretKeyRef: name: immich-secrets - key: dbPassword + key: apps.immich.dbPassword - name: TZ value: "{{ .apps.immich.timezone }}" - name: IMMICH_WORKERS_EXCLUDE diff --git a/apps/immich/deployment-server.yaml b/apps/immich/deployment-server.yaml index 1b79312..8755b15 100644 --- a/apps/immich/deployment-server.yaml +++ b/apps/immich/deployment-server.yaml @@ -36,7 +36,7 @@ spec: valueFrom: secretKeyRef: name: immich-secrets - key: dbPassword + key: apps.immich.dbPassword - name: TZ value: "{{ .apps.immich.timezone }}" - name: IMMICH_WORKERS_EXCLUDE diff --git a/apps/mysql/statefulset.yaml b/apps/mysql/statefulset.yaml index bd90c62..ea6975a 100644 --- a/apps/mysql/statefulset.yaml +++ b/apps/mysql/statefulset.yaml @@ -82,14 +82,14 @@ spec: valueFrom: secretKeyRef: name: mysql-secrets - key: rootPassword + key: apps.mysql.rootPassword - name: MYSQL_USER value: {{ .apps.mysql.user }} - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: mysql-secrets - key: password + key: apps.mysql.password - name: MYSQL_DATABASE value: {{ .apps.mysql.dbName }} - name: MYSQL_PORT diff --git a/apps/openproject/db-init-job.yaml b/apps/openproject/db-init-job.yaml index 1475c67..e20f516 100644 --- a/apps/openproject/db-init-job.yaml +++ b/apps/openproject/db-init-job.yaml @@ -36,7 +36,7 @@ spec: valueFrom: secretKeyRef: name: postgres-secrets - key: password + key: apps.postgres.password - name: DB_HOSTNAME value: "{{ .apps.openproject.dbHostname }}" - name: DB_DATABASE_NAME @@ -47,5 +47,5 @@ spec: valueFrom: secretKeyRef: name: openproject-secrets - key: dbPassword + key: apps.openproject.dbPassword restartPolicy: OnFailure \ No newline at end of file diff --git a/apps/openproject/seeder-job.yaml b/apps/openproject/seeder-job.yaml index 7866e71..8501815 100644 --- a/apps/openproject/seeder-job.yaml +++ b/apps/openproject/seeder-job.yaml @@ -62,12 +62,12 @@ spec: valueFrom: secretKeyRef: name: openproject-secrets - key: dbPassword + key: apps.openproject.dbPassword - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD valueFrom: secretKeyRef: name: openproject-secrets - key: adminPassword + key: apps.openproject.adminPassword resources: limits: memory: 200Mi @@ -106,12 +106,12 @@ spec: valueFrom: secretKeyRef: name: openproject-secrets - key: dbPassword + key: apps.openproject.dbPassword - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD valueFrom: secretKeyRef: name: openproject-secrets - key: adminPassword + key: apps.openproject.adminPassword resources: limits: memory: 512Mi diff --git a/apps/openproject/web-deployment.yaml b/apps/openproject/web-deployment.yaml index 328a878..7eb1321 100644 --- a/apps/openproject/web-deployment.yaml +++ b/apps/openproject/web-deployment.yaml @@ -84,12 +84,12 @@ spec: valueFrom: secretKeyRef: name: openproject-secrets - key: dbPassword + key: apps.openproject.dbPassword - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD valueFrom: secretKeyRef: name: openproject-secrets - key: adminPassword + key: apps.openproject.adminPassword args: - /app/docker/prod/wait-for-db resources: @@ -127,12 +127,12 @@ spec: valueFrom: secretKeyRef: name: openproject-secrets - key: dbPassword + key: apps.openproject.dbPassword - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD valueFrom: secretKeyRef: name: openproject-secrets - key: adminPassword + key: apps.openproject.adminPassword args: - /app/docker/prod/web volumeMounts: diff --git a/apps/openproject/worker-deployment.yaml b/apps/openproject/worker-deployment.yaml index e07d3db..fc6cc22 100644 --- a/apps/openproject/worker-deployment.yaml +++ b/apps/openproject/worker-deployment.yaml @@ -84,12 +84,12 @@ spec: valueFrom: secretKeyRef: name: openproject-secrets - key: dbPassword + key: apps.openproject.dbPassword - name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD valueFrom: secretKeyRef: name: openproject-secrets - key: adminPassword + key: apps.openproject.adminPassword args: - bash - /app/docker/prod/wait-for-db @@ -132,7 +132,7 @@ spec: valueFrom: secretKeyRef: name: openproject-secrets - key: dbPassword + key: apps.openproject.dbPassword - name: "OPENPROJECT_GOOD_JOB_QUEUES" value: "" volumeMounts: diff --git a/apps/postgres/deployment.yaml b/apps/postgres/deployment.yaml index d3bef8a..d14151f 100644 --- a/apps/postgres/deployment.yaml +++ b/apps/postgres/deployment.yaml @@ -44,7 +44,7 @@ spec: valueFrom: secretKeyRef: name: postgres-secrets - key: password + key: apps.postgres.password volumeMounts: - name: postgres-data mountPath: /var/lib/postgresql/data diff --git a/apps/postgres/doctor/test-job.yaml b/apps/postgres/doctor/test-job.yaml index ceea3bc..8d3850b 100644 --- a/apps/postgres/doctor/test-job.yaml +++ b/apps/postgres/doctor/test-job.yaml @@ -73,5 +73,5 @@ spec: valueFrom: secretKeyRef: name: postgres-secrets - key: password + key: apps.postgres.password restartPolicy: Never diff --git a/bin/wild-app-deploy b/bin/wild-app-deploy index ae3083a..8813300 100755 --- a/bin/wild-app-deploy +++ b/bin/wild-app-deploy @@ -79,27 +79,22 @@ deploy_secrets() { echo "Deploying secrets for app '${app_name}' in namespace '${namespace}'" - # Create secret data + # Gather data for app secret local secret_data="" while IFS= read -r secret_path; do - # Get the secret value using full path 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 [[ "${secret_value}" == CHANGE_ME_* ]]; then echo "Warning: Secret '${secret_path}' for app '${app_name}' still has dummy value: ${secret_value}" fi - secret_data="${secret_data} --from-literal=${secret_key}=${secret_value}" + secret_data="${secret_data} --from-literal=${secret_path}=${secret_value}" else echo "Error: Required secret '${secret_path}' not found in ${SECRETS_FILE} for app '${app_name}'" exit 1 fi done < <(yq eval '.requiredSecrets[]' "${manifest_file}") - # Create the secret if we have data + # Create/update app secret in cluster if [ -n "${secret_data}" ]; then echo "Creating/updating secret '${app_name}-secrets' in namespace '${namespace}'" if [ "${DRY_RUN:-}" = "--dry-run=client" ]; then @@ -112,9 +107,11 @@ deploy_secrets() { fi } -# Step 1: Create namespaces first (dependencies and main app) +# Step 1: Create namespaces first echo "Creating namespaces..." MANIFEST_FILE="apps/${APP_NAME}/manifest.yaml" + +# Create dependency namespaces. if [ -f "${MANIFEST_FILE}" ]; then if yq eval '.requires' "${MANIFEST_FILE}" | grep -q -v '^null$'; then yq eval '.requires[].name' "${MANIFEST_FILE}" | while read -r required_app; do