From e2ba656c42d9cc2f315a9dbea9106928e93e682a Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Thu, 16 Apr 2026 18:38:22 +0300 Subject: [PATCH 01/30] feat(skills): add cozy-external-app skill for scaffolding external apps Add a new skill that generates the complete package structure for Cozystack external apps, including chart skeleton, values with cozyvalues-gen annotations, CozystackResourceDefinition registration, and dependency integration via managed CNPG clusters or external secret references. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .claude-plugin/marketplace.json | 6 + README.md | 1 + .../.claude-plugin/plugin.json | 9 + .../skills/cozy-external-app/SKILL.md | 612 ++++++++++++++++++ 4 files changed, 628 insertions(+) create mode 100644 skills/cozy-external-app/.claude-plugin/plugin.json create mode 100644 skills/cozy-external-app/skills/cozy-external-app/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3a1cb20..088abe1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,6 +12,12 @@ "description": "Deploy a Cozystack package to a dev cluster via make + cozyhr — handles fresh install and dev-loop iteration with ExternalArtifact support", "source": "./skills/cozy-deploy", "category": "infrastructure" + }, + { + "name": "cozy-external-app", + "description": "Scaffold a new Cozystack external app package — generates chart skeleton, CozystackResourceDefinition, and handles dependency integration (e.g. Immich → Postgres) via managed CNPG clusters or external secret references", + "source": "./skills/cozy-external-app", + "category": "infrastructure" } ] } diff --git a/README.md b/README.md index 0b04724..bd3e857 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Install a plugin: | Plugin | Description | | --- | --- | | **cozy-deploy** | Deploy a Cozystack package to a dev cluster via make + cozyhr | +| **cozy-external-app** | Scaffold a new Cozystack external app package with dependency integration | ## License diff --git a/skills/cozy-external-app/.claude-plugin/plugin.json b/skills/cozy-external-app/.claude-plugin/plugin.json new file mode 100644 index 0000000..5ce2f71 --- /dev/null +++ b/skills/cozy-external-app/.claude-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "cozy-external-app", + "version": "1.0.0", + "description": "Scaffold a new Cozystack external app package — generates chart skeleton, CozystackResourceDefinition, and handles dependency integration (e.g. Immich → Postgres) via managed CNPG clusters or external secret references", + "author": { + "name": "Cozystack", + "url": "https://github.com/cozystack" + } +} diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md new file mode 100644 index 0000000..24d2282 --- /dev/null +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -0,0 +1,612 @@ +--- +name: cozy-external-app +description: Scaffold a new Cozystack external app package inside an external-apps repository. Generates the full chart skeleton (Chart.yaml, Makefile, values.yaml with cozyvalues-gen annotations, templates), registers it in core/platform (namespace, HelmRelease, CozystackResourceDefinition), and wires dependency integration — supports managed CNPG Postgres clusters provisioned in-chart and external secret references for pre-existing services. Use when adding a new application (e.g. Immich, Gitea, Nextcloud) to an external-apps repo that follows the cozystack/external-apps-example layout. +argument-hint: " [--depends-on=postgres,redis] [--operator=] [--repo-dir=]" +--- + +# cozy-external-app + +This skill scaffolds a new Cozystack external app package. It creates all files needed for the app to appear in the Cozystack dashboard and be deployable via the GitOps pipeline (GitRepository → Flux HelmRelease → CozystackResourceDefinition). + +This is a **generate-only** skill. It never applies anything to a cluster, never commits, and never pushes. The user handles git operations themselves. + +Work in reasoning mode. Follow the phases in order. When a step fails or is ambiguous, stop and ask — do not guess API shapes or secret names. + +Use the phrasing "`cozy-external-app`" (not "the skill") in messages to the user, and state progress at each phase boundary. + +## Phase 1 — Parse arguments + +`$ARGUMENTS` contains the free-form tail after `/cozy-external-app`. Extract: + +- Positional `` — lowercase, hyphen-separated (e.g., `immich`, `my-app`). Required. +- `--depends-on=` — comma-separated dependency names (e.g., `postgres`, `redis`). Default: none. +- `--operator=` — Helm chart repository URL for a required operator (e.g., `https://immich-app.github.io/immich-charts`). Default: none. +- `--repo-dir=` — path to the external-apps repository root. Default: current working directory. + +If `` is missing, use `AskUserQuestion` to ask for it. + +## Phase 2 — Pre-flight checks + +Bail early if any check fails. + +1. **Repository structure**: verify `$REPO_DIR` contains `init.yaml`, `packages/core/platform/Chart.yaml`, and `scripts/package.mk`. If not, tell the user to `cd` into the external-apps repo root or pass `--repo-dir`. +2. **Tools installed**: check that `yq` (v4), `jq`, `base64`, `helm`, and `cozyvalues-gen` are available via `command -v`. If `cozyvalues-gen` is missing, print: + ```text + cozyvalues-gen is required. Install it from: + https://github.com/cozystack/cozyvalues-gen/releases/latest + ``` + Do not install it automatically. Stop. +3. **No collision**: verify `packages/apps/$APP_NAME/` does not already exist. If it does, stop and ask the user whether to overwrite or pick a different name. + +## Phase 3 — Gather app specification + +Use `AskUserQuestion` to collect: + +1. **Chart source**: upstream Helm chart (provide repo URL + chart name + version) or custom templates from scratch? +2. **Container image**: image reference (e.g., `ghcr.io/immich-app/immich-server:v1.120.0`). +3. **Public port**: does the app expose an HTTP port? If yes, which port number? Should an Ingress template be generated? +4. **Persistent storage**: does the app need a PVC? If yes, default size (e.g., `10Gi`). +5. **Icon**: path to an SVG file for the dashboard. If not available yet, note it — Phase 6 will create a `logos/` placeholder and Phase 9 will fail until the user provides one. + +Record all answers. Proceed only after user confirms the summary. + +## Phase 4 — Gather dependency specification + +If `--depends-on` was not passed, use `AskUserQuestion`: "Does this app need any backing services (e.g., postgres, redis, mongodb)? List them or say 'none'." + +For each dependency, determine the **integration pattern** via `AskUserQuestion`: + +### Pattern A — Managed provisioning (recommended for postgres) + +The app chart creates the backing service itself (e.g., a CNPG `Cluster` CR in its own templates). The app chart owns the lifecycle. + +Collect: +- Database name (default: `app`) +- Database user (default: `app`) +- Number of replicas (default: `2`) +- Storage size (default: `5Gi`) + +**CNPG secret convention** (verified from cozystack harbor/keycloak): +- Cluster named `{{ .Release.Name }}-db` creates: + - Service: `{{ .Release.Name }}-db-rw` (read-write), `{{ .Release.Name }}-db-r` (read-only) + - Secret: `{{ .Release.Name }}-db-app` with keys: `host`, `port`, `username`, `password`, `dbname` + - Secret: `{{ .Release.Name }}-db-superuser` with superuser credentials + +Collect the env variable mapping for the app container. Defaults: +```yaml +DB_HOST: secretKeyRef → {{ .Release.Name }}-db-app → host +DB_PORT: secretKeyRef → {{ .Release.Name }}-db-app → port +DB_USERNAME: secretKeyRef → {{ .Release.Name }}-db-app → username +DB_PASSWORD: secretKeyRef → {{ .Release.Name }}-db-app → password +DB_NAME: secretKeyRef → {{ .Release.Name }}-db-app → dbname +``` + +Ask the user if these env names are correct for their app, or if the app expects different names (e.g., `DATABASE_URL`, `PGHOST`, `POSTGRES_PASSWORD`). + +### Pattern B — External reference + +The app expects a pre-existing service. The user provisions it separately (e.g., via the Cozystack dashboard postgres app) and passes connection details as values. + +Collect: +- Which values.yaml fields to expose (e.g., `postgres.host`, `postgres.port`, `postgres.secretName`) +- How the app consumes them (env vars, config file mount, etc.) + +Present a summary of all dependencies with chosen patterns. Proceed only after user confirms. + +## Phase 5 — Create operator chart (conditional) + +Skip if `--operator` was not passed and the app does not need a custom operator. + +If an operator is needed, create `packages/system/$APP_NAME-operator/`: + +```bash +mkdir -p $REPO_DIR/packages/system/$APP_NAME-operator/charts +``` + +Create `Chart.yaml`: +```yaml +apiVersion: v2 +name: external-$APP_NAME-operator +version: 0.0.0 +``` + +Create `Makefile`: +```makefile +export NAME=$APP_NAME-operator +export NAMESPACE=external-$(NAME) + +include ../../../scripts/package.mk + +update: + rm -rf charts + helm repo add $APP_NAME $OPERATOR_REPO_URL + helm repo update $APP_NAME + helm pull $APP_NAME/$OPERATOR_CHART_NAME --untar --untardir charts --version "$OPERATOR_CHART_VERSION" +``` + +Run the update target to pull the operator chart: +```bash +cd $REPO_DIR/packages/system/$APP_NAME-operator && make update +``` + +Use `AskUserQuestion` to confirm the operator chart was pulled successfully before proceeding. + +## Phase 6 — Create app chart skeleton + +Create `packages/apps/$APP_NAME/` with these files: + +### Chart.yaml + +```yaml +apiVersion: v2 +name: $APP_NAME +description: A Helm chart for $APP_DISPLAY_NAME on Cozystack +type: application +version: 0.0.1 +appVersion: "$APP_VERSION" +icon: /logos/$APP_NAME.svg +``` + +### Makefile + +```makefile +include ../../../scripts/package.mk + +generate: + cozyvalues-gen -v values.yaml -s values.schema.json -r README.md + ../../../hack/update-crd.sh +``` + +### logos/$APP_NAME.svg + +If the user provided an icon path, copy it: +```bash +mkdir -p $REPO_DIR/packages/apps/$APP_NAME/logos +cp $ICON_PATH $REPO_DIR/packages/apps/$APP_NAME/logos/$APP_NAME.svg +``` + +If no icon was provided, create the `logos/` directory and print: +```text +Place your app icon at packages/apps/$APP_NAME/logos/$APP_NAME.svg before running make generate. +``` + +### values.yaml + +Use cozyvalues-gen annotation format. Follow the exact style from the cozystack postgres chart: + +```yaml +## +## @section Common parameters +## + +## @param {string} [host] - Hostname for external access. +host: "" + +## @param {quantity} size - Persistent Volume Claim size for application data. +size: 10Gi + +## @param {string} storageClass - StorageClass used to store the data. +storageClass: "" +``` + +**For Pattern A (managed postgres) dependencies**, add: + +```yaml +## +## @section Database configuration +## + +## @typedef {struct} Database - PostgreSQL database configuration (provisioned via CloudNativePG). +## @field {quantity} size - Persistent Volume size for database storage. +## @field {int} replicas - Number of database instances. + +## @param {Database} database - PostgreSQL database configuration. +database: + size: 5Gi + replicas: 2 +``` + +**For Pattern B (external reference) dependencies**, add: + +```yaml +## +## @section External PostgreSQL configuration +## + +## @typedef {struct} Postgres - External PostgreSQL connection configuration. +## @field {string} host - PostgreSQL host address. +## @field {int} port - PostgreSQL port. +## @field {string} secretName - Name of the Kubernetes Secret containing credentials (keys: username, password, dbname). + +## @param {Postgres} postgres - External PostgreSQL connection configuration. +postgres: + host: "" + port: 5432 + secretName: "" +``` + +### values.schema.json + +Generate via: +```bash +cd $REPO_DIR/packages/apps/$APP_NAME && cozyvalues-gen -v values.yaml -s values.schema.json -r README.md +``` + +If `cozyvalues-gen` fails, write a minimal valid JSON schema manually based on values.yaml fields. Verify with: +```bash +jq . values.schema.json > /dev/null +``` + +### README.md + +Generated by `cozyvalues-gen` in the same command above. If manual, create a parameters table matching the mongodb example format. + +### .helmignore + +```text +logos/ +``` + +## Phase 7 — Create templates + +Create `packages/apps/$APP_NAME/templates/` with the following files. + +### Main workload — $APP_NAME.yaml + +Generate the primary workload template. If the user chose "upstream Helm chart", wrap it in a Flux HelmRelease (like harbor does). If "custom templates", create a Deployment or StatefulSet directly. + +**For a direct Deployment example:** + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + app: {{ .Release.Name }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ .Release.Name }} + spec: + containers: + - name: $APP_NAME + image: $CONTAINER_IMAGE + ports: + - name: http + containerPort: $APP_PORT + env: + # Dependency env vars are added per Phase 4 specification + resources: {} +``` + +Add env vars for each dependency based on the pattern chosen in Phase 4. + +**For Pattern A (managed postgres) env vars:** + +```yaml + env: + - name: $DB_HOST_ENV + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-db-app + key: host + - name: $DB_PORT_ENV + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-db-app + key: port + - name: $DB_USERNAME_ENV + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-db-app + key: username + - name: $DB_PASSWORD_ENV + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-db-app + key: password + - name: $DB_NAME_ENV + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-db-app + key: dbname +``` + +**For Pattern B (external reference) env vars:** + +```yaml + env: + - name: $DB_HOST_ENV + value: {{ .Values.postgres.host | quote }} + - name: $DB_PORT_ENV + value: {{ .Values.postgres.port | quote }} + - name: $DB_PASSWORD_ENV + valueFrom: + secretKeyRef: + name: {{ .Values.postgres.secretName }} + key: password +``` + +### database.yaml (Pattern A only) + +Create a CNPG Cluster resource. Follow the exact pattern from cozystack `system/harbor/templates/database.yaml`: + +```yaml +--- +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: {{ .Release.Name }}-db +spec: + instances: {{ .Values.database.replicas }} + imageName: ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie + storage: + size: {{ .Values.database.size }} + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + bootstrap: + initdb: + database: app + owner: app + encoding: UTF8 + localeCollate: en_US.UTF-8 + localeCType: en_US.UTF-8 + monitoring: + enablePodMonitor: true + inheritedMetadata: + labels: + policy.cozystack.io/allow-to-apiserver: "true" +``` + +The CNPG operator automatically creates: +- Secret `{{ .Release.Name }}-db-app` with keys: `host`, `port`, `username`, `password`, `dbname`, `uri`, `jdbc-uri` +- Service `{{ .Release.Name }}-db-rw` (read-write primary) +- Service `{{ .Release.Name }}-db-r` (read replicas) +- Service `{{ .Release.Name }}-db-ro` (read-only replicas) + +### service.yaml (if app exposes a port) + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} +spec: + type: ClusterIP + ports: + - name: http + port: $APP_PORT + targetPort: http + selector: + app: {{ .Release.Name }} +``` + +### ingress.yaml (if user requested it) + +```yaml +{{- if .Values.host }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-ingress +spec: + rules: + - host: {{ .Values.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ .Release.Name }} + port: + name: http +{{- end }} +``` + +Use `AskUserQuestion` to confirm the generated templates before writing them. Show a summary of what will be created. + +## Phase 8 — Register in core/platform + +Update three files under `packages/core/platform/templates/`. Read each file first, then append. + +### namespaces.yaml + +If an operator was created (Phase 5), append: + +```yaml +--- +apiVersion: v1 +kind: Namespace +metadata: + labels: + cozystack.io/system: "true" + name: external-$APP_NAME-operator +``` + +### helmreleases.yaml + +If an operator was created, append a HelmRelease for it: + +```yaml +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: $APP_NAME-operator + namespace: external-$APP_NAME-operator +spec: + interval: 5m + targetNamespace: external-$APP_NAME-operator + chart: + spec: + chart: ./packages/system/$APP_NAME-operator + sourceRef: + kind: GitRepository + name: external-apps + namespace: cozy-public + version: '*' +``` + +Verify the `sourceRef.name` matches the GitRepository name in `init.yaml`. Read `init.yaml` to extract it: +```bash +yq -r '.metadata.name' $REPO_DIR/init.yaml | head -1 +``` + +If the app chart itself has a **Pattern A postgres dependency** and an operator is also present, add `spec.dependsOn` to the app's HelmRelease to ensure the operator is ready: + +```yaml +spec: + dependsOn: + - name: $APP_NAME-operator + namespace: external-$APP_NAME-operator +``` + +### cozyrds.yaml + +Append a `CozystackResourceDefinition` for the app. The `openAPISchema` must match `values.schema.json` content. + +```yaml +--- +apiVersion: cozystack.io/v1alpha1 +kind: CozystackResourceDefinition +metadata: + name: $APP_NAME + namespace: cozy-system +spec: + application: + kind: $APP_KIND + openAPISchema: | + + plural: $APP_PLURAL + singular: $APP_NAME + release: + chart: + name: ./packages/apps/$APP_NAME + sourceRef: + kind: GitRepository + name: $GIT_REPO_NAME + namespace: cozy-public + labels: + cozystack.io/ui: "true" + prefix: $APP_NAME- + dashboard: + category: $CATEGORY + singular: $APP_DISPLAY_NAME + plural: $APP_DISPLAY_NAME + description: $APP_DESCRIPTION + tags: + - $TAG1 + icon: $ICON_B64 +``` + +To compute `$ICON_B64`: +```bash +base64 < $REPO_DIR/packages/apps/$APP_NAME/logos/$APP_NAME.svg | tr -d '\n' +``` + +If `hack/update-crd.sh` exists and is functional, prefer running it instead of manually composing the CRD: +```bash +cd $REPO_DIR/packages/apps/$APP_NAME && make generate +``` + +This runs `cozyvalues-gen` (regenerates schema + README) and `hack/update-crd.sh` (updates the CozystackResourceDefinition with correct openAPISchema, icon, keysOrder). Check the output path — the script writes to `../../system/cozystack-resource-definitions/cozyrds/$APP_NAME.yaml` by default. If that directory does not exist (it won't in external-apps repos), the script may fail. In that case, manually compose the CRD entry and append it to `packages/core/platform/templates/cozyrds.yaml`. + +**Important**: read `hack/update-crd.sh` to check the `$OUT` variable default. If it points to a non-existent path, set `OUT` explicitly: +```bash +OUT=$REPO_DIR/packages/core/platform/templates/cozyrds-$APP_NAME.yaml \ + CRD_DIR="" \ + bash $REPO_DIR/hack/update-crd.sh +``` + +Or simply append the generated YAML block to `cozyrds.yaml` manually. + +Use `AskUserQuestion` to confirm all core/platform changes before writing. Show the diff of what will be appended to each file. + +## Phase 9 — Validation + +Run the following checks: + +1. **Generate schema and README:** + ```bash + cd $REPO_DIR/packages/apps/$APP_NAME && make generate + ``` + +2. **Helm template render:** + ```bash + cd $REPO_DIR/packages/apps/$APP_NAME && helm template test . + ``` + Fix any template errors before proceeding. + +3. **JSON schema validity:** + ```bash + jq . $REPO_DIR/packages/apps/$APP_NAME/values.schema.json > /dev/null + ``` + +4. **YAML validity of cozyrds:** + ```bash + yq e '.' $REPO_DIR/packages/core/platform/templates/cozyrds.yaml > /dev/null + ``` + +5. **Platform chart render:** + ```bash + cd $REPO_DIR/packages/core/platform && helm template test . + ``` + +If any check fails, fix the issue and re-run. If the fix is not obvious, stop and report the error to the user. + +## Phase 10 — Summary + +Print a report: + +- **App name**: `$APP_NAME` +- **Files created** (list all new files with paths relative to repo root) +- **Files modified** (list all modified files: `cozyrds.yaml`, `helmreleases.yaml`, `namespaces.yaml`) +- **Dependencies**: for each dependency, state the chosen pattern (A: managed / B: external) and which secrets the app consumes +- **Operator**: created or not, chart source +- **Dashboard**: category, tags, icon status (present / missing) +- **Next steps for the user**: + 1. Review all generated files + 2. Place icon SVG if not yet provided: `packages/apps/$APP_NAME/logos/$APP_NAME.svg` + 3. Run `cd packages/apps/$APP_NAME && make generate` to regenerate CRD after any values.yaml changes + 4. Commit and push — Flux picks up changes via the GitRepository defined in `init.yaml` (default interval: 1m) + 5. Verify in cluster: `kubectl get $APP_PLURAL.$APP_NAME.apps.cozystack.io --all-namespaces` + +## Guardrails + +- **Never** commit or push on behalf of the user. This is a generate-only skill. +- **Never** apply anything to a cluster — no `kubectl apply`, no `helm install`, no `make apply`. This skill only creates files. +- **Never** overwrite existing `packages/apps/$APP_NAME/` without explicit user confirmation. +- **Never** guess CNPG secret names or key names. The verified convention is: Cluster `-db` → Secret `-db-app` with keys `host`, `port`, `username`, `password`, `dbname`. If the user's scenario differs (e.g., custom bootstrap, non-standard secret names), stop and ask. +- **Never** edit files in a cozystack checkout used as reference — those are read-only. +- **Never** modify `init.yaml` — the user manages their GitRepository and root HelmRelease manually. +- **Always** use `AskUserQuestion` before creating files in Phase 6, 7, and 8. Show what will be created. +- **Always** read existing files before appending to them (cozyrds.yaml, helmreleases.yaml, namespaces.yaml). +- If `cozyvalues-gen` is not installed, do not attempt to generate schema/README manually beyond a minimal placeholder. Tell the user to install it and re-run `make generate`. +- If `hack/update-crd.sh` output path does not exist, handle gracefully — generate the CRD inline rather than failing silently. + +## References + +Read these files on demand when reasoning about structure and conventions: + +- `packages/core/platform/templates/cozyrds.yaml` — existing CozystackResourceDefinition entries, structure reference +- `packages/core/platform/templates/helmreleases.yaml` — existing HelmRelease entries for operators +- `packages/core/platform/templates/namespaces.yaml` — existing namespace entries +- `hack/update-crd.sh` — how icon base64 encoding, openAPISchema injection, and keysOrder generation work +- `scripts/package.mk` — make targets: `show`, `apply`, `diff`, `suspend`, `resume`, `delete` +- `init.yaml` — GitRepository name and root HelmRelease (needed for sourceRef in CRD and HelmRelease) +- Cozystack external apps docs: https://cozystack.io/docs/applications/external/ +- Flux HelmRelease spec (dependsOn): https://fluxcd.io/flux/components/helm/helmreleases/ +- CloudNativePG Cluster CRD: https://cloudnative-pg.io/documentation/current/cloudnative-pg.v1/ +- CNPG bootstrap initdb: https://cloudnative-pg.io/documentation/current/bootstrap/#initdb +- `cozystack/packages/apps/postgres/values.yaml` — reference for cozyvalues-gen annotation style (`@param`, `@typedef`, `@field`, `@enum`, `@section`) +- `cozystack/packages/system/harbor/templates/database.yaml` — reference Pattern A: managed CNPG Cluster in chart templates +- `cozystack/packages/system/keycloak/templates/sts.yaml` (lines 142-168) — reference Pattern A: consuming CNPG secret via secretKeyRef with keys `host`, `port`, `username`, `password`, `dbname` +- `cozystack/packages/apps/harbor/templates/harbor.yaml` (lines 132-141) — reference Pattern A: database connection config with `existingSecret` and `host` pointing to CNPG `-rw` service From 223ccd51fac7d36ace2d249ed9964a247f17ab67 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 14:18:17 +0300 Subject: [PATCH 02/30] fix(cozy-external-app): collect dashboard and resource metadata in phase 3 Phase 8 renders a CozystackResourceDefinition that requires display name, description, category, tags, Kind, and Plural. Phase 3 previously collected none of these, forcing the skill to improvise in a later phase. Co-Authored-By: Claude Signed-off-by: ZverGuy --- skills/cozy-external-app/skills/cozy-external-app/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 24d2282..a027dc3 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -47,6 +47,8 @@ Use `AskUserQuestion` to collect: 3. **Public port**: does the app expose an HTTP port? If yes, which port number? Should an Ingress template be generated? 4. **Persistent storage**: does the app need a PVC? If yes, default size (e.g., `10Gi`). 5. **Icon**: path to an SVG file for the dashboard. If not available yet, note it — Phase 6 will create a `logos/` placeholder and Phase 9 will fail until the user provides one. +6. **Dashboard metadata**: Display Name (e.g., `Immich`), Description (e.g., `Self-hosted photo and video management solution`), Category (e.g., `Media`), and Tags (comma-separated list, e.g., `photo, video`). +7. **Resource definition**: Kind (e.g., `Immich`) and Plural (e.g., `immichs`) for the `CozystackResourceDefinition` created in Phase 8. Record all answers. Proceed only after user confirms the summary. From 1e0c2897f7caa97056dc7c54f546548ffc38cef4 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 14:18:28 +0300 Subject: [PATCH 03/30] fix(cozy-external-app): collect operator chart name and version in phase 5 The generated Makefile interpolates $OPERATOR_CHART_NAME and $OPERATOR_CHART_VERSION when pulling the operator chart, but the skill never asked the user for them. Add an explicit AskUserQuestion step so the variables are populated before the Makefile is written. Co-Authored-By: Claude Signed-off-by: ZverGuy --- skills/cozy-external-app/skills/cozy-external-app/SKILL.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index a027dc3..5db463c 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -99,7 +99,12 @@ Present a summary of all dependencies with chosen patterns. Proceed only after u Skip if `--operator` was not passed and the app does not need a custom operator. -If an operator is needed, create `packages/system/$APP_NAME-operator/`: +If an operator is needed, use `AskUserQuestion` to collect, from the repository at `$OPERATOR_REPO_URL`: + +- `$OPERATOR_CHART_NAME` — chart name inside the repository (e.g., `immich`). +- `$OPERATOR_CHART_VERSION` — pinned chart version (e.g., `0.9.4`). Never use `latest` — the Makefile requires a fixed version for reproducible builds. + +Then create `packages/system/$APP_NAME-operator/`: ```bash mkdir -p $REPO_DIR/packages/system/$APP_NAME-operator/charts From 6cae7a34a68edacd3cc9e959c49149026df0cf3f Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 14:18:43 +0300 Subject: [PATCH 04/30] fix(cozy-external-app): add Flux HelmRelease wrapper template example Phase 7 instructs the skill to wrap upstream charts in a Flux HelmRelease but only showed a direct Deployment example. Add an explicit HelmRelease template so the upstream chart case has a concrete shape to follow. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 5db463c..a3fdce1 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -262,6 +262,31 @@ Create `packages/apps/$APP_NAME/templates/` with the following files. Generate the primary workload template. If the user chose "upstream Helm chart", wrap it in a Flux HelmRelease (like harbor does). If "custom templates", create a Deployment or StatefulSet directly. +**For a Flux HelmRelease wrapper example (upstream chart case):** + +```yaml +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: {{ .Release.Name }} +spec: + interval: 1h + chart: + spec: + chart: $UPSTREAM_CHART_NAME + version: $UPSTREAM_CHART_VERSION + sourceRef: + kind: HelmRepository + name: $UPSTREAM_REPO_NAME + namespace: cozy-public + values: + # Map .Values.* to the upstream chart's value schema here. + # Pull credentials from the CNPG secret (Pattern A) or from + # .Values.postgres.* (Pattern B) as established in Phase 4. +``` + +The referenced `HelmRepository` resource must exist in the cluster. If it does not, register it in Phase 8 alongside the other platform resources. + **For a direct Deployment example:** ```yaml From 47d010454654d153a4281e9877cf2a554ab0b7a6 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 14:19:02 +0300 Subject: [PATCH 05/30] fix(cozy-external-app): thread $GIT_REPO_NAME through phase 8 sourceRefs The operator HelmRelease template hardcoded sourceRef.name to "external-apps" while the CozystackResourceDefinition already used $GIT_REPO_NAME. Extract the value from init.yaml once at the top of phase 8 and reuse it in both templates so the skill works for users who renamed their repository. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index a3fdce1..a512231 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -449,6 +449,20 @@ Use `AskUserQuestion` to confirm the generated templates before writing them. Sh Update three files under `packages/core/platform/templates/`. Read each file first, then append. +Before generating any YAML in this phase, extract the GitRepository name from `init.yaml` — it is referenced as `sourceRef.name` in both the operator HelmRelease and the CozystackResourceDefinition below: + +```bash +GIT_REPO_NAME=$(yq -r '.metadata.name' $REPO_DIR/init.yaml | head -1) +``` + +If `init.yaml` contains multiple documents, pick the `GitRepository` kind explicitly: + +```bash +GIT_REPO_NAME=$(yq -r 'select(.kind == "GitRepository") | .metadata.name' $REPO_DIR/init.yaml | head -1) +``` + +Stop and ask the user if the extracted value is empty. + ### namespaces.yaml If an operator was created (Phase 5), append: @@ -482,16 +496,11 @@ spec: chart: ./packages/system/$APP_NAME-operator sourceRef: kind: GitRepository - name: external-apps + name: $GIT_REPO_NAME namespace: cozy-public version: '*' ``` -Verify the `sourceRef.name` matches the GitRepository name in `init.yaml`. Read `init.yaml` to extract it: -```bash -yq -r '.metadata.name' $REPO_DIR/init.yaml | head -1 -``` - If the app chart itself has a **Pattern A postgres dependency** and an operator is also present, add `spec.dependsOn` to the app's HelmRelease to ensure the operator is ready: ```yaml From 6d7c7289c5882f59a6d3c75f6c092544e9bcb245 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 14:19:32 +0300 Subject: [PATCH 06/30] fix(cozy-external-app): document openAPISchema indentation requirement The CozystackResourceDefinition embeds values.schema.json under a YAML literal block scalar. Without indenting the JSON by six spaces the YAML parser silently absorbs the schema as plain text. Call out the rule and provide a sed one-liner for the manual path. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index a512231..8d7924a 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -525,7 +525,7 @@ spec: application: kind: $APP_KIND openAPISchema: | - + plural: $APP_PLURAL singular: $APP_NAME release: @@ -553,6 +553,14 @@ To compute `$ICON_B64`: base64 < $REPO_DIR/packages/apps/$APP_NAME/logos/$APP_NAME.svg | tr -d '\n' ``` +To produce the correctly indented `openAPISchema` block when composing the CRD inline, prefix every line of `values.schema.json` with six spaces so the JSON becomes a valid child of the `|` literal scalar: + +```bash +sed 's/^/ /' $REPO_DIR/packages/apps/$APP_NAME/values.schema.json +``` + +Verify the final YAML with `yq e '.' cozyrds.yaml > /dev/null` before moving on — an off-by-one indentation silently breaks the schema. + If `hack/update-crd.sh` exists and is functional, prefer running it instead of manually composing the CRD: ```bash cd $REPO_DIR/packages/apps/$APP_NAME && make generate From c34cdfd06d3ff53ae3cc21000fa909d3f46eb95c Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:13:17 +0300 Subject: [PATCH 07/30] fix(cozy-external-app): rename to ApplicationDefinition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CRD kind CozystackResourceDefinition does not exist in Cozystack v1.x — it was renamed to ApplicationDefinition. The skill, plugin metadata, and marketplace entry all referenced the stale name, so every generated manifest would have been rejected by the API server. Switch to the current kind everywhere. The filename cozyrds.yaml is kept unchanged, matching upstream convention in external-apps-example. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .claude-plugin/marketplace.json | 2 +- .../cozy-external-app/.claude-plugin/plugin.json | 2 +- .../skills/cozy-external-app/SKILL.md | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 088abe1..613f8bd 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -15,7 +15,7 @@ }, { "name": "cozy-external-app", - "description": "Scaffold a new Cozystack external app package — generates chart skeleton, CozystackResourceDefinition, and handles dependency integration (e.g. Immich → Postgres) via managed CNPG clusters or external secret references", + "description": "Scaffold a new Cozystack external app package — generates chart skeleton, ApplicationDefinition, and handles dependency integration (e.g. Immich → Postgres) via managed CNPG clusters or external secret references", "source": "./skills/cozy-external-app", "category": "infrastructure" } diff --git a/skills/cozy-external-app/.claude-plugin/plugin.json b/skills/cozy-external-app/.claude-plugin/plugin.json index 5ce2f71..b045f07 100644 --- a/skills/cozy-external-app/.claude-plugin/plugin.json +++ b/skills/cozy-external-app/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "cozy-external-app", "version": "1.0.0", - "description": "Scaffold a new Cozystack external app package — generates chart skeleton, CozystackResourceDefinition, and handles dependency integration (e.g. Immich → Postgres) via managed CNPG clusters or external secret references", + "description": "Scaffold a new Cozystack external app package — generates chart skeleton, ApplicationDefinition, and handles dependency integration (e.g. Immich → Postgres) via managed CNPG clusters or external secret references", "author": { "name": "Cozystack", "url": "https://github.com/cozystack" diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 8d7924a..fa5e4d8 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -1,12 +1,12 @@ --- name: cozy-external-app -description: Scaffold a new Cozystack external app package inside an external-apps repository. Generates the full chart skeleton (Chart.yaml, Makefile, values.yaml with cozyvalues-gen annotations, templates), registers it in core/platform (namespace, HelmRelease, CozystackResourceDefinition), and wires dependency integration — supports managed CNPG Postgres clusters provisioned in-chart and external secret references for pre-existing services. Use when adding a new application (e.g. Immich, Gitea, Nextcloud) to an external-apps repo that follows the cozystack/external-apps-example layout. +description: Scaffold a new Cozystack external app package inside an external-apps repository. Generates the full chart skeleton (Chart.yaml, Makefile, values.yaml with cozyvalues-gen annotations, templates), registers it in core/platform (namespace, HelmRepository, HelmChart, HelmRelease, ApplicationDefinition), and wires dependency integration — supports managed CNPG Postgres clusters provisioned in-chart and external secret references for pre-existing services. Use when adding a new application (e.g. Immich, Gitea, Nextcloud) to an external-apps repo that follows the cozystack/external-apps-example layout. argument-hint: " [--depends-on=postgres,redis] [--operator=] [--repo-dir=]" --- # cozy-external-app -This skill scaffolds a new Cozystack external app package. It creates all files needed for the app to appear in the Cozystack dashboard and be deployable via the GitOps pipeline (GitRepository → Flux HelmRelease → CozystackResourceDefinition). +This skill scaffolds a new Cozystack external app package. It creates all files needed for the app to appear in the Cozystack dashboard and be deployable via the GitOps pipeline (GitRepository → Flux HelmRelease → ApplicationDefinition). This is a **generate-only** skill. It never applies anything to a cluster, never commits, and never pushes. The user handles git operations themselves. @@ -48,7 +48,7 @@ Use `AskUserQuestion` to collect: 4. **Persistent storage**: does the app need a PVC? If yes, default size (e.g., `10Gi`). 5. **Icon**: path to an SVG file for the dashboard. If not available yet, note it — Phase 6 will create a `logos/` placeholder and Phase 9 will fail until the user provides one. 6. **Dashboard metadata**: Display Name (e.g., `Immich`), Description (e.g., `Self-hosted photo and video management solution`), Category (e.g., `Media`), and Tags (comma-separated list, e.g., `photo, video`). -7. **Resource definition**: Kind (e.g., `Immich`) and Plural (e.g., `immichs`) for the `CozystackResourceDefinition` created in Phase 8. +7. **Resource definition**: Kind (e.g., `Immich`) and Plural (e.g., `immichs`) for the `ApplicationDefinition` created in Phase 8. Record all answers. Proceed only after user confirms the summary. @@ -449,7 +449,7 @@ Use `AskUserQuestion` to confirm the generated templates before writing them. Sh Update three files under `packages/core/platform/templates/`. Read each file first, then append. -Before generating any YAML in this phase, extract the GitRepository name from `init.yaml` — it is referenced as `sourceRef.name` in both the operator HelmRelease and the CozystackResourceDefinition below: +Before generating any YAML in this phase, extract the GitRepository name from `init.yaml` — it is referenced as `sourceRef.name` in both the operator HelmRelease and the ApplicationDefinition below: ```bash GIT_REPO_NAME=$(yq -r '.metadata.name' $REPO_DIR/init.yaml | head -1) @@ -512,12 +512,12 @@ spec: ### cozyrds.yaml -Append a `CozystackResourceDefinition` for the app. The `openAPISchema` must match `values.schema.json` content. +Append a `ApplicationDefinition` for the app. The `openAPISchema` must match `values.schema.json` content. ```yaml --- apiVersion: cozystack.io/v1alpha1 -kind: CozystackResourceDefinition +kind: ApplicationDefinition metadata: name: $APP_NAME namespace: cozy-system @@ -566,7 +566,7 @@ If `hack/update-crd.sh` exists and is functional, prefer running it instead of m cd $REPO_DIR/packages/apps/$APP_NAME && make generate ``` -This runs `cozyvalues-gen` (regenerates schema + README) and `hack/update-crd.sh` (updates the CozystackResourceDefinition with correct openAPISchema, icon, keysOrder). Check the output path — the script writes to `../../system/cozystack-resource-definitions/cozyrds/$APP_NAME.yaml` by default. If that directory does not exist (it won't in external-apps repos), the script may fail. In that case, manually compose the CRD entry and append it to `packages/core/platform/templates/cozyrds.yaml`. +This runs `cozyvalues-gen` (regenerates schema + README) and `hack/update-crd.sh` (updates the ApplicationDefinition with correct openAPISchema, icon, keysOrder). Check the output path — the script writes to `../../system/cozystack-resource-definitions/cozyrds/$APP_NAME.yaml` by default. If that directory does not exist (it won't in external-apps repos), the script may fail. In that case, manually compose the CRD entry and append it to `packages/core/platform/templates/cozyrds.yaml`. **Important**: read `hack/update-crd.sh` to check the `$OUT` variable default. If it points to a non-existent path, set `OUT` explicitly: ```bash @@ -645,7 +645,7 @@ Print a report: Read these files on demand when reasoning about structure and conventions: -- `packages/core/platform/templates/cozyrds.yaml` — existing CozystackResourceDefinition entries, structure reference +- `packages/core/platform/templates/cozyrds.yaml` — existing ApplicationDefinition entries, structure reference - `packages/core/platform/templates/helmreleases.yaml` — existing HelmRelease entries for operators - `packages/core/platform/templates/namespaces.yaml` — existing namespace entries - `hack/update-crd.sh` — how icon base64 encoding, openAPISchema injection, and keysOrder generation work From 377d5c7260e12d45652d450602d4a21ab35ba2a6 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:13:40 +0300 Subject: [PATCH 08/30] fix(cozy-external-app): correct ApplicationDefinition shape Three schema-level issues made the generated manifest invalid: metadata.namespace was set on a cluster-scoped kind; release.chart.sourceRef does not exist in the CRD (required field is release.chartRef with kind HelmChart/ExternalArtifact/OCIRepository); keysOrder was missing and the dashboard UI would render fields in arbitrary order. Align the template with the CRD schema and the minecraft-server reference in external-apps-example. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index fa5e4d8..b477a58 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -512,7 +512,9 @@ spec: ### cozyrds.yaml -Append a `ApplicationDefinition` for the app. The `openAPISchema` must match `values.schema.json` content. +Append an `ApplicationDefinition` for the app. The `openAPISchema` must match `values.schema.json` content. + +`ApplicationDefinition` is cluster-scoped (`scope: Cluster` in the CRD), so `metadata.namespace` must be omitted. The `release.chartRef` field references the flux `HelmChart` defined in `helmcharts.yaml` above — both must use the same name and namespace (`cozy-public`). ```yaml --- @@ -520,7 +522,6 @@ apiVersion: cozystack.io/v1alpha1 kind: ApplicationDefinition metadata: name: $APP_NAME - namespace: cozy-system spec: application: kind: $APP_KIND @@ -529,12 +530,10 @@ spec: plural: $APP_PLURAL singular: $APP_NAME release: - chart: - name: ./packages/apps/$APP_NAME - sourceRef: - kind: GitRepository - name: $GIT_REPO_NAME - namespace: cozy-public + chartRef: + kind: HelmChart + name: $GIT_REPO_NAME-$APP_NAME + namespace: cozy-public labels: cozystack.io/ui: "true" prefix: $APP_NAME- @@ -546,6 +545,18 @@ spec: tags: - $TAG1 icon: $ICON_B64 + keysOrder: + - - apiVersion + - - kind + - - metadata + - - metadata + - name + # Append one entry per top-level key in values.yaml, in the order the + # user should see them in the dashboard form. Example: + # - - spec + # - host + # - - spec + # - size ``` To compute `$ICON_B64`: From 89c4b9344371e79749e566b37c0c269351254169 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:14:20 +0300 Subject: [PATCH 09/30] fix(cozy-external-app): operator via flux HelmRepository, not local package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reference external-apps layout registers operator charts via a flux HelmRepository (typically oci://) and references them from the HelmRelease with sourceRef.kind: HelmRepository. The skill instead created a stub packages/system/-operator/ chart with a make update target that pre-pulled the upstream into charts/ — a pattern that diverges from the reference and leaves the HelmRelease pointing at an empty stub Chart.yaml. Rewrite Phase 5 to gather operator specification only (type, URL, chart name, version) and switch the Phase 8 operator HelmRelease template to sourceRef.kind: HelmRepository. Also fix the misplaced dependsOn paragraph: the application HelmRelease is controller-generated from the ApplicationDefinition and is not user-authored, so dependsOn only makes sense on the operator HelmRelease. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 67 +++++-------------- 1 file changed, 16 insertions(+), 51 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index b477a58..7517402 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -95,48 +95,20 @@ Collect: Present a summary of all dependencies with chosen patterns. Proceed only after user confirms. -## Phase 5 — Create operator chart (conditional) +## Phase 5 — Gather operator specification (conditional) Skip if `--operator` was not passed and the app does not need a custom operator. -If an operator is needed, use `AskUserQuestion` to collect, from the repository at `$OPERATOR_REPO_URL`: +The reference layout (`external-apps-example`) pulls operator charts at reconcile time via a flux `HelmRepository` — it does not vendor a local package in `packages/system/`. This phase gathers the details needed to register the `HelmRepository` and operator `HelmRelease` in Phase 8. -- `$OPERATOR_CHART_NAME` — chart name inside the repository (e.g., `immich`). -- `$OPERATOR_CHART_VERSION` — pinned chart version (e.g., `0.9.4`). Never use `latest` — the Makefile requires a fixed version for reproducible builds. - -Then create `packages/system/$APP_NAME-operator/`: - -```bash -mkdir -p $REPO_DIR/packages/system/$APP_NAME-operator/charts -``` - -Create `Chart.yaml`: -```yaml -apiVersion: v2 -name: external-$APP_NAME-operator -version: 0.0.0 -``` - -Create `Makefile`: -```makefile -export NAME=$APP_NAME-operator -export NAMESPACE=external-$(NAME) - -include ../../../scripts/package.mk - -update: - rm -rf charts - helm repo add $APP_NAME $OPERATOR_REPO_URL - helm repo update $APP_NAME - helm pull $APP_NAME/$OPERATOR_CHART_NAME --untar --untardir charts --version "$OPERATOR_CHART_VERSION" -``` +Use `AskUserQuestion` to collect: -Run the update target to pull the operator chart: -```bash -cd $REPO_DIR/packages/system/$APP_NAME-operator && make update -``` +- `$OPERATOR_REPO_TYPE` — `oci` if `$OPERATOR_REPO_URL` starts with `oci://`, otherwise leave empty (standard HTTPS Helm repo). +- `$OPERATOR_CHART_NAME` — chart name inside the repository (e.g., `minecraft-operator`, `immich`). +- `$OPERATOR_CHART_VERSION` — pinned chart version or semver range (e.g., `0.9.4`, `>=1.0.0`). Avoid `'*'` in production use. +- `$OPERATOR_REPO_NAME` — local alias used as `HelmRepository.metadata.name` and referenced by the HelmRelease. Default: `$APP_NAME-operator`. -Use `AskUserQuestion` to confirm the operator chart was pulled successfully before proceeding. +No files are created in this phase. All operator resources are written in Phase 8 alongside the other platform resources. ## Phase 6 — Create app chart skeleton @@ -479,36 +451,29 @@ metadata: ### helmreleases.yaml -If an operator was created, append a HelmRelease for it: +If an operator was gathered in Phase 5, append a HelmRelease that pulls it from the `HelmRepository` registered above (same namespace, same name alias): ```yaml --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: - name: $APP_NAME-operator + name: $OPERATOR_REPO_NAME namespace: external-$APP_NAME-operator spec: interval: 5m + releaseName: $OPERATOR_REPO_NAME targetNamespace: external-$APP_NAME-operator chart: spec: - chart: ./packages/system/$APP_NAME-operator + chart: $OPERATOR_CHART_NAME sourceRef: - kind: GitRepository - name: $GIT_REPO_NAME - namespace: cozy-public - version: '*' + kind: HelmRepository + name: $OPERATOR_REPO_NAME + version: '$OPERATOR_CHART_VERSION' ``` -If the app chart itself has a **Pattern A postgres dependency** and an operator is also present, add `spec.dependsOn` to the app's HelmRelease to ensure the operator is ready: - -```yaml -spec: - dependsOn: - - name: $APP_NAME-operator - namespace: external-$APP_NAME-operator -``` +If the operator must wait on another operator (e.g., CNPG) before reconciling, add `spec.dependsOn` here — dependency ordering belongs on user-authored HelmReleases. The application's own HelmRelease is generated by the Cozystack controller from the `ApplicationDefinition` and is not user-writable. ### cozyrds.yaml From 8d49dad81a9a25315b39b99a7e12219f01485923 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:15:07 +0300 Subject: [PATCH 10/30] fix(cozy-external-app): register HelmRepository and HelmChart in phase 8 The reference layout has five platform template files, not three. helmcharts.yaml must have one entry per app (the source referenced by ApplicationDefinition.spec.release.chartRef), and helmrepositories.yaml must have one entry per operator chart. Without these the chartRef has no source and flux cannot produce the chart artifact. Add both subsections to phase 8, expand the phase 2 pre-flight to assert all five files, and update summary/guardrails to match. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 7517402..879e687 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -29,7 +29,7 @@ If `` is missing, use `AskUserQuestion` to ask for it. Bail early if any check fails. -1. **Repository structure**: verify `$REPO_DIR` contains `init.yaml`, `packages/core/platform/Chart.yaml`, and `scripts/package.mk`. If not, tell the user to `cd` into the external-apps repo root or pass `--repo-dir`. +1. **Repository structure**: verify `$REPO_DIR` contains `init.yaml`, `packages/core/platform/Chart.yaml`, `scripts/package.mk`, and the five platform template files `packages/core/platform/templates/{namespaces,helmrepositories,helmreleases,helmcharts,cozyrds}.yaml`. If any are missing, tell the user to `cd` into the external-apps repo root or pass `--repo-dir` — Phase 8 appends to all five. 2. **Tools installed**: check that `yq` (v4), `jq`, `base64`, `helm`, and `cozyvalues-gen` are available via `command -v`. If `cozyvalues-gen` is missing, print: ```text cozyvalues-gen is required. Install it from: @@ -419,7 +419,7 @@ Use `AskUserQuestion` to confirm the generated templates before writing them. Sh ## Phase 8 — Register in core/platform -Update three files under `packages/core/platform/templates/`. Read each file first, then append. +Update five files under `packages/core/platform/templates/`. Read each file first, then append. Before generating any YAML in this phase, extract the GitRepository name from `init.yaml` — it is referenced as `sourceRef.name` in both the operator HelmRelease and the ApplicationDefinition below: @@ -437,7 +437,7 @@ Stop and ask the user if the extracted value is empty. ### namespaces.yaml -If an operator was created (Phase 5), append: +If an operator was gathered in Phase 5, append the operator namespace: ```yaml --- @@ -449,6 +449,26 @@ metadata: name: external-$APP_NAME-operator ``` +### helmrepositories.yaml + +If an operator was gathered in Phase 5, append a `HelmRepository` entry in the operator namespace. The HelmRelease below references it by name without a namespace field, so both must live in the same namespace: + +```yaml +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: $OPERATOR_REPO_NAME + namespace: external-$APP_NAME-operator +spec: + interval: 5m + url: $OPERATOR_REPO_URL + # Uncomment for OCI registries (e.g., ghcr.io): + # type: oci +``` + +If `$OPERATOR_REPO_TYPE` is `oci`, set `spec.type: oci` explicitly — standard HTTPS Helm repos omit the field. + ### helmreleases.yaml If an operator was gathered in Phase 5, append a HelmRelease that pulls it from the `HelmRepository` registered above (same namespace, same name alias): @@ -475,6 +495,28 @@ spec: If the operator must wait on another operator (e.g., CNPG) before reconciling, add `spec.dependsOn` here — dependency ordering belongs on user-authored HelmReleases. The application's own HelmRelease is generated by the Cozystack controller from the `ApplicationDefinition` and is not user-writable. +### helmcharts.yaml + +Always append a flux `HelmChart` that backs the `ApplicationDefinition.spec.release.chartRef` below. Without this entry the `chartRef` has no source and flux cannot produce the chart artifact: + +```yaml +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmChart +metadata: + name: $GIT_REPO_NAME-$APP_NAME + namespace: cozy-public +spec: + interval: 5m + chart: ./packages/apps/$APP_NAME + sourceRef: + kind: GitRepository + name: $GIT_REPO_NAME + reconcileStrategy: Revision +``` + +The `metadata.name` here must match the `release.chartRef.name` in the `ApplicationDefinition` below, and the `metadata.namespace` must match `release.chartRef.namespace` (`cozy-public`, same as the GitRepository in `init.yaml`). + ### cozyrds.yaml Append an `ApplicationDefinition` for the app. The `openAPISchema` must match `values.schema.json` content. @@ -593,7 +635,7 @@ Print a report: - **App name**: `$APP_NAME` - **Files created** (list all new files with paths relative to repo root) -- **Files modified** (list all modified files: `cozyrds.yaml`, `helmreleases.yaml`, `namespaces.yaml`) +- **Files modified** (list all modified files under `packages/core/platform/templates/`: `namespaces.yaml`, `helmrepositories.yaml`, `helmreleases.yaml`, `helmcharts.yaml`, `cozyrds.yaml`) - **Dependencies**: for each dependency, state the chosen pattern (A: managed / B: external) and which secrets the app consumes - **Operator**: created or not, chart source - **Dashboard**: category, tags, icon status (present / missing) @@ -613,7 +655,7 @@ Print a report: - **Never** edit files in a cozystack checkout used as reference — those are read-only. - **Never** modify `init.yaml` — the user manages their GitRepository and root HelmRelease manually. - **Always** use `AskUserQuestion` before creating files in Phase 6, 7, and 8. Show what will be created. -- **Always** read existing files before appending to them (cozyrds.yaml, helmreleases.yaml, namespaces.yaml). +- **Always** read existing files before appending to them (`namespaces.yaml`, `helmrepositories.yaml`, `helmreleases.yaml`, `helmcharts.yaml`, `cozyrds.yaml`). - If `cozyvalues-gen` is not installed, do not attempt to generate schema/README manually beyond a minimal placeholder. Tell the user to install it and re-run `make generate`. - If `hack/update-crd.sh` output path does not exist, handle gracefully — generate the CRD inline rather than failing silently. From 48288bd4c9aaeeb73a5166bc150329b998ff2107 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:15:58 +0300 Subject: [PATCH 11/30] fix(cozy-external-app): generated Makefile runs and matches reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two problems made the generated app Makefile unusable against external-apps repos. It lacked export NAME and export NAMESPACE, so every downstream target inherited from scripts/package.mk failed with "env NAME is not set!". And it invoked ../../../hack/update-crd.sh, a script that only exists in the cozystack monorepo — not in external-apps-example. Add the required exports, drop the hack/update-crd.sh call, switch cozyvalues-gen to its long flags, and strip the phase 8 paragraphs that promoted a script users do not have. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 879e687..30bd557 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -128,14 +128,20 @@ icon: /logos/$APP_NAME.svg ### Makefile +The generated Makefile must export `NAME` and `NAMESPACE` — `scripts/package.mk` has a `check:` target (a dependency of `apply`, `show`, `diff`, `delete`, `suspend`, `resume`) that exits with `env NAME is not set!` when either is empty. `$NAMESPACE` should be the operator namespace when the app depends on one, otherwise use `cozy-system`. + ```makefile +export NAME=$APP_NAME +export NAMESPACE= + include ../../../scripts/package.mk generate: - cozyvalues-gen -v values.yaml -s values.schema.json -r README.md - ../../../hack/update-crd.sh + cozyvalues-gen --values values.yaml --schema values.schema.json --readme README.md ``` +The reference `external-apps-example` repo does not ship `hack/update-crd.sh` — that script lives only in the cozystack monorepo. Do not call it from the generated Makefile. The `ApplicationDefinition` entry in `cozyrds.yaml` is composed by hand in Phase 8. + ### logos/$APP_NAME.svg If the user provided an icon path, copy it: @@ -579,22 +585,6 @@ sed 's/^/ /' $REPO_DIR/packages/apps/$APP_NAME/values.schema.json Verify the final YAML with `yq e '.' cozyrds.yaml > /dev/null` before moving on — an off-by-one indentation silently breaks the schema. -If `hack/update-crd.sh` exists and is functional, prefer running it instead of manually composing the CRD: -```bash -cd $REPO_DIR/packages/apps/$APP_NAME && make generate -``` - -This runs `cozyvalues-gen` (regenerates schema + README) and `hack/update-crd.sh` (updates the ApplicationDefinition with correct openAPISchema, icon, keysOrder). Check the output path — the script writes to `../../system/cozystack-resource-definitions/cozyrds/$APP_NAME.yaml` by default. If that directory does not exist (it won't in external-apps repos), the script may fail. In that case, manually compose the CRD entry and append it to `packages/core/platform/templates/cozyrds.yaml`. - -**Important**: read `hack/update-crd.sh` to check the `$OUT` variable default. If it points to a non-existent path, set `OUT` explicitly: -```bash -OUT=$REPO_DIR/packages/core/platform/templates/cozyrds-$APP_NAME.yaml \ - CRD_DIR="" \ - bash $REPO_DIR/hack/update-crd.sh -``` - -Or simply append the generated YAML block to `cozyrds.yaml` manually. - Use `AskUserQuestion` to confirm all core/platform changes before writing. Show the diff of what will be appended to each file. ## Phase 9 — Validation @@ -657,7 +647,6 @@ Print a report: - **Always** use `AskUserQuestion` before creating files in Phase 6, 7, and 8. Show what will be created. - **Always** read existing files before appending to them (`namespaces.yaml`, `helmrepositories.yaml`, `helmreleases.yaml`, `helmcharts.yaml`, `cozyrds.yaml`). - If `cozyvalues-gen` is not installed, do not attempt to generate schema/README manually beyond a minimal placeholder. Tell the user to install it and re-run `make generate`. -- If `hack/update-crd.sh` output path does not exist, handle gracefully — generate the CRD inline rather than failing silently. ## References @@ -665,9 +654,10 @@ Read these files on demand when reasoning about structure and conventions: - `packages/core/platform/templates/cozyrds.yaml` — existing ApplicationDefinition entries, structure reference - `packages/core/platform/templates/helmreleases.yaml` — existing HelmRelease entries for operators +- `packages/core/platform/templates/helmrepositories.yaml` — existing HelmRepository entries for operator chart sources +- `packages/core/platform/templates/helmcharts.yaml` — existing HelmChart entries that back each app's `release.chartRef` - `packages/core/platform/templates/namespaces.yaml` — existing namespace entries -- `hack/update-crd.sh` — how icon base64 encoding, openAPISchema injection, and keysOrder generation work -- `scripts/package.mk` — make targets: `show`, `apply`, `diff`, `suspend`, `resume`, `delete` +- `scripts/package.mk` — make targets: `show`, `apply`, `diff`, `suspend`, `resume`, `delete`. Requires `NAME` and `NAMESPACE` exports. - `init.yaml` — GitRepository name and root HelmRelease (needed for sourceRef in CRD and HelmRelease) - Cozystack external apps docs: https://cozystack.io/docs/applications/external/ - Flux HelmRelease spec (dependsOn): https://fluxcd.io/flux/components/helm/helmreleases/ From 7620113349fb91cc07b6e194d7e20c40e3cd8222 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:21:45 +0300 Subject: [PATCH 12/30] feat(cozy-external-app): add dependency catalog with verified patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 previously hardcoded only the CNPG/postgres pattern. A redis or mongodb dependency would have left the assistant to improvise CR kinds, secret naming, and credential keys — a guaranteed source of silent wiring bugs. Introduce a dependency catalog documenting four common patterns verified against the cozystack monorepo (CNPG for postgres, Spotahome RedisFailover for redis, Percona PSMDB for mongodb, Strimzi for kafka) plus a research procedure for anything not listed. The catalog explicitly distinguishes operator-emitted secrets (CNPG) from chart-created ones (RedisFailover), since the wiring differs. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 30bd557..2b122ed 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -636,6 +636,73 @@ Print a report: 4. Commit and push — Flux picks up changes via the GitRepository defined in `init.yaml` (default interval: 1m) 5. Verify in cluster: `kubectl get $APP_PLURAL.$APP_NAME.apps.cozystack.io --all-namespaces` +## Dependency catalog + +For any Pattern A dependency, you MUST know three things before writing a creation template or an env mapping: (1) the exact CR `apiVersion`/`kind`, (2) which secret is produced, by whom, and with which keys, (3) how the app consumes those secrets. Never guess. The entries below are verified against the cozystack monorepo (`git.com/cozystack/cozystack`). For any dependency not listed here, research a reference implementation before writing templates. + +### Research procedure (for dependencies not in the catalog) + +1. Locate a reference in the cozystack monorepo: + + ```bash + grep -rlE "kind: (Cluster|RedisFailover|PerconaServerMongoDB|Kafka|ClickHouseInstallation|NATS)" \ + $COZYSTACK_REPO/packages/{apps,system}/*/templates/ + ``` + +2. Read the CR template and the consumer template (typically the app's main workload). Extract: + - CR `apiVersion` and `kind`. + - Whether the operator auto-creates a credentials Secret, or the chart must create one itself. + - Exact Secret name template and key names. + - Whether the app wires credentials via `env + secretKeyRef`, a mounted volume, or a config file. +3. Record the findings before proceeding to Phase 7. If research does not yield a verified answer, stop and ask the user — do not invent CR shapes or secret key names. + +### postgres — CloudNativePG `Cluster` + +| Field | Value | +| --- | --- | +| Operator | CloudNativePG (`cnpg.io`) — provided by `packages/system/cnpg-operator` | +| CR | `postgresql.cnpg.io/v1` → `Cluster` | +| Reference template | `cozystack/packages/system/harbor/templates/database.yaml` | +| Reference consumer | `cozystack/packages/system/keycloak/templates/sts.yaml:142-168` | +| Output Secret | Auto-created by the operator. If cluster is named `-db`, the Secret is `-db-app`. | +| Output Secret keys | `host`, `port`, `username`, `password`, `dbname`, `uri`, `jdbc-uri` | +| Superuser Secret | `-db-superuser` (same keys + `superuser`) | +| Services | `-db-rw` (primary), `-db-r` (read replicas), `-db-ro` (read-only) | +| App wiring | env via `secretKeyRef` to the auto-created Secret | + +### redis — Spotahome `RedisFailover` + +| Field | Value | +| --- | --- | +| Operator | Spotahome Redis Operator (`databases.spotahome.com`) — provided by `packages/system/redis-operator` | +| CR | `databases.spotahome.com/v1` → `RedisFailover` | +| Reference template | `cozystack/packages/system/harbor/templates/redis.yaml` | +| Output Secret | **Not auto-created** — the chart itself creates a Secret alongside the CR (naming is chart-choice, commonly `-redis-auth` with key `password`). | +| CR ↔ Secret wiring | `spec.auth.secretPath: ` — the operator reads the Secret by that name. | +| App wiring | The same Secret is mounted or read via env by the app. The chart generates the password (e.g., from `.Values.redis.password` or a randomly generated one) and stores it in the Secret. | + +Unlike CNPG, the Spotahome operator does NOT emit connection details. The chart is responsible for password generation and for wiring the same Secret into both the operator (`auth.secretPath`) and the consuming app. + +### mongodb — Percona `PerconaServerMongoDB` + +| Field | Value | +| --- | --- | +| Operator | Percona Server for MongoDB (`psmdb.percona.com`) — provided by `packages/system/psmdb-operator` | +| CR | `psmdb.percona.com/v1` → `PerconaServerMongoDB` | +| Reference template | `cozystack/packages/apps/mongodb/templates/mongodb.yaml` | +| Seed Secret | **Chart-created**, referenced via `spec.secrets.users` — the operator reads this for initial user/password seeding. | +| App wiring | Depends on the app — typically a `DATABASE_URL` assembled from the Secret. Verify against the specific app's expected env before wiring. | + +### kafka — Strimzi `Kafka` + +| Field | Value | +| --- | --- | +| Operator | Strimzi (`kafka.strimzi.io`) | +| CR | `kafka.strimzi.io/v1beta2` → `Kafka` (plus `KafkaUser` for SCRAM/TLS) | +| Reference template | `cozystack/packages/apps/kafka/templates/kafka.yaml` | +| Output Secrets | Brokers expose services. Client credentials are issued per `KafkaUser` CR — Strimzi creates a Secret named `` with `password` (SCRAM) and/or `user.crt`/`user.key` (TLS). | +| App wiring | SCRAM-SHA-512 via env, or TLS via mounted volume. Consult the KafkaUser status to discover the actual Secret layout. | + ## Guardrails - **Never** commit or push on behalf of the user. This is a generate-only skill. From 13da43a5167978c447321b288745b18d8cc3d4ba Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:22:20 +0300 Subject: [PATCH 13/30] feat(cozy-external-app): require research before pattern A dependency wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split Phase 4 Pattern A into three explicit steps: research the dependency (CR kind, output Secret owner/name/keys, app-side wiring), collect CR-spec values, collect env mapping. The earlier flow jumped straight to hardcoded CNPG defaults and would have silently produced wrong Secret references for redis or mongodb. Env mapping now uses facts captured during research — catalog-driven examples show how wiring differs between operator-emitted secrets (CNPG) and chart-created ones (Spotahome RedisFailover). Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 2b122ed..c58a0b1 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -58,23 +58,36 @@ If `--depends-on` was not passed, use `AskUserQuestion`: "Does this app need any For each dependency, determine the **integration pattern** via `AskUserQuestion`: -### Pattern A — Managed provisioning (recommended for postgres) +### Pattern A — Managed provisioning (in-chart) -The app chart creates the backing service itself (e.g., a CNPG `Cluster` CR in its own templates). The app chart owns the lifecycle. +The app chart creates the backing service itself (a CR for the operator running in the cluster) and wires the resulting credentials into the app container. The app chart owns the lifecycle of both the CR and the Secret that the app reads. + +#### Step 1 — Research the dependency (mandatory) + +Before asking the user for spec values, consult the **Dependency catalog** (appendix at the bottom of this skill). For each Pattern A dependency, record four facts: + +1. **CR identity**: `apiVersion` and `kind` of the resource the chart will create (e.g., `postgresql.cnpg.io/v1 Cluster`, `databases.spotahome.com/v1 RedisFailover`). +2. **Output Secret** — who creates it (operator or chart), its name template, and its keys. +3. **CR ↔ Secret wiring** — whether the CR auto-produces the Secret (CNPG), or the chart must create the Secret and point the CR at it (RedisFailover `auth.secretPath`). +4. **App-side consumption** — env via `secretKeyRef`, volume mount, or config file. + +If the dependency is in the catalog, copy its facts into the conversation so later phases can refer to them. If it is not, run the research procedure described in the catalog appendix before proceeding. **Never invent CR shapes, secret names, or secret keys.** When research is inconclusive, stop and ask the user. + +#### Step 2 — Collect spec values + +The values to collect depend on the CR, not on a generic list. Use the fields exposed by the CR spec as recorded in Step 1. Common groupings: + +- postgres (CNPG `Cluster`): database name, database user, replicas (default `2`), storage size (default `5Gi`). +- redis (Spotahome `RedisFailover`): replicas (default `3`), storage size (default `2Gi`), password source (random generated or user-supplied via `values.yaml`). +- mongodb (Percona `PerconaServerMongoDB`): replica-set size, storage size, users to seed. +- Other deps: consult the CR schema captured in Step 1. + +#### Step 3 — Collect env mapping + +Use the Secret name and keys recorded in Step 1 — not a hardcoded list. For every environment variable the app expects, record which Secret key it maps to. Ask the user to confirm the app's expected env names. + +Example for postgres via CNPG (Secret `{{ .Release.Name }}-db-app`, keys `host`, `port`, `username`, `password`, `dbname`): -Collect: -- Database name (default: `app`) -- Database user (default: `app`) -- Number of replicas (default: `2`) -- Storage size (default: `5Gi`) - -**CNPG secret convention** (verified from cozystack harbor/keycloak): -- Cluster named `{{ .Release.Name }}-db` creates: - - Service: `{{ .Release.Name }}-db-rw` (read-write), `{{ .Release.Name }}-db-r` (read-only) - - Secret: `{{ .Release.Name }}-db-app` with keys: `host`, `port`, `username`, `password`, `dbname` - - Secret: `{{ .Release.Name }}-db-superuser` with superuser credentials - -Collect the env variable mapping for the app container. Defaults: ```yaml DB_HOST: secretKeyRef → {{ .Release.Name }}-db-app → host DB_PORT: secretKeyRef → {{ .Release.Name }}-db-app → port @@ -83,7 +96,15 @@ DB_PASSWORD: secretKeyRef → {{ .Release.Name }}-db-app → password DB_NAME: secretKeyRef → {{ .Release.Name }}-db-app → dbname ``` -Ask the user if these env names are correct for their app, or if the app expects different names (e.g., `DATABASE_URL`, `PGHOST`, `POSTGRES_PASSWORD`). +Example for redis via Spotahome (chart-created Secret `{{ .Release.Name }}-redis-auth`, key `password`): + +```yaml +REDIS_HOST: value → rfs-{{ .Release.Name }}-redis # sentinel service from RedisFailover +REDIS_PORT: value → "26379" +REDIS_PASSWORD: secretKeyRef → {{ .Release.Name }}-redis-auth → password +``` + +If the app expects a compound value (`DATABASE_URL`, `REDIS_URL`, etc.), note the assembly pattern — it usually needs a Helm template expression, not a direct `secretKeyRef`. ### Pattern B — External reference From 2a3e4a477e48d81c2247feab312b402197a4fcf5 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:23:03 +0300 Subject: [PATCH 14/30] feat(cozy-external-app): emit per-dependency creation templates in phase 7 Phase 7 previously shipped a single hardcoded database.yaml for postgres/CNPG, implying the same wiring for every dependency. Rework the section into "per-dep creation templates": one file per Pattern A dependency, shape dictated by the facts captured during Phase 4 research. Add a full redis.yaml example (Secret + RedisFailover with auth.secretPath) to illustrate that chart-created secrets require a fundamentally different wiring from CNPG. Tighten guardrails to forbid copying postgres wiring onto other dependencies. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 68 ++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index c58a0b1..2230f57 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -364,9 +364,15 @@ Add env vars for each dependency based on the pattern chosen in Phase 4. key: password ``` -### database.yaml (Pattern A only) +### Dependency creation templates (Pattern A only) -Create a CNPG Cluster resource. Follow the exact pattern from cozystack `system/harbor/templates/database.yaml`: +For each Pattern A dependency recorded in Phase 4, emit one template file under `packages/apps/$APP_NAME/templates/`, named after the dependency (`database.yaml`, `redis.yaml`, `mongodb.yaml`, etc.). Each template must reflect the CR identity and wiring captured during Phase 4 research — do not reuse the postgres pattern for other dependencies. + +The `database.yaml` and `redis.yaml` examples below correspond to the catalog entries at the end of this skill. For anything else (mongodb, kafka, clickhouse, opensearch, …), open the reference template recorded in Phase 4 and mirror its structure; do not invent spec fields. + +#### database.yaml — postgres via CloudNativePG + +Reference: `cozystack/packages/system/harbor/templates/database.yaml`. ```yaml --- @@ -396,11 +402,56 @@ spec: policy.cozystack.io/allow-to-apiserver: "true" ``` -The CNPG operator automatically creates: -- Secret `{{ .Release.Name }}-db-app` with keys: `host`, `port`, `username`, `password`, `dbname`, `uri`, `jdbc-uri` -- Service `{{ .Release.Name }}-db-rw` (read-write primary) -- Service `{{ .Release.Name }}-db-r` (read replicas) -- Service `{{ .Release.Name }}-db-ro` (read-only replicas) +Outputs (auto-created by the CNPG operator; no chart-side Secret): + +- Secret `{{ .Release.Name }}-db-app` — keys `host`, `port`, `username`, `password`, `dbname`, `uri`, `jdbc-uri`. +- Services `{{ .Release.Name }}-db-rw` (primary), `-db-r` (read replicas), `-db-ro` (read-only). + +#### redis.yaml — redis via Spotahome RedisFailover + +Reference: `cozystack/packages/system/harbor/templates/redis.yaml`. + +The chart creates the Secret **before** the CR — the operator reads it via `spec.auth.secretPath`: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-redis-auth +stringData: + password: {{ .Values.redis.password | quote }} +--- +apiVersion: databases.spotahome.com/v1 +kind: RedisFailover +metadata: + name: {{ .Release.Name }}-redis +spec: + sentinel: + replicas: 3 + redis: + replicas: {{ .Values.redis.replicas }} + storage: + persistentVolumeClaim: + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.redis.size }} + {{- with .Values.storageClass }} + storageClassName: {{ . }} + {{- end }} + auth: + secretPath: {{ .Release.Name }}-redis-auth +``` + +Outputs: + +- Secret `{{ .Release.Name }}-redis-auth` (chart-created) — key `password`. +- Sentinel service `rfs-{{ .Release.Name }}-redis` on port `26379`. + +If `.Values.redis.password` is empty, generate a password inline so re-renders are stable — see the [`randAlphaNum`](https://pkg.go.dev/github.com/Masterminds/sprig/v3#hdr-String_Functions) Sprig helper and the `lookup` function to reuse an existing Secret on upgrade. ### service.yaml (if app exposes a port) @@ -729,7 +780,8 @@ Unlike CNPG, the Spotahome operator does NOT emit connection details. The chart - **Never** commit or push on behalf of the user. This is a generate-only skill. - **Never** apply anything to a cluster — no `kubectl apply`, no `helm install`, no `make apply`. This skill only creates files. - **Never** overwrite existing `packages/apps/$APP_NAME/` without explicit user confirmation. -- **Never** guess CNPG secret names or key names. The verified convention is: Cluster `-db` → Secret `-db-app` with keys `host`, `port`, `username`, `password`, `dbname`. If the user's scenario differs (e.g., custom bootstrap, non-standard secret names), stop and ask. +- **Never** guess a dependency's CR shape, Secret name, or Secret keys. For every Pattern A dependency the research step in Phase 4 is mandatory — use the Dependency catalog appendix first; for anything not in the catalog, open a reference implementation in the cozystack monorepo before writing templates. If research does not yield a verified answer, stop and ask. +- **Never** copy the postgres/CNPG wiring onto a different dependency. CNPG auto-creates the credentials Secret; Spotahome RedisFailover does not (the chart creates it). Other operators differ further — always verify. - **Never** edit files in a cozystack checkout used as reference — those are read-only. - **Never** modify `init.yaml` — the user manages their GitRepository and root HelmRelease manually. - **Always** use `AskUserQuestion` before creating files in Phase 6, 7, and 8. Show what will be created. From 3791d9cdf59c9eda145492f55d67d426787b0b42 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:33:06 +0300 Subject: [PATCH 15/30] feat(cozy-external-app): generalize phase 5 to register any upstream chart source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 was scoped to operator-only chart sources, but flux HelmRepository registration is equally needed when the app wraps an upstream helm chart (Gitea, Immich, Nextcloud — all have first-party maintained charts). Rewrite the phase around a $SOURCE_ROLE (main | operator) so either case registers a HelmRepository; phase 8 can then reuse one code path for both. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 2230f57..c973ac8 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -116,20 +116,26 @@ Collect: Present a summary of all dependencies with chosen patterns. Proceed only after user confirms. -## Phase 5 — Gather operator specification (conditional) +## Phase 5 — Register upstream Helm chart sources (conditional) -Skip if `--operator` was not passed and the app does not need a custom operator. +Flux reconciles external Helm charts via the `HelmRepository` resource. Two situations require a `HelmRepository` registration in this phase: -The reference layout (`external-apps-example`) pulls operator charts at reconcile time via a flux `HelmRepository` — it does not vendor a local package in `packages/system/`. This phase gathers the details needed to register the `HelmRepository` and operator `HelmRelease` in Phase 8. +1. **App wraps an upstream Helm chart** (see Phase 3 question 1). Example: Gitea wraps `https://dl.gitea.com/charts`. +2. **App requires a dedicated operator** shipped as a separate chart. Example: `minecraft-operator` from `oci://ghcr.io/lexfrei/charts`. -Use `AskUserQuestion` to collect: +Skip this phase only if BOTH conditions are false (app uses custom templates AND no dedicated operator). + +For each source needed, use `AskUserQuestion` to collect: -- `$OPERATOR_REPO_TYPE` — `oci` if `$OPERATOR_REPO_URL` starts with `oci://`, otherwise leave empty (standard HTTPS Helm repo). -- `$OPERATOR_CHART_NAME` — chart name inside the repository (e.g., `minecraft-operator`, `immich`). -- `$OPERATOR_CHART_VERSION` — pinned chart version or semver range (e.g., `0.9.4`, `>=1.0.0`). Avoid `'*'` in production use. -- `$OPERATOR_REPO_NAME` — local alias used as `HelmRepository.metadata.name` and referenced by the HelmRelease. Default: `$APP_NAME-operator`. +- `$SOURCE_ROLE` — `main` (upstream chart for the app itself) or `operator` (dedicated operator chart). +- `$SOURCE_REPO_URL` — repository URL. Prefix with `oci://` for OCI registries, otherwise use plain HTTPS. +- `$SOURCE_REPO_TYPE` — `oci` if the URL starts with `oci://`, otherwise leave empty. +- `$SOURCE_CHART_NAME` — chart name inside the repository (e.g., `gitea`, `minecraft-operator`, `immich`). +- `$SOURCE_CHART_VERSION` — pinned version or semver range (e.g., `12.0.1`, `>=1.0.0`). Avoid `'*'` in production use. +- `$SOURCE_REPO_NAME` — alias used as `HelmRepository.metadata.name`. Default: `$APP_NAME` for `main`, `$APP_NAME-operator` for `operator`. +- `$SOURCE_NAMESPACE` — namespace the `HelmRepository` lives in. Default: `external-$APP_NAME-operator` for operator sources, `external-$APP_NAME` for main sources (the same namespace later hosts any app-scoped HelmRelease). -No files are created in this phase. All operator resources are written in Phase 8 alongside the other platform resources. +No files are created in this phase. All source and release resources are written in Phase 8 alongside the other platform resources. ## Phase 6 — Create app chart skeleton From 3c67c224710cb7b33f54467a9e0a5c03659bf572 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:34:01 +0300 Subject: [PATCH 16/30] feat(cozy-external-app): default main workload to a HelmRelease wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For apps with a maintained first-party helm chart, wrapping in a Flux HelmRelease is strictly better than hand-rolling a Deployment: upgrades, init jobs, probes, PDBs, and CVE mitigations all stay the responsibility of the upstream maintainers. Rewrite Phase 3 question 1 to bias toward the wrapper pattern, and rework Phase 7 main workload so the HelmRelease example is the primary shape — complete with dependsOn against pattern C sibling HelmReleases, bundled-subchart disablement, and valuesFrom lifting passwords from cozystack-created Secrets into the upstream chart value schema. The custom Deployment example is demoted to a fallback for apps without a viable upstream chart. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 67 +++++++++++++++---- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index c973ac8..ca5d1bc 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -42,7 +42,10 @@ Bail early if any check fails. Use `AskUserQuestion` to collect: -1. **Chart source**: upstream Helm chart (provide repo URL + chart name + version) or custom templates from scratch? +1. **Chart source**: is there a maintained first-party Helm chart for this app (official repo, community-run, appeared on Artifact Hub)? + - **Default: wrap the upstream chart via Flux `HelmRelease`**. You inherit upgrades, init-jobs, probes, PDBs, ingress templates, and every breaking-change mitigation the upstream maintainers ship. Phase 5 will register its `HelmRepository`; Phase 7 emits the wrapping `HelmRelease`. + - Fall back to **custom templates** only when no upstream chart exists, the upstream is abandoned, or it conflicts with Cozystack conventions in ways that cannot be overridden via values. Custom templates shift lifecycle ownership onto the skill's user — every upstream CVE must then be tracked by hand. + - Record `$CHART_SOURCE` as `upstream` or `custom`. If `upstream`, collect repo URL + chart name + version here (feeds Phase 5). 2. **Container image**: image reference (e.g., `ghcr.io/immich-app/immich-server:v1.120.0`). 3. **Public port**: does the app expose an HTTP port? If yes, which port number? Should an Ingress template be generated? 4. **Persistent storage**: does the app need a PVC? If yes, default size (e.g., `10Gi`). @@ -265,9 +268,11 @@ Create `packages/apps/$APP_NAME/templates/` with the following files. ### Main workload — $APP_NAME.yaml -Generate the primary workload template. If the user chose "upstream Helm chart", wrap it in a Flux HelmRelease (like harbor does). If "custom templates", create a Deployment or StatefulSet directly. +Generate the primary workload template. If Phase 3 recorded `$CHART_SOURCE = upstream`, emit a Flux `HelmRelease` wrapping the upstream chart — the preferred path. If `$CHART_SOURCE = custom`, emit a `Deployment` (or `StatefulSet`) authored from scratch. -**For a Flux HelmRelease wrapper example (upstream chart case):** +#### Upstream chart wrapper (preferred) + +The HelmRelease registered below references the `HelmRepository` created in Phase 8 and injects cozystack-wired connection details via `values` and `valuesFrom`. `valuesFrom` is the cleanest way to pipe a password out of a Secret created by a Pattern C sibling CR (see Phase 4 Pattern C, Dependency catalog appendix) directly into the upstream chart's value path — no Deployment env rewriting required. ```yaml apiVersion: helm.toolkit.fluxcd.io/v2 @@ -275,24 +280,60 @@ kind: HelmRelease metadata: name: {{ .Release.Name }} spec: - interval: 1h + interval: 5m chart: spec: - chart: $UPSTREAM_CHART_NAME - version: $UPSTREAM_CHART_VERSION + chart: $SOURCE_CHART_NAME + version: $SOURCE_CHART_VERSION sourceRef: kind: HelmRepository - name: $UPSTREAM_REPO_NAME - namespace: cozy-public + name: $SOURCE_REPO_NAME + namespace: $SOURCE_NAMESPACE + # dependsOn ensures sibling cozystack CRs reconcile before this release tries to use their outputs. + # Reference the generated HelmReleases (named ``) that cozystack controllers + # produce from the Pattern C sibling CRs you emit in templates/postgres.yaml, templates/redis.yaml, etc. + dependsOn: + - name: postgres-{{ .Release.Name }}-db + namespace: {{ .Release.Namespace }} + - name: redis-{{ .Release.Name }}-redis + namespace: {{ .Release.Namespace }} + # Disable the upstream chart's bundled subcharts — we provide backing services via Pattern C. values: - # Map .Values.* to the upstream chart's value schema here. - # Pull credentials from the CNPG secret (Pattern A) or from - # .Values.postgres.* (Pattern B) as established in Phase 4. + postgresql: { enabled: false } + postgresql-ha: { enabled: false } + redis: { enabled: false } + redis-cluster: { enabled: false } + # Hostnames/ports are stable from the cozystack ApplicationDefinition naming convention + # (postgres--rw, rfs-redis- — see Dependency catalog Pattern C entries). + # Username, database name, and port values come from the spec recorded in Phase 4. + app: + config: + database: + host: postgres-{{ .Release.Name }}-db-rw + port: 5432 + name: $APP_DB_NAME + user: $APP_DB_USER + redis: + host: rfs-redis-{{ .Release.Name }}-redis + port: 26379 + sentinelMaster: mymaster + # Secrets must be read at reconcile time — never inline passwords into values. + valuesFrom: + - kind: Secret + name: postgres-{{ .Release.Name }}-db-credentials + valuesKey: $APP_DB_USER + targetPath: app.config.database.password + - kind: Secret + name: redis-{{ .Release.Name }}-redis-auth + valuesKey: password + targetPath: app.config.redis.password ``` -The referenced `HelmRepository` resource must exist in the cluster. If it does not, register it in Phase 8 alongside the other platform resources. +Replace the `app.config.*` value paths with the actual schema of your upstream chart — this example uses a generic layout. Common real-world paths: Gitea uses `gitea.config.database.HOST`/`PASSWD`, Immich uses `immich.env.DB_HOSTNAME`/`DB_PASSWORD`, Nextcloud uses `internalDatabase.*`. Verify against the upstream chart's `values.yaml` before wiring. + +The referenced `HelmRepository` must exist in the cluster. Phase 8 registers it using the `$SOURCE_*` variables gathered in Phase 5. -**For a direct Deployment example:** +#### Custom Deployment (fallback) ```yaml apiVersion: apps/v1 From 04e1eb238dd38c01068585d9b86f81f55a23b230 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:34:40 +0300 Subject: [PATCH 17/30] feat(cozy-external-app): introduce pattern C as default for external apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pattern A (in-chart operator CR) bypasses the cozystack abstraction — dependencies never appear in the dashboard, WorkloadMonitor/backup conventions get reinvented per app, and platform upgrades do not propagate. For tenant-facing external apps the correct default is Pattern C: the app chart creates a sibling cozystack CR (Postgres, Redis, MariaDB, etc.), the cozystack controller reconciles it into its own HelmRelease, and the app reads the Secret/Service that downstream chart produces. Add pattern C to phase 4 as the recommended path and demote pattern A to a system-style escape hatch. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index ca5d1bc..e337b82 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -59,11 +59,39 @@ Record all answers. Proceed only after user confirms the summary. If `--depends-on` was not passed, use `AskUserQuestion`: "Does this app need any backing services (e.g., postgres, redis, mongodb)? List them or say 'none'." -For each dependency, determine the **integration pattern** via `AskUserQuestion`: +For each dependency, determine the **integration pattern** via `AskUserQuestion`. Three patterns exist — the recommended default for external apps is **Pattern C**. -### Pattern A — Managed provisioning (in-chart) +### Pattern C — Sibling Cozystack ApplicationDefinition (recommended for external apps) -The app chart creates the backing service itself (a CR for the operator running in the cluster) and wires the resulting credentials into the app container. The app chart owns the lifecycle of both the CR and the Secret that the app reads. +The app chart creates a **cozystack-level CR** (e.g., `Redis`, `Postgres`, `MariaDB`, `Kafka`) in its own templates. The cozystack controller then reconciles that CR into a HelmRelease which deploys the corresponding `packages/apps//` chart — the same chart dashboard users invoke when they deploy Redis/Postgres manually. + +Why this is the default for external apps: + +- Every sibling instance appears in the dashboard as a first-class entity. A tenant can list, inspect, back up, and restore it independently of the app. +- WorkloadMonitor, PodMonitor, backup schedules, and migration logic shipped by cozystack's own `apps//` chart apply automatically — none of that needs to be re-implemented per app. +- Upgrading cozystack itself upgrades the dependency wiring for every consumer at once. + +Before collecting spec values, research the sibling ApplicationDefinition (see **Dependency catalog Pattern C** appendix). Each entry records the three facts the app chart needs to wire an instance correctly: + +1. **Prefix** — cozystack controller prepends this to the CR name when rendering the downstream HelmRelease. E.g., `Redis/foo` → HelmRelease `redis-foo`. +2. **Credentials Secret name template** — where the downstream chart exposes passwords. E.g., `postgres-{{ .name }}-credentials`, `redis-{{ .name }}-auth`. +3. **Services** — which Service names the downstream chart creates. E.g., `postgres--rw`, `rfs-redis-`. + +These contracts are declared in `packages/system/-rd/cozyrds/.yaml` under `spec.secrets.include.resourceNames` and `spec.services.include.resourceNames` — authoritative per cozystack version. + +In Phase 7 the app chart emits one Pattern C CR per dependency (template `.yaml`), mapping chart values onto the CR's spec. The main workload HelmRelease (see Phase 7 Main workload) then references the downstream-emitted Secret via `valuesFrom` and targets the downstream Service via `values`. + +Spec parameters to collect depend on the sibling CR's own `openAPISchema`. Common fields: + +- `Postgres`: `size`, `replicas`, `users` (map of `: password: `), `databases` (map of `: roles: { admin: [users] }`). +- `Redis`: `size`, `replicas`, `authEnabled` (default `true`), `storageClass`. +- `MariaDB`, `MongoDB`, `Kafka`, `ClickHouse`: consult their ApplicationDefinition under `packages/system/-rd/cozyrds/.yaml`. + +### Pattern A — In-chart operator CR (system-style) + +The app chart creates the operator CR itself (e.g., a CNPG `Cluster` or Spotahome `RedisFailover`) instead of a cozystack-level sibling CR. The app chart owns both the CR and its output Secret. No separate dashboard entity for the dependency. + +Use Pattern A only when a Pattern C sibling does not exist for the dependency, or when the app is explicitly system-scoped (like cozystack's own `harbor` or `keycloak`, which predate the sibling-CR pattern). For tenant-facing external apps, prefer Pattern C. #### Step 1 — Research the dependency (mandatory) From 7e1fd8e2b5b4d323aca0bad57c29e4d7358829b0 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:35:23 +0300 Subject: [PATCH 18/30] feat(cozy-external-app): catalog verified pattern C contracts for postgres and redis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the sibling-CR naming contracts (HelmRelease prefix, credentials Secret template, Service names) from cozystack packages/system/postgres-rd and redis-rd cozyrds files and publish them as catalog entries. Each entry ships a ready-to-adapt templates/.yaml snippet and a matching main-HelmRelease values/valuesFrom block — the two pieces a user needs to wire a pattern C dependency end to end. For mariadb, mongodb, kafka, and friends, point readers at packages/system/-rd/cozyrds/.yaml as the authoritative source rather than publishing unverified copies. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 118 +++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index e337b82..00449e7 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -785,9 +785,123 @@ Print a report: ## Dependency catalog -For any Pattern A dependency, you MUST know three things before writing a creation template or an env mapping: (1) the exact CR `apiVersion`/`kind`, (2) which secret is produced, by whom, and with which keys, (3) how the app consumes those secrets. Never guess. The entries below are verified against the cozystack monorepo (`git.com/cozystack/cozystack`). For any dependency not listed here, research a reference implementation before writing templates. +The skill supports three integration patterns (see Phase 4): -### Research procedure (for dependencies not in the catalog) +- **Pattern C** — app chart creates a cozystack-level sibling CR (`Postgres`, `Redis`, …). Default for external apps. +- **Pattern A** — app chart creates the operator CR directly (CNPG `Cluster`, Spotahome `RedisFailover`). System-style; rarely appropriate for external apps. +- **Pattern B** — user provides a pre-existing service via values. Unchanged from plain Helm. + +Pattern C entries below capture the naming contract that cozystack controllers commit to: the downstream HelmRelease name, the Secret the downstream chart creates, and the Services it exposes. These values are pulled directly from `packages/system/-rd/cozyrds/.yaml` — authoritative per cozystack version. If a cozystack upgrade changes a prefix or resourceName pattern, the catalog here must be re-verified. + +Pattern A entries record the operator-CR shape for cases where Pattern C is not available or not desired. + +### Pattern C — postgres (cozystack `Postgres`) + +| Field | Value | +| --- | --- | +| Sibling CR | `apps.cozystack.io/v1alpha1` → `Postgres` | +| Source of truth | `cozystack/packages/system/postgres-rd/cozyrds/postgres.yaml` | +| Downstream HelmRelease | `postgres-{{ .name }}` (prefix `postgres-` from the ApplicationDefinition) | +| Credentials Secret | `postgres-{{ .name }}-credentials` — keys are the usernames configured in `spec.users..password`. The key's value is the plaintext password. | +| Services | `postgres-{{ .name }}-rw` (primary), `-r` (read replicas), `-ro` (read-only), `-external-write` (LoadBalancer when `spec.external: true`) | +| Port | `5432` | +| Sibling CR spec essentials | `size`, `replicas`, `users` (map `: {password: ...}`), `databases` (map `: {roles: {admin: [...usernames]}}`), `external`, `storageClass` | + +Example Pattern C CR (rendered from the app chart's `templates/postgres.yaml`): + +```yaml +apiVersion: apps.cozystack.io/v1alpha1 +kind: Postgres +metadata: + name: {{ .Release.Name }}-db + namespace: {{ .Release.Namespace }} +spec: + size: {{ .Values.database.size }} + replicas: {{ .Values.database.replicas }} + external: false + users: + {{ .Values.database.user }}: + password: {{ .Values.database.password | default (randAlphaNum 32) | quote }} + databases: + {{ .Values.database.name }}: + roles: + admin: + - {{ .Values.database.user }} +``` + +Wiring in the main workload HelmRelease: + +```yaml +values: + app: + config: + database: + host: postgres-{{ .Release.Name }}-db-rw + port: 5432 + name: {{ .Values.database.name }} + user: {{ .Values.database.user }} +valuesFrom: + - kind: Secret + name: postgres-{{ .Release.Name }}-db-credentials + valuesKey: {{ .Values.database.user }} + targetPath: app.config.database.password +``` + +### Pattern C — redis (cozystack `Redis`) + +| Field | Value | +| --- | --- | +| Sibling CR | `apps.cozystack.io/v1alpha1` → `Redis` | +| Source of truth | `cozystack/packages/system/redis-rd/cozyrds/redis.yaml` | +| Downstream HelmRelease | `redis-{{ .name }}` (prefix `redis-`) | +| Credentials Secret | `redis-{{ .name }}-auth` — key `password`. Present only when `spec.authEnabled: true` (default). | +| Services | `rfs-redis-{{ .name }}` (sentinel :26379), `rfrm-redis-{{ .name }}` (master), `rfrs-redis-{{ .name }}` (slaves), `redis-{{ .name }}-external-lb` (LoadBalancer when `spec.external: true`) | +| Sibling CR spec essentials | `size`, `replicas`, `authEnabled`, `external`, `version` (`v8`/`v7`), `storageClass` | + +Example Pattern C CR (rendered from the app chart's `templates/redis.yaml`): + +```yaml +apiVersion: apps.cozystack.io/v1alpha1 +kind: Redis +metadata: + name: {{ .Release.Name }}-redis + namespace: {{ .Release.Namespace }} +spec: + size: {{ .Values.redis.size }} + replicas: {{ .Values.redis.replicas }} + external: false + authEnabled: true +``` + +Wiring in the main workload HelmRelease: + +```yaml +values: + app: + config: + redis: + host: rfs-redis-{{ .Release.Name }}-redis + port: 26379 + sentinelMaster: mymaster +valuesFrom: + - kind: Secret + name: redis-{{ .Release.Name }}-redis-auth + valuesKey: password + targetPath: app.config.redis.password +``` + +### Pattern C — other dependencies + +For `MariaDB`, `MongoDB`, `Kafka`, `ClickHouse`, `RabbitMQ`, `FoundationDB`, `Qdrant`, `OpenSearch`, `NATS`, `Openbao` — read the corresponding `packages/system/-rd/cozyrds/.yaml` and extract: + +- `spec.application.kind` — sibling CR kind. +- `spec.release.prefix` — HelmRelease name prefix. +- `spec.secrets.include.resourceNames` — exact Secret naming template (often `{{ .name }}-credentials` or similar). +- `spec.services.include.resourceNames` — Service naming templates. + +Do not extrapolate from the postgres/redis entries above. Each ApplicationDefinition declares its own naming contract. + +### Pattern A — research procedure (for dependencies not in the Pattern A catalog below) 1. Locate a reference in the cozystack monorepo: From 95c7e5a4cdb3bfb477efc7a260e245317328d817 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:36:02 +0300 Subject: [PATCH 19/30] feat(cozy-external-app): emit pattern C templates in phase 7 Phase 7 now ships ready-to-adapt pattern C templates (apps.cozystack.io Postgres and Redis) as the preferred shape, with pattern A CNPG and RedisFailover examples kept for the escape-hatch case. Phase 10 summary surfaces the chosen pattern per dependency (A/B/C) plus the chart-source mode (upstream wrapper vs custom templates) so a quick glance at the summary tells a reader exactly how the generated package is wired. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 00449e7..b40d798 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -439,13 +439,72 @@ Add env vars for each dependency based on the pattern chosen in Phase 4. key: password ``` -### Dependency creation templates (Pattern A only) +### Dependency creation templates (Pattern A and Pattern C) -For each Pattern A dependency recorded in Phase 4, emit one template file under `packages/apps/$APP_NAME/templates/`, named after the dependency (`database.yaml`, `redis.yaml`, `mongodb.yaml`, etc.). Each template must reflect the CR identity and wiring captured during Phase 4 research — do not reuse the postgres pattern for other dependencies. +For each Pattern A or Pattern C dependency recorded in Phase 4, emit one template file under `packages/apps/$APP_NAME/templates/`, named after the dependency (`postgres.yaml`, `redis.yaml`, `mariadb.yaml`, …). Each template must reflect the CR identity and wiring captured during Phase 4 — do not reuse the postgres pattern for other dependencies. -The `database.yaml` and `redis.yaml` examples below correspond to the catalog entries at the end of this skill. For anything else (mongodb, kafka, clickhouse, opensearch, …), open the reference template recorded in Phase 4 and mirror its structure; do not invent spec fields. +The Pattern C examples below are the preferred shape for external apps; the Pattern A examples (system-style, in-chart operator CR) follow for the escape-hatch case. For Pattern B there is no per-dep template to emit — the app chart merely reads connection values the user supplies. -#### database.yaml — postgres via CloudNativePG +#### postgres.yaml — Pattern C (cozystack `Postgres` sibling CR) + +Reference: `cozystack/packages/system/postgres-rd/cozyrds/postgres.yaml` (authoritative contract), `cozystack/packages/apps/postgres/templates/` (what the downstream chart renders). Pattern C is the cleanest path: the app chart creates one cozystack CR, the controller does the rest. + +```yaml +--- +apiVersion: apps.cozystack.io/v1alpha1 +kind: Postgres +metadata: + name: {{ .Release.Name }}-db + namespace: {{ .Release.Namespace }} +spec: + size: {{ .Values.database.size }} + replicas: {{ .Values.database.replicas }} + external: false + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} + users: + {{ .Values.database.user }}: + password: {{ .Values.database.password | default (randAlphaNum 32) | quote }} + databases: + {{ .Values.database.name }}: + roles: + admin: + - {{ .Values.database.user }} +``` + +Outputs (rendered by the downstream `packages/apps/postgres/` chart): + +- Secret `postgres-{{ .Release.Name }}-db-credentials` — one key per user, value is the password. +- Services `postgres-{{ .Release.Name }}-db-rw`, `-r`, `-ro`; port `5432`. + +Consume these from the main workload HelmRelease via `values` + `valuesFrom` (see Phase 7 Main workload and Dependency catalog Pattern C appendix). + +#### redis.yaml — Pattern C (cozystack `Redis` sibling CR) + +```yaml +--- +apiVersion: apps.cozystack.io/v1alpha1 +kind: Redis +metadata: + name: {{ .Release.Name }}-redis + namespace: {{ .Release.Namespace }} +spec: + size: {{ .Values.redis.size }} + replicas: {{ .Values.redis.replicas }} + external: false + authEnabled: true + {{- with .Values.storageClass }} + storageClass: {{ . }} + {{- end }} +``` + +Outputs (rendered by the downstream `packages/apps/redis/` chart): + +- Secret `redis-{{ .Release.Name }}-redis-auth` — key `password`. +- Sentinel service `rfs-redis-{{ .Release.Name }}-redis` on port `26379`. + +#### database.yaml — Pattern A (in-chart CloudNativePG `Cluster`) Reference: `cozystack/packages/system/harbor/templates/database.yaml`. @@ -482,7 +541,7 @@ Outputs (auto-created by the CNPG operator; no chart-side Secret): - Secret `{{ .Release.Name }}-db-app` — keys `host`, `port`, `username`, `password`, `dbname`, `uri`, `jdbc-uri`. - Services `{{ .Release.Name }}-db-rw` (primary), `-db-r` (read replicas), `-db-ro` (read-only). -#### redis.yaml — redis via Spotahome RedisFailover +#### redis.yaml — Pattern A (in-chart Spotahome `RedisFailover`) Reference: `cozystack/packages/system/harbor/templates/redis.yaml`. @@ -773,7 +832,8 @@ Print a report: - **App name**: `$APP_NAME` - **Files created** (list all new files with paths relative to repo root) - **Files modified** (list all modified files under `packages/core/platform/templates/`: `namespaces.yaml`, `helmrepositories.yaml`, `helmreleases.yaml`, `helmcharts.yaml`, `cozyrds.yaml`) -- **Dependencies**: for each dependency, state the chosen pattern (A: managed / B: external) and which secrets the app consumes +- **Dependencies**: for each dependency, state the chosen pattern (C: sibling cozystack CR / A: in-chart operator CR / B: external reference) and which Secret/Service the app consumes +- **Chart source**: `upstream` (HelmRelease wrapper; note upstream repo + chart name + version) or `custom` (hand-written Deployment/StatefulSet) - **Operator**: created or not, chart source - **Dashboard**: category, tags, icon status (present / missing) - **Next steps for the user**: From 9301a036358ce897b5dc400250a332051c5c2533 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:37:00 +0300 Subject: [PATCH 20/30] fix(cozy-external-app): align phase 8 templates with $SOURCE_* variables After phase 5 was generalized, phase 8 still referenced the old $OPERATOR_* variable names and assumed a single chart source per app. Rewrite the namespaces, helmrepositories, and helmreleases subsections to iterate over every $SOURCE_* entry gathered in phase 5 (main and/or operator) and emit the corresponding resource once per source. Also call out that the main upstream chart HelmRelease lives inside the app chart itself (one per tenant instance) and must not appear in the platform-level helmreleases.yaml. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index b40d798..bf29378 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -649,7 +649,10 @@ Stop and ask the user if the extracted value is empty. ### namespaces.yaml -If an operator was gathered in Phase 5, append the operator namespace: +Append one namespace per source role recorded in Phase 5: + +- Operator source → `external-$APP_NAME-operator` (hosts both the operator `HelmRelease` and its `HelmRepository`). +- Main source → `external-$APP_NAME` (hosts the `HelmRepository` for the upstream app chart; user instances deployed via the dashboard live in tenant namespaces and reference this `HelmRepository` cross-namespace). ```yaml --- @@ -658,54 +661,56 @@ kind: Namespace metadata: labels: cozystack.io/system: "true" - name: external-$APP_NAME-operator + name: $SOURCE_NAMESPACE ``` +Emit one entry for each `$SOURCE_NAMESPACE` gathered (deduplicate if operator and main share a namespace by mistake). + ### helmrepositories.yaml -If an operator was gathered in Phase 5, append a `HelmRepository` entry in the operator namespace. The HelmRelease below references it by name without a namespace field, so both must live in the same namespace: +For every source gathered in Phase 5 (operator and/or main), append a `HelmRepository`: ```yaml --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: - name: $OPERATOR_REPO_NAME - namespace: external-$APP_NAME-operator + name: $SOURCE_REPO_NAME + namespace: $SOURCE_NAMESPACE spec: interval: 5m - url: $OPERATOR_REPO_URL - # Uncomment for OCI registries (e.g., ghcr.io): + url: $SOURCE_REPO_URL + # If $SOURCE_REPO_TYPE is `oci`, uncomment the next line: # type: oci ``` -If `$OPERATOR_REPO_TYPE` is `oci`, set `spec.type: oci` explicitly — standard HTTPS Helm repos omit the field. - ### helmreleases.yaml -If an operator was gathered in Phase 5, append a HelmRelease that pulls it from the `HelmRepository` registered above (same namespace, same name alias): +Only **operator** sources get a platform-level `HelmRelease` here. The main upstream chart (when `$CHART_SOURCE = upstream`) is wrapped by a `HelmRelease` rendered *inside the app chart itself* (Phase 7 Main workload) — one per user-deployed instance — and never appears in `packages/core/platform/templates/helmreleases.yaml`. + +For each operator source: ```yaml --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: - name: $OPERATOR_REPO_NAME - namespace: external-$APP_NAME-operator + name: $SOURCE_REPO_NAME + namespace: $SOURCE_NAMESPACE spec: interval: 5m - releaseName: $OPERATOR_REPO_NAME - targetNamespace: external-$APP_NAME-operator + releaseName: $SOURCE_REPO_NAME + targetNamespace: $SOURCE_NAMESPACE chart: spec: - chart: $OPERATOR_CHART_NAME + chart: $SOURCE_CHART_NAME sourceRef: kind: HelmRepository - name: $OPERATOR_REPO_NAME - version: '$OPERATOR_CHART_VERSION' + name: $SOURCE_REPO_NAME + version: '$SOURCE_CHART_VERSION' ``` -If the operator must wait on another operator (e.g., CNPG) before reconciling, add `spec.dependsOn` here — dependency ordering belongs on user-authored HelmReleases. The application's own HelmRelease is generated by the Cozystack controller from the `ApplicationDefinition` and is not user-writable. +If the operator must wait on another operator (e.g., CNPG) before reconciling, add `spec.dependsOn` here — dependency ordering belongs on user-authored `HelmRelease`s. The application's own `HelmRelease` is rendered per user-instance from the app chart; it can declare its own `dependsOn` against sibling cozystack-rendered HelmReleases (see Phase 7 Main workload). ### helmcharts.yaml From f88630eed6a714b49da40d56949624128865a9d6 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:46:21 +0300 Subject: [PATCH 21/30] feat(cozy-external-app): resolve dependencies from chart and cozystack at runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the "ask user to list deps" flow with a chart-driven, dynamically-resolved procedure. Step 1 introspects the upstream chart (helm show chart/values/readme + keyword scan of value paths) to surface candidate deps along with the exact targetPath the app expects. Step 2 resolves each dep against cozystack — local checkout, then gh api, then live cluster via kubectl — and records a $DEP_CONTRACT with kind, prefix, secret/service templates, and openAPISchema. Steps 3-5 pick the integration pattern, collect spec values from the resolved schema, and write the wiring record. Patterns A/B/C are preserved but now consume $DEP_CONTRACT instead of ad-hoc lookups, and pattern C is only offered when resolution succeeded. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 159 +++++++++++------- 1 file changed, 102 insertions(+), 57 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index bf29378..63a754f 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -55,97 +55,142 @@ Use `AskUserQuestion` to collect: Record all answers. Proceed only after user confirms the summary. -## Phase 4 — Gather dependency specification +## Phase 4 — Resolve and gather dependencies -If `--depends-on` was not passed, use `AskUserQuestion`: "Does this app need any backing services (e.g., postgres, redis, mongodb)? List them or say 'none'." +Dependencies are **discovered from the app itself**, not asked blind. The phase runs in five steps: discover what the app needs, resolve each against a cozystack contract, pick the integration pattern, collect spec values, record wiring. Never skip Steps 1–2 — they are what prevent wrong or invented Secret/Service references. -For each dependency, determine the **integration pattern** via `AskUserQuestion`. Three patterns exist — the recommended default for external apps is **Pattern C**. +### Step 1 — Chart requirement analysis -### Pattern C — Sibling Cozystack ApplicationDefinition (recommended for external apps) +Discover candidate dependencies from the chart itself. -The app chart creates a **cozystack-level CR** (e.g., `Redis`, `Postgres`, `MariaDB`, `Kafka`) in its own templates. The cozystack controller then reconciles that CR into a HelmRelease which deploys the corresponding `packages/apps//` chart — the same chart dashboard users invoke when they deploy Redis/Postgres manually. +**When `$CHART_SOURCE = upstream`:** pull the upstream chart metadata once and inspect it. -Why this is the default for external apps: +```bash +helm repo add --force-update $SOURCE_REPO_NAME $SOURCE_REPO_URL # HTTPS repos only +helm show chart $SOURCE_REPO_NAME/$SOURCE_CHART_NAME --version $SOURCE_CHART_VERSION > /tmp/Chart.yaml +helm show values $SOURCE_REPO_NAME/$SOURCE_CHART_NAME --version $SOURCE_CHART_VERSION > /tmp/values.yaml +helm show readme $SOURCE_REPO_NAME/$SOURCE_CHART_NAME --version $SOURCE_CHART_VERSION > /tmp/README.md +``` -- Every sibling instance appears in the dashboard as a first-class entity. A tenant can list, inspect, back up, and restore it independently of the app. -- WorkloadMonitor, PodMonitor, backup schedules, and migration logic shipped by cozystack's own `apps//` chart apply automatically — none of that needs to be re-implemented per app. -- Upgrading cozystack itself upgrades the dependency wiring for every consumer at once. +For OCI sources use `oci://$SOURCE_REPO_URL/$SOURCE_CHART_NAME` directly — `helm show` handles OCI without `repo add`. -Before collecting spec values, research the sibling ApplicationDefinition (see **Dependency catalog Pattern C** appendix). Each entry records the three facts the app chart needs to wire an instance correctly: +Extract three signals: -1. **Prefix** — cozystack controller prepends this to the CR name when rendering the downstream HelmRelease. E.g., `Redis/foo` → HelmRelease `redis-foo`. -2. **Credentials Secret name template** — where the downstream chart exposes passwords. E.g., `postgres-{{ .name }}-credentials`, `redis-{{ .name }}-auth`. -3. **Services** — which Service names the downstream chart creates. E.g., `postgres--rw`, `rfs-redis-`. +1. **Declared subcharts** from `Chart.yaml → dependencies[]`. Match names from this vocabulary: `postgresql`, `postgresql-ha`, `mariadb`, `mysql`, `redis`, `redis-cluster`, `valkey`, `mongodb`, `kafka`, `clickhouse`, `rabbitmq`, `memcached`, `nats`, `minio`. Every matched subchart is a candidate dep — note its `enabled` default and which values key disables it (typically `.enabled: false`). +2. **Config paths** in `values.yaml`. Recurse and record every path matching these keywords until you reach a leaf with `host`, `hostname`, `url`, `dsn`, `uri`, `password`, `user`, `username`, `database`, `dbname`, `port`: `database`, `db`, `postgres*`, `mysql`, `mariadb`, `cache`, `redis`, `valkey`, `session`, `queue`, `broker`, `mongodb`, `kafka`, `rabbitmq`, `memcached`. These paths are the `targetPath` values the app chart will later inject via `valuesFrom`. +3. **README hints** — maintainers usually call out supported databases ("supports PostgreSQL, MySQL, SQLite") near the top and show sample values for external services. Treat this as narrative context, not definitive — the actual schema in `values.yaml` wins on conflict. -These contracts are declared in `packages/system/-rd/cozyrds/.yaml` under `spec.secrets.include.resourceNames` and `spec.services.include.resourceNames` — authoritative per cozystack version. +Emit findings in a table: -In Phase 7 the app chart emits one Pattern C CR per dependency (template `.yaml`), mapping chart values onto the CR's spec. The main workload HelmRelease (see Phase 7 Main workload) then references the downstream-emitted Secret via `valuesFrom` and targets the downstream Service via `values`. +| Dependency | Signals | Wiring path(s) | Subchart to disable | +| --- | --- | --- | --- | +| postgres | Chart dep `postgresql-ha`; values keys `gitea.config.database.*` | `gitea.config.database.HOST`, `…USER`, `…NAME`, `…PASSWD` | `postgresql-ha.enabled: false` | +| redis | Values keys `gitea.config.cache.*`, `gitea.config.session.*` | `gitea.config.cache.HOST`, `…PASSWORD` | `redis-cluster.enabled: false` | -Spec parameters to collect depend on the sibling CR's own `openAPISchema`. Common fields: +Present the table to the user via `AskUserQuestion`: "Detected these dependencies from the chart. Add, remove, or mark any as optional?" This is where heuristics can be corrected — the user might know the app can use one of several databases, or that a detected cache is optional. -- `Postgres`: `size`, `replicas`, `users` (map of `: password: `), `databases` (map of `: roles: { admin: [users] }`). -- `Redis`: `size`, `replicas`, `authEnabled` (default `true`), `storageClass`. -- `MariaDB`, `MongoDB`, `Kafka`, `ClickHouse`: consult their ApplicationDefinition under `packages/system/-rd/cozyrds/.yaml`. +**When `$CHART_SOURCE = custom`:** there is no chart to introspect. Ask the user directly which backing services the app needs and, for each, which config file / env variable the app reads. -### Pattern A — In-chart operator CR (system-style) +### Step 2 — Contract resolution (per dependency) -The app chart creates the operator CR itself (e.g., a CNPG `Cluster` or Spotahome `RedisFailover`) instead of a cozystack-level sibling CR. The app chart owns both the CR and its output Secret. No separate dashboard entity for the dependency. +For each dependency from Step 1, resolve a `$DEP_CONTRACT` from cozystack. Try these sources in order; stop at the first success: -Use Pattern A only when a Pattern C sibling does not exist for the dependency, or when the app is explicitly system-scoped (like cozystack's own `harbor` or `keycloak`, which predate the sibling-CR pattern). For tenant-facing external apps, prefer Pattern C. +1. **Local cozystack checkout** — when `$COZYSTACK_REPO` is set (or detectable as a sibling dir of `$REPO_DIR`). Read: -#### Step 1 — Research the dependency (mandatory) + ```bash + cat $COZYSTACK_REPO/packages/system/-rd/cozyrds/.yaml + ``` + +2. **GitHub API** — when `gh` CLI is available. Fetch directly from upstream cozystack: + + ```bash + gh api repos/cozystack/cozystack/contents/packages/system/-rd/cozyrds/.yaml \ + --jq .content | base64 --decode + ``` -Before asking the user for spec values, consult the **Dependency catalog** (appendix at the bottom of this skill). For each Pattern A dependency, record four facts: +3. **Live cluster** — when `kubectl` has a usable context (`kubectl config current-context` succeeds) AND the dep's ApplicationDefinition is installed: -1. **CR identity**: `apiVersion` and `kind` of the resource the chart will create (e.g., `postgresql.cnpg.io/v1 Cluster`, `databases.spotahome.com/v1 RedisFailover`). -2. **Output Secret** — who creates it (operator or chart), its name template, and its keys. -3. **CR ↔ Secret wiring** — whether the CR auto-produces the Secret (CNPG), or the chart must create the Secret and point the CR at it (RedisFailover `auth.secretPath`). -4. **App-side consumption** — env via `secretKeyRef`, volume mount, or config file. + ```bash + kubectl get applicationdefinition --output yaml + ``` -If the dependency is in the catalog, copy its facts into the conversation so later phases can refer to them. If it is not, run the research procedure described in the catalog appendix before proceeding. **Never invent CR shapes, secret names, or secret keys.** When research is inconclusive, stop and ask the user. +Confirm the current context is the intended cozystack cluster before relying on this source (read-only operation, but still worth double-checking). This source is authoritative for *that specific cluster version* — if sources 1 or 2 disagree, prefer the live source and note the drift to the user. -#### Step 2 — Collect spec values +From the resolved document, extract and record `$DEP_CONTRACT.`: -The values to collect depend on the CR, not on a generic list. Use the fields exposed by the CR spec as recorded in Step 1. Common groupings: +| Contract field | Source path in the ApplicationDefinition | +| --- | --- | +| `kind` | `spec.application.kind` (`Postgres`, `Redis`, `MariaDB`, …) | +| `plural` | `spec.application.plural` | +| `prefix` | `spec.release.prefix` (`postgres-`, `redis-`, …) | +| `secretTemplates` | `spec.secrets.include[].resourceNames` (list of Go-template strings using `{{ .name }}`) | +| `serviceTemplates` | `spec.services.include[].resourceNames` | +| `specSchema` | `spec.application.openAPISchema` (JSON-parseable; drives Step 4) | +| `apiVersion` | `apiVersion` of the ApplicationDefinition itself (typically `cozystack.io/v1alpha1`) | -- postgres (CNPG `Cluster`): database name, database user, replicas (default `2`), storage size (default `5Gi`). -- redis (Spotahome `RedisFailover`): replicas (default `3`), storage size (default `2Gi`), password source (random generated or user-supplied via `values.yaml`). -- mongodb (Percona `PerconaServerMongoDB`): replica-set size, storage size, users to seed. -- Other deps: consult the CR schema captured in Step 1. +If all three sources fail, record Pattern C as **unavailable** for this dependency. The user must install the ApplicationDefinition in the target cluster, provide `$COZYSTACK_REPO`, or fall back to Pattern A or B. -#### Step 3 — Collect env mapping +### Step 3 — Integration pattern choice -Use the Secret name and keys recorded in Step 1 — not a hardcoded list. For every environment variable the app expects, record which Secret key it maps to. Ask the user to confirm the app's expected env names. +For each dependency with a resolved `$DEP_CONTRACT`, offer the integration pattern via `AskUserQuestion`. Default is **Pattern C**; Pattern A and Pattern B are opt-ins. Pattern C is unavailable (greyed out) when Step 2 failed. -Example for postgres via CNPG (Secret `{{ .Release.Name }}-db-app`, keys `host`, `port`, `username`, `password`, `dbname`): +- **Pattern C — Sibling cozystack ApplicationDefinition (recommended for external apps).** The app chart emits a `${DEP_CONTRACT.kind}` CR; cozystack reconciles it into its own HelmRelease; the app consumes the resulting Secret and Service. See the **Pattern C** subsection below. +- **Pattern A — In-chart operator CR (system-style escape hatch).** The app chart creates the operator CR directly (CNPG `Cluster`, Spotahome `RedisFailover`, etc.). Use when no cozystack ApplicationDefinition exists for the dep or when the app is explicitly system-scoped (harbor/keycloak style). See the **Pattern A** subsection. +- **Pattern B — External reference.** The user provides connection details via values; the app chart provisions nothing. See the **Pattern B** subsection. -```yaml -DB_HOST: secretKeyRef → {{ .Release.Name }}-db-app → host -DB_PORT: secretKeyRef → {{ .Release.Name }}-db-app → port -DB_USERNAME: secretKeyRef → {{ .Release.Name }}-db-app → username -DB_PASSWORD: secretKeyRef → {{ .Release.Name }}-db-app → password -DB_NAME: secretKeyRef → {{ .Release.Name }}-db-app → dbname -``` +### Pattern C — Sibling cozystack ApplicationDefinition -Example for redis via Spotahome (chart-created Secret `{{ .Release.Name }}-redis-auth`, key `password`): +The app chart creates a `${DEP_CONTRACT.kind}` CR (e.g., `Postgres`, `Redis`) inside its own templates. The cozystack controller reconciles that CR into a HelmRelease named `${DEP_CONTRACT.prefix}`, deploying the corresponding `packages/apps//` chart — the same chart a tenant invokes through the dashboard. -```yaml -REDIS_HOST: value → rfs-{{ .Release.Name }}-redis # sentinel service from RedisFailover -REDIS_PORT: value → "26379" -REDIS_PASSWORD: secretKeyRef → {{ .Release.Name }}-redis-auth → password -``` +Why this is the default for external apps: -If the app expects a compound value (`DATABASE_URL`, `REDIS_URL`, etc.), note the assembly pattern — it usually needs a Helm template expression, not a direct `secretKeyRef`. +- The sibling instance shows up in the dashboard as a first-class entity. A tenant can list, inspect, back up, and restore it independently of the wrapping app. +- WorkloadMonitor, PodMonitor, backup schedules, and migrations shipped by cozystack's own `apps//` chart apply automatically — none of that needs to be re-implemented per app. +- Upgrading cozystack upgrades the dependency wiring for every consumer at once. + +All wiring is driven by `$DEP_CONTRACT` from Step 2 — never hardcode prefixes, secret names, or service names. Phase 7 emits one Pattern C template per dep (`templates/.yaml`) and wires the main workload HelmRelease's `values` / `valuesFrom` against `$DEP_CONTRACT.secretTemplates` and `$DEP_CONTRACT.serviceTemplates` substituted with the CR's own name. + +### Pattern A — In-chart operator CR (system-style) + +The app chart creates the operator CR itself (e.g., CNPG `Cluster`, Spotahome `RedisFailover`) and owns both the CR and its output Secret. No separate dashboard entity for the dep. Use when a cozystack ApplicationDefinition for the dep is unavailable, or when the app is explicitly system-scoped (harbor, keycloak). For tenant-facing external apps, prefer Pattern C. + +Pattern A still requires research: the operator CR shape and its Secret/Service output convention are operator-specific. The **Pattern A catalog** appendix records the verified shapes for CNPG and Spotahome. For any operator not in that catalog, follow the research procedure in the appendix before writing Phase 7 templates. ### Pattern B — External reference -The app expects a pre-existing service. The user provisions it separately (e.g., via the Cozystack dashboard postgres app) and passes connection details as values. +The app expects a pre-existing service. The user provisions it separately and passes connection details as values — typically `postgres.host`, `postgres.port`, `postgres.secretName`. The app chart does not provision anything. Collect which values.yaml fields to expose and how the app consumes them (env vars, config mount). + +### Step 4 — Collect spec values + +Drive field selection from `$DEP_CONTRACT.specSchema` (Pattern C) or from the operator CR schema researched for Pattern A — not a hand-picked list. For Pattern C postgres, `specSchema` declares `size`, `replicas`, `users`, `databases`, `external`, `storageClass`, etc.; collect defaults for each the user expects to expose. For Pattern B, the values schema is whatever the user and the app agree on. + +### Step 5 — Record wiring mapping + +The shape of the wiring record depends on chart source: + +- **Upstream chart (`$CHART_SOURCE = upstream`)**: record a list of `{targetPath, source}` entries. `targetPath` is one of the value paths surfaced in Step 1; `source` is either an inline value (e.g., the Service hostname) or a `valuesFrom` reference using `$DEP_CONTRACT.secretTemplates` + the CR's name. + + Example for Gitea + Pattern C postgres (CR named `{{ .Release.Name }}-db`, contract prefix `postgres-`, secret template `postgres-{{ .name }}-credentials`): + + ```yaml + values: + gitea: + config: + database: + DB_TYPE: postgres + HOST: postgres-{{ .Release.Name }}-db-rw:5432 # from $DEP_CONTRACT.serviceTemplates + NAME: {{ .Values.database.name }} + USER: {{ .Values.database.user }} + valuesFrom: + - kind: Secret + name: postgres-{{ .Release.Name }}-db-credentials # from $DEP_CONTRACT.secretTemplates + valuesKey: {{ .Values.database.user }} # see Secret key convention per catalog entry + targetPath: gitea.config.database.PASSWD + ``` -Collect: -- Which values.yaml fields to expose (e.g., `postgres.host`, `postgres.port`, `postgres.secretName`) -- How the app consumes them (env vars, config file mount, etc.) +- **Custom chart (`$CHART_SOURCE = custom`)**: record container env entries. `valueFrom.secretKeyRef` targets `$DEP_CONTRACT.secretTemplates` (Pattern C) or the operator's output Secret (Pattern A). Inline values come from services or helm expressions. -Present a summary of all dependencies with chosen patterns. Proceed only after user confirms. +Present a single summary table of all dependencies with: name, chosen pattern, resolved kind, resolved secret/service, wiring targets. Proceed only after user confirms. ## Phase 5 — Register upstream Helm chart sources (conditional) From 6c0985ed0ebfbd5a322c8bce369e4bd56e6b6d93 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:47:02 +0300 Subject: [PATCH 22/30] refactor(cozy-external-app): treat catalog as reference, not enumeration Phase 4 now resolves dependency contracts at runtime from cozystack itself, so the catalog appendix stops trying to enumerate every supported dep. Rewrite the intro as an interpretation guide that shows how to read an ApplicationDefinition into a $DEP_CONTRACT record, keep postgres and redis as worked examples so the assistant can self-check its parsing, and delete the "other dependencies" hand-wave that had become dead weight. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 63a754f..7b73e24 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -895,15 +895,41 @@ Print a report: ## Dependency catalog -The skill supports three integration patterns (see Phase 4): +Contracts are **resolved at runtime** in Phase 4 Step 2. This appendix is a reference, not a pre-baked list of supported deps — two worked examples below (postgres, redis) show what a correctly resolved `$DEP_CONTRACT` looks like so the assistant can verify its own parsing and the user can eyeball a familiar case. -- **Pattern C** — app chart creates a cozystack-level sibling CR (`Postgres`, `Redis`, …). Default for external apps. -- **Pattern A** — app chart creates the operator CR directly (CNPG `Cluster`, Spotahome `RedisFailover`). System-style; rarely appropriate for external apps. -- **Pattern B** — user provides a pre-existing service via values. Unchanged from plain Helm. +For any dependency the skill has not seen before, run Phase 4 Step 2 against cozystack itself. There is no need — and no place — to keep a hand-maintained enumeration of every supported dep; cozystack's `packages/system/-rd/cozyrds/.yaml` is the single source of truth. -Pattern C entries below capture the naming contract that cozystack controllers commit to: the downstream HelmRelease name, the Secret the downstream chart creates, and the Services it exposes. These values are pulled directly from `packages/system/-rd/cozyrds/.yaml` — authoritative per cozystack version. If a cozystack upgrade changes a prefix or resourceName pattern, the catalog here must be re-verified. +### Interpreting a resolved contract -Pattern A entries record the operator-CR shape for cases where Pattern C is not available or not desired. +An ApplicationDefinition document has this structure (minimal view — see `cozystack.io_applicationdefinitions.yaml` CRD for the full schema): + +```yaml +apiVersion: cozystack.io/v1alpha1 +kind: ApplicationDefinition +metadata: + name: +spec: + application: + kind: # → $DEP_CONTRACT.kind + plural: # → $DEP_CONTRACT.plural + openAPISchema: | # → $DEP_CONTRACT.specSchema (drives Phase 4 Step 4) + { ... JSON ... } + release: + prefix: - # → $DEP_CONTRACT.prefix + chartRef: ... + secrets: + include: + - resourceNames: + - {{ .name }}- # → $DEP_CONTRACT.secretTemplates + services: + include: + - resourceNames: + - {{ .name }}- # → $DEP_CONTRACT.serviceTemplates +``` + +`{{ .name }}` in the resourceNames is the **name of the cozystack-level CR the app chart emits**, not the app's own `.Release.Name`. Concretely: when the app chart emits `${DEP_CONTRACT.kind}/{{ .Release.Name }}-db`, substitute `{{ .name }} = -db` in every template. + +The next two sections show the result of the resolution procedure for postgres and redis — verified against upstream cozystack `main` at the time this skill was written. When a cozystack release changes these contracts, the sections below go stale; trust runtime resolution, not this appendix. ### Pattern C — postgres (cozystack `Postgres`) @@ -1000,17 +1026,6 @@ valuesFrom: targetPath: app.config.redis.password ``` -### Pattern C — other dependencies - -For `MariaDB`, `MongoDB`, `Kafka`, `ClickHouse`, `RabbitMQ`, `FoundationDB`, `Qdrant`, `OpenSearch`, `NATS`, `Openbao` — read the corresponding `packages/system/-rd/cozyrds/.yaml` and extract: - -- `spec.application.kind` — sibling CR kind. -- `spec.release.prefix` — HelmRelease name prefix. -- `spec.secrets.include.resourceNames` — exact Secret naming template (often `{{ .name }}-credentials` or similar). -- `spec.services.include.resourceNames` — Service naming templates. - -Do not extrapolate from the postgres/redis entries above. Each ApplicationDefinition declares its own naming contract. - ### Pattern A — research procedure (for dependencies not in the Pattern A catalog below) 1. Locate a reference in the cozystack monorepo: From b80316df0ebe82f323e8a6d4777d8341535fc1ea Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:47:43 +0300 Subject: [PATCH 23/30] feat(cozy-external-app): surface contract-resolution source in pre-flight Phase 2 now detects which of the three contract sources (local cozystack checkout, gh api, live cluster) is available and records $COZYSTACK_CONTRACT_SOURCE. Pattern C is marked unavailable up front when no source is reachable, preventing the skill from silently falling back to invented contracts. Tighten guardrails to forbid skipping either resolution step, and add the source-priority pointer to the References section. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 7b73e24..07bd06b 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -37,6 +37,12 @@ Bail early if any check fails. ``` Do not install it automatically. Stop. 3. **No collision**: verify `packages/apps/$APP_NAME/` does not already exist. If it does, stop and ask the user whether to overwrite or pick a different name. +4. **Cozystack contract resolution source**: Phase 4 Step 2 needs to read `packages/system/-rd/cozyrds/.yaml` from cozystack. Detect the best available source in this order and record it as `$COZYSTACK_CONTRACT_SOURCE`: + - `local` — `$COZYSTACK_REPO` is set and points at a cozystack checkout containing `packages/system/`. Fastest, works offline. + - `github` — `gh` CLI is authenticated (`gh auth status` succeeds). Resolves against `cozystack/cozystack@main`. + - `cluster` — `kubectl config current-context` succeeds and the target context is a cozystack cluster. Authoritative for that cluster version; read-only. + + If none is available, warn the user. Phase 4 Pattern C will be unavailable; only Patterns A and B remain. ## Phase 3 — Gather app specification @@ -1094,7 +1100,8 @@ Unlike CNPG, the Spotahome operator does NOT emit connection details. The chart - **Never** commit or push on behalf of the user. This is a generate-only skill. - **Never** apply anything to a cluster — no `kubectl apply`, no `helm install`, no `make apply`. This skill only creates files. - **Never** overwrite existing `packages/apps/$APP_NAME/` without explicit user confirmation. -- **Never** guess a dependency's CR shape, Secret name, or Secret keys. For every Pattern A dependency the research step in Phase 4 is mandatory — use the Dependency catalog appendix first; for anything not in the catalog, open a reference implementation in the cozystack monorepo before writing templates. If research does not yield a verified answer, stop and ask. +- **Never** skip Phase 4 Step 1 (chart requirement analysis) or Step 2 (contract resolution). Every Pattern C dependency must have a `$DEP_CONTRACT` record with `kind`, `prefix`, `secretTemplates`, `serviceTemplates`, and `specSchema` resolved from a cozystack source before Phase 7 emits any template. No speculation — if resolution fails, Pattern C is unavailable for that dep. +- **Never** guess a dependency's CR shape, Secret name, or Secret keys. For Pattern A deps the operator-CR research step is mandatory — use the Pattern A catalog first; for anything not in the catalog, open a reference implementation in the cozystack monorepo before writing templates. If research does not yield a verified answer, stop and ask. - **Never** copy the postgres/CNPG wiring onto a different dependency. CNPG auto-creates the credentials Secret; Spotahome RedisFailover does not (the chart creates it). Other operators differ further — always verify. - **Never** edit files in a cozystack checkout used as reference — those are read-only. - **Never** modify `init.yaml` — the user manages their GitRepository and root HelmRelease manually. @@ -1113,6 +1120,7 @@ Read these files on demand when reasoning about structure and conventions: - `packages/core/platform/templates/namespaces.yaml` — existing namespace entries - `scripts/package.mk` — make targets: `show`, `apply`, `diff`, `suspend`, `resume`, `delete`. Requires `NAME` and `NAMESPACE` exports. - `init.yaml` — GitRepository name and root HelmRelease (needed for sourceRef in CRD and HelmRelease) +- `packages/system/-rd/cozyrds/.yaml` — authoritative contract per dep. Resolved at runtime by Phase 4 Step 2 from `$COZYSTACK_REPO`, `gh api repos/cozystack/cozystack`, or `kubectl get applicationdefinition ` in that order. - Cozystack external apps docs: https://cozystack.io/docs/applications/external/ - Flux HelmRelease spec (dependsOn): https://fluxcd.io/flux/components/helm/helmreleases/ - CloudNativePG Cluster CRD: https://cloudnative-pg.io/documentation/current/cloudnative-pg.v1/ From 14559c1530d3c495fc983e8f7749391b20f2557e Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 15:55:27 +0300 Subject: [PATCH 24/30] feat(cozy-external-app): add phase 5.5 present-plan gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill previously gated file writes with a per-phase AskUserQuestion in phases 6, 7, and 8 — the user never saw the full picture before files started landing on disk. Phase 5.5 assembles everything decided in phases 1-5 (app summary, chart source, contract source, exact file list, wiring table, open items) into one plan and asks for a single approve/edit/abort. Once approved via $PLAN_APPROVED, phases 6-8 skip their individual confirmations and run through without extra prompts. The per-phase gates remain as a fallback for runs where 5.5 was not reached or the plan was rejected. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 68 ++++++++++++++++++- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 07bd06b..1f654e7 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -219,6 +219,68 @@ For each source needed, use `AskUserQuestion` to collect: No files are created in this phase. All source and release resources are written in Phase 8 alongside the other platform resources. +## Phase 5.5 — Present plan + +Before any file is written, assemble every decision gathered so far into a single plan and present it to the user once. This is the gate — after approval, Phases 6–8 proceed without further prompts (except critical ones, e.g., existing-file collision). Without this consolidated view, the user never sees the whole picture until files start appearing on disk. + +Build a plan document with five sections: + +1. **App summary** — `$APP_NAME`, Kind, Plural, dashboard metadata (Display Name, Description, Category, Tags), icon status (supplied / placeholder). +2. **Chart source** — `$CHART_SOURCE` (`upstream` or `custom`). For `upstream`, include `$SOURCE_REPO_URL`, chart name, version. For `custom`, note "no upstream wrapper, templates authored from scratch". +3. **Contract source** — which of `local` / `github` / `cluster` was used in Phase 4 Step 2 (`$COZYSTACK_CONTRACT_SOURCE`). Flag if cozystack `main` was used vs. a pinned version. +4. **Files to create / modify** — explicit path list, grouped: + - `packages/apps/$APP_NAME/Chart.yaml`, `Makefile`, `values.yaml`, `values.schema.json`, `README.md`, `.helmignore`, `logos/$APP_NAME.svg`. + - `packages/apps/$APP_NAME/templates/.yaml` per Pattern C/A dependency (e.g., `postgres.yaml`, `redis.yaml`). + - `packages/apps/$APP_NAME/templates/$APP_NAME.yaml` (HelmRelease wrapper or Deployment). + - `packages/core/platform/templates/{namespaces,helmrepositories,helmreleases,helmcharts,cozyrds}.yaml` — only the ones that actually get modified, with the specific entry each receives. +5. **Dependency wiring table** — for every dep, one row: chosen pattern, resolved kind, CR name template, output Secret template + key, output Service + port, upstream chart `targetPath` (for HelmRelease wrapper) or container env name (for custom chart). This is the table from Phase 4 Step 5, restated for the approval gate. + +Also list any **open items** explicitly: missing icon, missing upstream chart version, skipped dependency pattern, unresolved `$DEP_CONTRACT`. The user should not be surprised later. + +Example for `/cozy-external-app gitea`: + +```text +App : gitea (Kind: Gitea, Plural: giteas) +Dashboard : "Gitea" / "Self-hosted Git service" / Developer tools / git, vcs +Chart source : upstream — https://dl.gitea.com/charts gitea@12.0.1 (HelmRelease wrapper) +Contract source : local ($COZYSTACK_REPO=/Users/kitsunoff/git/github.com/cozystack/cozystack) + +Create (packages/apps/gitea/): + Chart.yaml, Makefile, values.yaml, values.schema.json, README.md, .helmignore + logos/gitea.svg ⚠ placeholder — supply real SVG before make generate + templates/postgres.yaml Pattern C → apps.cozystack.io/v1alpha1 Postgres + templates/redis.yaml Pattern C → apps.cozystack.io/v1alpha1 Redis + templates/gitea.yaml HelmRelease wrapping gitea/gitea:12.0.1 + +Modify (packages/core/platform/templates/): + namespaces.yaml +1 entry: external-gitea + helmrepositories.yaml +1 entry: gitea → dl.gitea.com/charts (namespace: external-gitea) + helmcharts.yaml +1 entry: external-apps-gitea → ./packages/apps/gitea (namespace: cozy-public) + cozyrds.yaml +1 ApplicationDefinition: gitea (kind Gitea) + +Dependencies: + postgres Pattern C Postgres/{{ .Release.Name }}-db + Secret postgres-{{ .Release.Name }}-db-credentials key=gitea → gitea.config.database.PASSWD + Service postgres-{{ .Release.Name }}-db-rw:5432 → gitea.config.database.HOST + + redis Pattern C Redis/{{ .Release.Name }}-redis + Secret redis-{{ .Release.Name }}-redis-auth key=password → gitea.config.cache.PASSWORD + Service rfs-redis-{{ .Release.Name }}-redis:26379 → gitea.config.cache.HOST (sentinel URL) + +Subcharts disabled in upstream values: postgresql-ha, redis-cluster + +Open items: + - Icon not supplied; placeholder will be written. Phase 9 validation will fail until an SVG is placed. +``` + +Use `AskUserQuestion` with three options: + +- `approve` — record `$PLAN_APPROVED = true`, proceed to Phase 6 without per-phase confirmations. +- `edit` — ask which phase to revisit (3 for app metadata, 4 for dependencies, 5 for chart sources), then re-run from there and re-present the plan. +- `abort` — stop. No files are written. + +Record `$PLAN_APPROVED` state; Phases 6, 7, and 8 read it to decide whether to skip their individual write-time confirmations. + ## Phase 6 — Create app chart skeleton Create `packages/apps/$APP_NAME/` with these files: @@ -678,7 +740,7 @@ spec: {{- end }} ``` -Use `AskUserQuestion` to confirm the generated templates before writing them. Show a summary of what will be created. +If `$PLAN_APPROVED` was set in Phase 5.5, write the templates directly. Otherwise use `AskUserQuestion` to confirm the generated templates before writing them and show a summary of what will be created. ## Phase 8 — Register in core/platform @@ -847,7 +909,7 @@ sed 's/^/ /' $REPO_DIR/packages/apps/$APP_NAME/values.schema.json Verify the final YAML with `yq e '.' cozyrds.yaml > /dev/null` before moving on — an off-by-one indentation silently breaks the schema. -Use `AskUserQuestion` to confirm all core/platform changes before writing. Show the diff of what will be appended to each file. +If `$PLAN_APPROVED` was set in Phase 5.5, write the platform-level additions directly. Otherwise use `AskUserQuestion` to confirm all core/platform changes before writing and show the diff of what will be appended to each file. ## Phase 9 — Validation @@ -1105,7 +1167,7 @@ Unlike CNPG, the Spotahome operator does NOT emit connection details. The chart - **Never** copy the postgres/CNPG wiring onto a different dependency. CNPG auto-creates the credentials Secret; Spotahome RedisFailover does not (the chart creates it). Other operators differ further — always verify. - **Never** edit files in a cozystack checkout used as reference — those are read-only. - **Never** modify `init.yaml` — the user manages their GitRepository and root HelmRelease manually. -- **Always** use `AskUserQuestion` before creating files in Phase 6, 7, and 8. Show what will be created. +- **Always** gate file creation behind user confirmation. The consolidated gate is Phase 5.5 (Present plan); once `$PLAN_APPROVED` is set, Phases 6-8 proceed without further prompts. If Phase 5.5 was skipped or the plan was not approved, fall back to per-phase `AskUserQuestion` confirmations in Phases 6, 7, and 8. - **Always** read existing files before appending to them (`namespaces.yaml`, `helmrepositories.yaml`, `helmreleases.yaml`, `helmcharts.yaml`, `cozyrds.yaml`). - If `cozyvalues-gen` is not installed, do not attempt to generate schema/README manually beyond a minimal placeholder. Tell the user to install it and re-run `make generate`. From 4e252df9fdbe80198dc003f70040ffa9d01c276a Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 16:08:48 +0300 Subject: [PATCH 25/30] fix(cozy-external-app): stop churning postgres password on every render The pattern C Postgres CR used randAlphaNum 32 as the default password, which regenerates on every helm template or flux reconcile. Since the downstream cozystack apps/postgres chart already preserves passwords via Sprig lookup against its own postgres--credentials Secret (templates/init-script.yaml), the external chart should simply not pass a password unless the user explicitly supplied one. Omitting the field lets the downstream chart own password lifecycle end-to-end. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 1f654e7..65e03c8 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -578,7 +578,9 @@ spec: {{- end }} users: {{ .Values.database.user }}: - password: {{ .Values.database.password | default (randAlphaNum 32) | quote }} + {{- with .Values.database.password }} + password: {{ . | quote }} + {{- end }} databases: {{ .Values.database.name }}: roles: @@ -586,6 +588,8 @@ spec: - {{ .Values.database.user }} ``` +Omit `password` when the user did not supply one. The downstream `packages/apps/postgres/` chart (`templates/init-script.yaml`) already uses Sprig's `lookup` to read the existing `postgres-{{ .Release.Name }}-db-credentials` Secret and reuses the password on subsequent renders. Passing `randAlphaNum` from this chart would generate a fresh value on every reconcile and silently rotate the Secret mid-run. + Outputs (rendered by the downstream `packages/apps/postgres/` chart): - Secret `postgres-{{ .Release.Name }}-db-credentials` — one key per user, value is the password. @@ -1025,7 +1029,9 @@ spec: external: false users: {{ .Values.database.user }}: - password: {{ .Values.database.password | default (randAlphaNum 32) | quote }} + {{- with .Values.database.password }} + password: {{ . | quote }} + {{- end }} databases: {{ .Values.database.name }}: roles: @@ -1033,6 +1039,8 @@ spec: - {{ .Values.database.user }} ``` +`password` is omitted unless the user explicitly supplied one — see the rationale in Phase 7's `postgres.yaml` section. The downstream `packages/apps/postgres/` chart generates and preserves the password itself via `lookup`. + Wiring in the main workload HelmRelease: ```yaml From 54c4861080d00ef7cfcd44888e34dd72c9bd429a Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 16:09:11 +0300 Subject: [PATCH 26/30] fix(cozy-external-app): document pattern C values.yaml schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 7 pattern C templates read .Values.database.{user,name,password,size,replicas} and .Values.redis.{size,replicas}, but phase 6 only documented pattern A and pattern B values sections. A user following the skill end-to-end on pattern C (the documented default for external apps) ended up with a chart that fails helm template — in every wired gitea.config.database.* path and an empty users map in the Postgres CR. Add a pattern C section ahead of A and B so the values.yaml skeleton actually covers the recommended path. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 65e03c8..87dc7ce 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -345,7 +345,45 @@ size: 10Gi storageClass: "" ``` -**For Pattern A (managed postgres) dependencies**, add: +**For Pattern C (sibling cozystack ApplicationDefinition) dependencies** — the default for external apps — add one section per dep. The fields must match what Phase 7 templates read. Example for postgres + redis: + +```yaml +## +## @section Database configuration +## + +## @typedef {struct} Database - PostgreSQL configuration (managed via the cozystack `Postgres` sibling CR). +## @field {quantity} size - Persistent Volume size for database storage. +## @field {int} replicas - Number of database instances. +## @field {string} user - Database user to create. +## @field {string} name - Database name to create. +## @field {string} [password] - Optional password. When empty, the cozystack postgres chart generates and preserves one via lookup. + +## @param {Database} database - PostgreSQL configuration. +database: + size: 10Gi + replicas: 2 + user: app + name: app + password: "" + +## +## @section Redis configuration +## + +## @typedef {struct} Redis - Redis configuration (managed via the cozystack `Redis` sibling CR). +## @field {quantity} size - Persistent Volume size for redis storage. +## @field {int} replicas - Number of redis replicas. + +## @param {Redis} redis - Redis configuration. +redis: + size: 1Gi + replicas: 2 +``` + +Add or remove sections to match the actual dependency set resolved in Phase 4. Field names must match `.Values..*` references emitted by Phase 7 templates — re-check after editing. + +**For Pattern A (in-chart operator CR) dependencies**, add: ```yaml ## From 5c8aaf1c28f566a51f21638e4960686bbbc61a45 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 18 Apr 2026 16:09:29 +0300 Subject: [PATCH 27/30] fix(cozy-external-app): select GitRepository explicitly in init.yaml parse The phase 8 yq extraction used .metadata.name + head -1 as primary path, picking whichever kind happened to come first in init.yaml. The external-apps-example layout has a GitRepository and a HelmRelease both named external-apps, so the old extraction worked by coincidence; if a user ever renames the repo and the HelmRelease ends up with a different name (a valid layout), head -1 silently grabs the HelmRelease name and every phase 8 sourceRef points at nonexistent source. Make kind=GitRepository the deterministic primary selector. Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 87dc7ce..4efbe84 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -788,19 +788,13 @@ If `$PLAN_APPROVED` was set in Phase 5.5, write the templates directly. Otherwis Update five files under `packages/core/platform/templates/`. Read each file first, then append. -Before generating any YAML in this phase, extract the GitRepository name from `init.yaml` — it is referenced as `sourceRef.name` in both the operator HelmRelease and the ApplicationDefinition below: +Before generating any YAML in this phase, extract the GitRepository name from `init.yaml` — it is referenced as `sourceRef.name` in both the operator HelmRelease and the ApplicationDefinition below. Always select by kind so multi-document `init.yaml` files (the reference layout has both a `GitRepository` and a `HelmRelease`, often sharing a name by coincidence) resolve deterministically: ```bash -GIT_REPO_NAME=$(yq -r '.metadata.name' $REPO_DIR/init.yaml | head -1) +GIT_REPO_NAME=$(yq -r 'select(.kind == "GitRepository") | .metadata.name' $REPO_DIR/init.yaml) ``` -If `init.yaml` contains multiple documents, pick the `GitRepository` kind explicitly: - -```bash -GIT_REPO_NAME=$(yq -r 'select(.kind == "GitRepository") | .metadata.name' $REPO_DIR/init.yaml | head -1) -``` - -Stop and ask the user if the extracted value is empty. +Stop and ask the user if the extracted value is empty or if the selector returns more than one GitRepository. ### namespaces.yaml From fccbbd55c9596e5711893981ea6bf42895e02190 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Wed, 22 Apr 2026 15:01:35 +0300 Subject: [PATCH 28/30] docs(cozy-external-app): address non-blocking review feedback - normalize cozyvalues-gen invocation to long flags, matching Makefile - split helmrepositories.yaml into explicit HTTPS and OCI variants so the skill never emits a commented-out type line - drop undefined sibling-dir detection clause from Phase 4 Step 2; Phase 2 Step 4 only reads $COZYSTACK_REPO - cite Spotahome RedisFailover default next to every sentinelMaster: mymaster so the value is traceable, not a guess - frame the Phase 7 HelmRelease example as a two-dep illustration and warn against copying unrelated dependsOn or subchart toggles Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 4efbe84..15f6888 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -101,7 +101,7 @@ Present the table to the user via `AskUserQuestion`: "Detected these dependencie For each dependency from Step 1, resolve a `$DEP_CONTRACT` from cozystack. Try these sources in order; stop at the first success: -1. **Local cozystack checkout** — when `$COZYSTACK_REPO` is set (or detectable as a sibling dir of `$REPO_DIR`). Read: +1. **Local cozystack checkout** — when `$COZYSTACK_REPO` is set. Read: ```bash cat $COZYSTACK_REPO/packages/system/-rd/cozyrds/.yaml @@ -423,7 +423,7 @@ postgres: Generate via: ```bash -cd $REPO_DIR/packages/apps/$APP_NAME && cozyvalues-gen -v values.yaml -s values.schema.json -r README.md +cd $REPO_DIR/packages/apps/$APP_NAME && cozyvalues-gen --values values.yaml --schema values.schema.json --readme README.md ``` If `cozyvalues-gen` fails, write a minimal valid JSON schema manually based on values.yaml fields. Verify with: @@ -453,6 +453,8 @@ Generate the primary workload template. If Phase 3 recorded `$CHART_SOURCE = ups The HelmRelease registered below references the `HelmRepository` created in Phase 8 and injects cozystack-wired connection details via `values` and `valuesFrom`. `valuesFrom` is the cleanest way to pipe a password out of a Secret created by a Pattern C sibling CR (see Phase 4 Pattern C, Dependency catalog appendix) directly into the upstream chart's value path — no Deployment env rewriting required. +The example below shows a Gitea-like app with **both** postgres and redis resolved in Phase 4. Treat it as an illustration, not a template to copy verbatim: emit a `dependsOn` entry and a `values..enabled: false` line only for dependencies actually recorded in Phase 4 Step 1/Step 2. An app with postgres only must not carry the redis entries, and vice versa — an unrelated `dependsOn` target blocks reconciliation until it appears (or forever, if it never will). + ```yaml apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease @@ -471,12 +473,17 @@ spec: # dependsOn ensures sibling cozystack CRs reconcile before this release tries to use their outputs. # Reference the generated HelmReleases (named ``) that cozystack controllers # produce from the Pattern C sibling CRs you emit in templates/postgres.yaml, templates/redis.yaml, etc. + # Emit one entry per dependency resolved in Phase 4 — drop these two lines for apps without that dep. dependsOn: - name: postgres-{{ .Release.Name }}-db namespace: {{ .Release.Namespace }} - name: redis-{{ .Release.Name }}-redis namespace: {{ .Release.Namespace }} # Disable the upstream chart's bundled subcharts — we provide backing services via Pattern C. + # Include only the subchart aliases that the upstream chart actually ships (discovered in + # Phase 4 Step 1 as `postgresql`, `postgresql-ha`, `redis`, `redis-cluster`, etc.). Emitting a + # toggle for a subchart the upstream does not declare is a no-op; emitting one for the *wrong* + # alias silently leaves the bundled subchart running. values: postgresql: { enabled: false } postgresql-ha: { enabled: false } @@ -495,6 +502,9 @@ spec: redis: host: rfs-redis-{{ .Release.Name }}-redis port: 26379 + # "mymaster" is Spotahome RedisFailover's default sentinel monitor + # name; cozystack's packages/apps/redis does not override it. Change + # only if a future cozystack release documents a different default. sentinelMaster: mymaster # Secrets must be read at reconcile time — never inline passwords into values. valuesFrom: @@ -817,7 +827,23 @@ Emit one entry for each `$SOURCE_NAMESPACE` gathered (deduplicate if operator an ### helmrepositories.yaml -For every source gathered in Phase 5 (operator and/or main), append a `HelmRepository`: +For every source gathered in Phase 5 (operator and/or main), append a `HelmRepository`. Emit exactly one of the two variants below based on `$SOURCE_REPO_TYPE` — do not leave commented lines in the output. + +**HTTPS source** (`$SOURCE_REPO_TYPE = https`): + +```yaml +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: $SOURCE_REPO_NAME + namespace: $SOURCE_NAMESPACE +spec: + interval: 5m + url: $SOURCE_REPO_URL +``` + +**OCI source** (`$SOURCE_REPO_TYPE = oci`): ```yaml --- @@ -828,9 +854,8 @@ metadata: namespace: $SOURCE_NAMESPACE spec: interval: 5m + type: oci url: $SOURCE_REPO_URL - # If $SOURCE_REPO_TYPE is `oci`, uncomment the next line: - # type: oci ``` ### helmreleases.yaml @@ -1126,6 +1151,9 @@ values: redis: host: rfs-redis-{{ .Release.Name }}-redis port: 26379 + # Spotahome RedisFailover default sentinel monitor name, not overridden + # by cozystack's packages/apps/redis. See the note above in the main + # workload example. sentinelMaster: mymaster valuesFrom: - kind: Secret From a7de97b348e238016a0ae2067913197165c6e4f6 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Wed, 22 Apr 2026 15:18:42 +0300 Subject: [PATCH 29/30] fix(cozy-external-app): address self-review blockers and observations Blockers: - main HelmRelease no longer bakes $APP_DB_USER / $APP_DB_NAME at generation time; use {{ .Values.database.user }} / .name so the values block and valuesFrom.valuesKey stay in sync with templates/ postgres.yaml when a tenant overrides database.user at deploy time - Pattern A Spotahome RedisFailover Secret now inlines the full lookup-then-randAlphaNum idiom from cozystack/packages/apps/redis/ templates/redisfailover.yaml, preventing the same password-churn class previously fixed for Pattern C postgres Observations: - Phase 3 collects Display Name plural separately; Phase 8 cozyrds now renders plural: $APP_DISPLAY_PLURAL instead of reusing the singular, matching the cozystack external-apps-example reference - cozyrds tags template shows two placeholders with a comment requiring one list item per collected tag - $SOURCE_REPO_TYPE is now https (not empty) for HTTPS sources so Phase 8's variant selector reads the exact value it documents - Phase 3 icon note and Phase 5.5 open-items line reference Phase 8 base64 as the actual failure point, not Phase 9 validation Co-Authored-By: Claude Signed-off-by: ZverGuy --- .../skills/cozy-external-app/SKILL.md | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index 15f6888..da0b377 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -55,8 +55,8 @@ Use `AskUserQuestion` to collect: 2. **Container image**: image reference (e.g., `ghcr.io/immich-app/immich-server:v1.120.0`). 3. **Public port**: does the app expose an HTTP port? If yes, which port number? Should an Ingress template be generated? 4. **Persistent storage**: does the app need a PVC? If yes, default size (e.g., `10Gi`). -5. **Icon**: path to an SVG file for the dashboard. If not available yet, note it — Phase 6 will create a `logos/` placeholder and Phase 9 will fail until the user provides one. -6. **Dashboard metadata**: Display Name (e.g., `Immich`), Description (e.g., `Self-hosted photo and video management solution`), Category (e.g., `Media`), and Tags (comma-separated list, e.g., `photo, video`). +5. **Icon**: path to an SVG file for the dashboard. If not available yet, note it — Phase 6 will create a `logos/` placeholder and Phase 8's `base64 < logos/$APP_NAME.svg` step will fail until the user provides one. +6. **Dashboard metadata**: Display Name singular (e.g., `Immich`), Display Name plural (e.g., `Immichs` — do not infer automatically; apps like `MinIO`, `Nextcloud`, or `Gitea` read incorrectly when the singular is reused), Description (e.g., `Self-hosted photo and video management solution`), Category (e.g., `Media`), and Tags (comma-separated list, e.g., `photo, video`). 7. **Resource definition**: Kind (e.g., `Immich`) and Plural (e.g., `immichs`) for the `ApplicationDefinition` created in Phase 8. Record all answers. Proceed only after user confirms the summary. @@ -211,7 +211,7 @@ For each source needed, use `AskUserQuestion` to collect: - `$SOURCE_ROLE` — `main` (upstream chart for the app itself) or `operator` (dedicated operator chart). - `$SOURCE_REPO_URL` — repository URL. Prefix with `oci://` for OCI registries, otherwise use plain HTTPS. -- `$SOURCE_REPO_TYPE` — `oci` if the URL starts with `oci://`, otherwise leave empty. +- `$SOURCE_REPO_TYPE` — `oci` if the URL starts with `oci://`, otherwise `https`. Phase 8 selects the `HelmRepository` variant by this exact value. - `$SOURCE_CHART_NAME` — chart name inside the repository (e.g., `gitea`, `minecraft-operator`, `immich`). - `$SOURCE_CHART_VERSION` — pinned version or semver range (e.g., `12.0.1`, `>=1.0.0`). Avoid `'*'` in production use. - `$SOURCE_REPO_NAME` — alias used as `HelmRepository.metadata.name`. Default: `$APP_NAME` for `main`, `$APP_NAME-operator` for `operator`. @@ -270,7 +270,7 @@ Dependencies: Subcharts disabled in upstream values: postgresql-ha, redis-cluster Open items: - - Icon not supplied; placeholder will be written. Phase 9 validation will fail until an SVG is placed. + - Icon not supplied; placeholder will be written. Phase 8 will fail on `base64 < logos/$APP_NAME.svg` until an SVG is placed — Phase 9 validation does not look at the icon itself. ``` Use `AskUserQuestion` with three options: @@ -497,8 +497,8 @@ spec: database: host: postgres-{{ .Release.Name }}-db-rw port: 5432 - name: $APP_DB_NAME - user: $APP_DB_USER + name: {{ .Values.database.name }} + user: {{ .Values.database.user }} redis: host: rfs-redis-{{ .Release.Name }}-redis port: 26379 @@ -507,10 +507,13 @@ spec: # only if a future cozystack release documents a different default. sentinelMaster: mymaster # Secrets must be read at reconcile time — never inline passwords into values. + # valuesKey must match the Postgres CR's user name at runtime (the Postgres + # CR in templates/postgres.yaml keys its Secret by `.Values.database.user`), + # so both sides must read the same value — never bake a literal at generation. valuesFrom: - kind: Secret name: postgres-{{ .Release.Name }}-db-credentials - valuesKey: $APP_DB_USER + valuesKey: {{ .Values.database.user }} targetPath: app.config.database.password - kind: Secret name: redis-{{ .Release.Name }}-redis-auth @@ -710,16 +713,21 @@ Outputs (auto-created by the CNPG operator; no chart-side Secret): Reference: `cozystack/packages/system/harbor/templates/redis.yaml`. -The chart creates the Secret **before** the CR — the operator reads it via `spec.auth.secretPath`: +The chart creates the Secret **before** the CR — the operator reads it via `spec.auth.secretPath`. The password is computed with `lookup` so an existing Secret is reused across re-renders; without this, `randAlphaNum` would emit a fresh value on every reconcile and silently rotate the RedisFailover credential. This matches the upstream idiom in `cozystack/packages/apps/redis/templates/redisfailover.yaml`: ```yaml +{{- $existing := lookup "v1" "Secret" .Release.Namespace (printf "%s-redis-auth" .Release.Name) }} +{{- $password := randAlphaNum 32 | b64enc }} +{{- if $existing }} +{{- $password = index $existing.data "password" }} +{{- end }} --- apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-redis-auth -stringData: - password: {{ .Values.redis.password | quote }} +data: + password: {{ $password }} --- apiVersion: databases.spotahome.com/v1 kind: RedisFailover @@ -745,13 +753,13 @@ spec: secretPath: {{ .Release.Name }}-redis-auth ``` +If the user supplied `.Values.redis.password`, prefer it over the random value — replace `$password := randAlphaNum 32 | b64enc` with `$password := .Values.redis.password | b64enc` under a `{{- with .Values.redis.password }}` guard, preserving the `lookup` branch as the upgrade path. + Outputs: - Secret `{{ .Release.Name }}-redis-auth` (chart-created) — key `password`. - Sentinel service `rfs-{{ .Release.Name }}-redis` on port `26379`. -If `.Values.redis.password` is empty, generate a password inline so re-renders are stable — see the [`randAlphaNum`](https://pkg.go.dev/github.com/Masterminds/sprig/v3#hdr-String_Functions) Sprig helper and the `lookup` function to reuse an existing Secret on upgrade. - ### service.yaml (if app exposes a port) ```yaml @@ -938,10 +946,12 @@ spec: dashboard: category: $CATEGORY singular: $APP_DISPLAY_NAME - plural: $APP_DISPLAY_NAME + plural: $APP_DISPLAY_PLURAL description: $APP_DESCRIPTION tags: + # Emit one list item per tag recorded in Phase 3 — do not collapse into a single entry. - $TAG1 + - $TAG2 icon: $ICON_B64 keysOrder: - - apiVersion From 34c04ade767c3fadb2d00b644dc71a4519137da7 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Wed, 22 Apr 2026 15:33:32 +0300 Subject: [PATCH 30/30] docs(cozy-external-app): anchor upstream references by block, not line number Line-number ranges like `sts.yaml:142-168` drift on any upstream refactor. Anchor the three cozystack references by the named template block they point at (`KC_DB_*` env block in keycloak sts.yaml; `database.external` values block in harbor.yaml) so the guidance stays valid across cozystack releases. Co-Authored-By: Claude Signed-off-by: ZverGuy --- skills/cozy-external-app/skills/cozy-external-app/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md index da0b377..b8ad4dd 100644 --- a/skills/cozy-external-app/skills/cozy-external-app/SKILL.md +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -1195,7 +1195,7 @@ valuesFrom: | Operator | CloudNativePG (`cnpg.io`) — provided by `packages/system/cnpg-operator` | | CR | `postgresql.cnpg.io/v1` → `Cluster` | | Reference template | `cozystack/packages/system/harbor/templates/database.yaml` | -| Reference consumer | `cozystack/packages/system/keycloak/templates/sts.yaml:142-168` | +| Reference consumer | `cozystack/packages/system/keycloak/templates/sts.yaml` — the `KC_DB_*` env block | | Output Secret | Auto-created by the operator. If cluster is named `-db`, the Secret is `-db-app`. | | Output Secret keys | `host`, `port`, `username`, `password`, `dbname`, `uri`, `jdbc-uri` | | Superuser Secret | `-db-superuser` (same keys + `superuser`) | @@ -1267,5 +1267,5 @@ Read these files on demand when reasoning about structure and conventions: - CNPG bootstrap initdb: https://cloudnative-pg.io/documentation/current/bootstrap/#initdb - `cozystack/packages/apps/postgres/values.yaml` — reference for cozyvalues-gen annotation style (`@param`, `@typedef`, `@field`, `@enum`, `@section`) - `cozystack/packages/system/harbor/templates/database.yaml` — reference Pattern A: managed CNPG Cluster in chart templates -- `cozystack/packages/system/keycloak/templates/sts.yaml` (lines 142-168) — reference Pattern A: consuming CNPG secret via secretKeyRef with keys `host`, `port`, `username`, `password`, `dbname` -- `cozystack/packages/apps/harbor/templates/harbor.yaml` (lines 132-141) — reference Pattern A: database connection config with `existingSecret` and `host` pointing to CNPG `-rw` service +- `cozystack/packages/system/keycloak/templates/sts.yaml` — reference Pattern A: the `KC_DB_*` env block consumes a CNPG Secret via `secretKeyRef` with keys `host`, `port`, `username`, `password`, `dbname` +- `cozystack/packages/apps/harbor/templates/harbor.yaml` — reference Pattern A: the `database.external` values block wires `existingSecret` and points `host` at the CNPG `-rw` service