diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b695f94..862850d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,6 +13,12 @@ "source": "./skills/cozy-deploy", "category": "infrastructure" }, + { + "name": "cozy-external-app", + "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" + }, { "name": "drbd-recovery", "description": "Diagnose and recover DRBD/LINSTOR storage issues in Kubernetes clusters — handles StandAlone, DELETING, Inconsistent, Diskless, quorum loss, bitmap errors, and other common failure modes", diff --git a/README.md b/README.md index b2340bf..53a5962 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 | | **drbd-recovery** | Diagnose and recover DRBD/LINSTOR storage issues in Kubernetes clusters | ## 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..b045f07 --- /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, 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 new file mode 100644 index 0000000..b8ad4dd --- /dev/null +++ b/skills/cozy-external-app/skills/cozy-external-app/SKILL.md @@ -0,0 +1,1271 @@ +--- +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, 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 → 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. + +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`, `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: + 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. +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 + +Use `AskUserQuestion` to collect: + +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`). +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. + +## Phase 4 — Resolve and gather dependencies + +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. + +### Step 1 — Chart requirement analysis + +Discover candidate dependencies from the chart itself. + +**When `$CHART_SOURCE = upstream`:** pull the upstream chart metadata once and inspect it. + +```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 +``` + +For OCI sources use `oci://$SOURCE_REPO_URL/$SOURCE_CHART_NAME` directly — `helm show` handles OCI without `repo add`. + +Extract three signals: + +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. + +Emit findings in a table: + +| 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` | + +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. + +**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. + +### Step 2 — Contract resolution (per dependency) + +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. Read: + + ```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 + ``` + +3. **Live cluster** — when `kubectl` has a usable context (`kubectl config current-context` succeeds) AND the dep's ApplicationDefinition is installed: + + ```bash + kubectl get applicationdefinition --output yaml + ``` + +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. + +From the resolved document, extract and record `$DEP_CONTRACT.`: + +| 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`) | + +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 — Integration pattern choice + +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. + +- **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. + +### Pattern C — Sibling cozystack ApplicationDefinition + +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. + +Why this is the default for external apps: + +- 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 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 + ``` + +- **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 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) + +Flux reconciles external Helm charts via the `HelmRepository` resource. Two situations require a `HelmRepository` registration in this phase: + +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`. + +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: + +- `$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 `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`. +- `$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 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 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: + +- `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: + +### 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 + +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 --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: +```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 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 +## +## @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 --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: +```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 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. + +#### 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. + +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 +metadata: + name: {{ .Release.Name }} +spec: + interval: 5m + chart: + spec: + chart: $SOURCE_CHART_NAME + version: $SOURCE_CHART_VERSION + sourceRef: + kind: HelmRepository + 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. + # 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 } + 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: {{ .Values.database.name }} + user: {{ .Values.database.user }} + 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. + # 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: {{ .Values.database.user }} + targetPath: app.config.database.password + - kind: Secret + name: redis-{{ .Release.Name }}-redis-auth + valuesKey: password + targetPath: app.config.redis.password +``` + +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. + +#### Custom Deployment (fallback) + +```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 +``` + +### Dependency creation templates (Pattern A and Pattern C) + +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 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. + +#### 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 }}: + {{- with .Values.database.password }} + password: {{ . | quote }} + {{- end }} + databases: + {{ .Values.database.name }}: + roles: + admin: + - {{ .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. +- 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`. + +```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" +``` + +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 — Pattern A (in-chart 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`. 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 +data: + password: {{ $password }} +--- +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 +``` + +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`. + +### 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 }} +``` + +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 + +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. 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 'select(.kind == "GitRepository") | .metadata.name' $REPO_DIR/init.yaml) +``` + +Stop and ask the user if the extracted value is empty or if the selector returns more than one GitRepository. + +### namespaces.yaml + +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 +--- +apiVersion: v1 +kind: Namespace +metadata: + labels: + cozystack.io/system: "true" + name: $SOURCE_NAMESPACE +``` + +Emit one entry for each `$SOURCE_NAMESPACE` gathered (deduplicate if operator and main share a namespace by mistake). + +### helmrepositories.yaml + +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 +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: $SOURCE_REPO_NAME + namespace: $SOURCE_NAMESPACE +spec: + interval: 5m + type: oci + url: $SOURCE_REPO_URL +``` + +### helmreleases.yaml + +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: $SOURCE_REPO_NAME + namespace: $SOURCE_NAMESPACE +spec: + interval: 5m + releaseName: $SOURCE_REPO_NAME + targetNamespace: $SOURCE_NAMESPACE + chart: + spec: + chart: $SOURCE_CHART_NAME + sourceRef: + kind: HelmRepository + 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 `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 + +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. + +`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 +--- +apiVersion: cozystack.io/v1alpha1 +kind: ApplicationDefinition +metadata: + name: $APP_NAME +spec: + application: + kind: $APP_KIND + openAPISchema: | + + plural: $APP_PLURAL + singular: $APP_NAME + release: + chartRef: + kind: HelmChart + name: $GIT_REPO_NAME-$APP_NAME + namespace: cozy-public + labels: + cozystack.io/ui: "true" + prefix: $APP_NAME- + dashboard: + category: $CATEGORY + singular: $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 + - - 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`: +```bash +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 `$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 + +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 under `packages/core/platform/templates/`: `namespaces.yaml`, `helmrepositories.yaml`, `helmreleases.yaml`, `helmcharts.yaml`, `cozyrds.yaml`) +- **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**: + 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` + +## Dependency catalog + +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. + +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. + +### Interpreting a resolved contract + +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`) + +| 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 }}: + {{- with .Values.database.password }} + password: {{ . | quote }} + {{- end }} + databases: + {{ .Values.database.name }}: + roles: + admin: + - {{ .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 +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 + # 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 + name: redis-{{ .Release.Name }}-redis-auth + valuesKey: password + targetPath: app.config.redis.password +``` + +### Pattern A — research procedure (for dependencies not in the Pattern A catalog below) + +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` — 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`) | +| 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. +- **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** 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. +- **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`. + +## References + +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 +- `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/ +- 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` — 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