The test that claimed to pin the HelmRelease v2 history shape wrote a HelmRelease document containing the field names it expected, then read that same document back with the same jsonpath. It established that yq can read a fixture the test itself composed. Its own comment described the failure it was meant to catch: a future Flux release renames the field, the jsonpath matches nothing, the guard reports no remediation cycle, and real cycles pass the e2e assertion. A rename moves neither the fixture nor the expression, so the test stays green through exactly that event. The comment also named where the truth lives, github.com/fluxcd/helm-controller/api/v2, and pinned a copy of the author's expectations instead. Replace it with a Go test that supplies neither half. The expression is read out of the shell library that ships it, and the object is built from the upstream type at the version go.mod holds, then serialized and matched the way kubectl does. A renamed or retyped field is a compile error or a failed match; a guard that stops reading history fails the lookup with a message saying so. Both directions were confirmed by mutation. Locating the expression by searching the file is not enough on its own. Inline in a long script, locating it means deciding which occurrences are code and which are prose, and every way of getting that wrong fails green: an example quoted near a broken read satisfies the pin while the guard reads nothing. So the expression moves into a named assignment in remediation-guard.sh, which the guard already sources, and the test sources that library and takes the value the shell is left holding. Deciding which text is the assignment is then not the test's problem: an `export`, an indent, a second assignment later in the file all resolve the way the guard will resolve them, because the thing resolving them is the shell. Sourcing borrows the environment along with the shell, so the snippet unsets the name before it sources. `go test` hands the child its own environment, and a shell that had exported this name would answer in place of a library that stopped assigning it: the value comes back non-empty, the empty check stays quiet, and the pin passes over a guard that reads nothing. That is this file's own subject reaching it through the environment rather than through the source. Moving the expression out of the call site puts a step between what is pinned and what runs, so the call site is checked too, and the check starts from the call rather than from the text: the local the guard is handed must be assigned exactly once, by an anchored assignment whose jsonpath is the expansion of the shared name. That a well-formed read exists somewhere in the file is a weaker claim than it reads as, since a second assignment to the same local, or a read filling another one, satisfies it while the guard runs a literal nothing here pins. The expansion and not the name: kubectl echoes a braceless template back verbatim, so a read that dropped the `$` would return its own text, clear the script's empty-history backstop, and carry no status the cycle check matches. The library is checked by sourcing rather than by reading, so an assignment the shell would run is resolved by the shell instead of by a guess about which text the shell would pick. Each rule is pinned by its own test, so a regression names which one it opened rather than only that extraction broke. How kubectl's printer treats an absent path is its own configuration rather than part of this contract, so it is left unasserted: guessing at it would put the expectation and the fixture back in the same hands. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
8.9 KiB
Cozystack Project Overview
This document provides detailed information about Cozystack project structure and conventions for AI agents.
About Cozystack
Cozystack is an open-source Kubernetes-based platform and framework for building cloud infrastructure. It provides:
- Managed Services: Databases, VMs, Kubernetes clusters, object storage, and more
- Multi-tenancy: Full isolation and self-service for tenants
- GitOps-driven: FluxCD-based continuous delivery
- Modular Architecture: Extensible with custom packages and services
- Developer Experience: Simplified local development with cozyhr tool
The platform exposes infrastructure services via the Kubernetes API with ready-made configs, built-in monitoring, and alerts.
Code Layout
.
├── packages/ # Main directory for cozystack packages
│ ├── core/ # Core platform logic charts (installer, platform)
│ ├── system/ # System charts (CSI, CNI, operators, etc.)
│ ├── apps/ # User-facing charts shown in dashboard catalog
│ └── extra/ # Tenant-specific modules, singleton charts which are used as dependencies
├── dashboards/ # Grafana dashboards for monitoring
├── hack/ # Helper scripts for local development
│ └── e2e-chainsaw/ # End-to-end application tests (Kyverno Chainsaw)
├── scripts/ # Scripts used by cozystack container
│ └── migrations/ # Version migration scripts
├── docs/ # Documentation
│ ├── agents/ # AI agent instructions
│ └── changelogs/ # Release changelogs
├── cmd/ # Go command entry points
│ ├── cozystack-api/
│ ├── cozystack-controller/
│ └── cozystack-assets-server/
├── internal/ # Internal Go packages
│ ├── controller/ # Controller implementations
│ └── lineagecontrollerwebhook/
├── pkg/ # Public Go packages
│ ├── apis/
│ ├── apiserver/
│ └── registry/
└── api/ # Kubernetes API definitions (CRDs)
└── v1alpha1/
Package Structure
Every package is a Helm chart following the umbrella chart pattern:
packages/<category>/<package-name>/
├── Chart.yaml # Chart definition and parameter docs
├── Makefile # Development workflow targets
├── charts/ # Vendored upstream charts
├── images/ # Dockerfiles and image build context
├── patches/ # Optional upstream chart patches
├── templates/ # Additional manifests
├── templates/dashboard-resourcemap.yaml # Dashboard resource mapping
├── values.yaml # Override values for upstream
└── values.schema.json # JSON schema for validation and UI
Build and Development Commands
Root targets (run from the repo root):
make build # Build all Docker images (needs: docker, skopeo, jq, gh, helm, yq, GNU tar/sed/awk)
make unit-tests # Run the unit tests (Helm, BATS, Go, etc.); does not reach ./internal/...
make test-controllers # Run the Go tests under ./internal/... (controllers, contract tests)
make generate # Code generation (hack/update-codegen.sh) — CRDs, DeepCopy, clients, RBAC
make manifests # Generate CRD manifests and operator YAML variants
make cozypkg # Build the cozypkg CLI
make test # Full E2E tests (requires a cluster)
make prepare-env # Prepare the E2E test environment (apply + prepare-cluster)
Per-package targets (run inside packages/{apps,system,extra,core}/<name>/):
make image # Build the package image
make show # Render the Helm template
make apply # Apply the Helm release to a cluster (needs NAME, NAMESPACE)
make diff # Diff the template against the cluster
make test # Package-specific tests
make generate # Regenerate values.schema.json + README.md from values.yaml
Build environment variables:
REGISTRY— Docker registry (defaultghcr.io/cozystack/cozystack).PUSH=1/LOAD=0— control buildx push/load behaviour.LOAD=1 PUSH=0— load images locally instead of pushing.
Values schema generation
Package values.yaml files carry annotations (@param, @typedef, @field, @enum, @section) that cozyvalues-gen turns into values.schema.json and README.md. Run make generate in the package after editing annotations. A pre-commit hook runs make generate across packages/apps/* and packages/extra/* on every commit to keep these in sync — see contributing.md for the regenerate-before-commit rule.
Testing
- Helm unit tests:
make helm-unit-tests(runshack/helm-unit-tests.shover every package that defines atesttarget). Atesttarget is expected to run helm-unittest: the sweep fails a package whose run reports no suite, because exiting 0 having asserted nothing is indistinguishable from passing. A target that drives something else belongs under another name, aspackages/core/testingalready does with its sandbox flow.make unit-testsruns the unit suite — Helm, BATS, Go, and the preset/readiness checks. It does not reach the controller and contract tests under./internal/..., which have their own target,make test-controllers, so covering both locally means running that target as well; CI runs them as two steps of one job. - E2E tests: Kyverno Chainsaw suites in
hack/e2e-chainsaw/(one directory per app), run withchainsaw test. Cluster bootstrap (hack/e2e-install-cozystack.bats) and the OpenAPI checks (hack/e2e-test-openapi.bats) remain BATS. Conventions for writing and stabilising them — and the CI that runs them — live ine2e-testing.md. - Go tests: standard
go test, with Ginkgo/Gomega for controllers.
Conventions
Helm Charts
- Follow umbrella chart pattern for system components
- Include upstream charts in
charts/directory (vendored, not referenced) - Override configuration in root
values.yaml - Use
values.schema.jsonfor input validation and dashboard UI rendering
Go Code
- Follow standard Go conventions and idioms
- Use controller-runtime patterns for Kubernetes controllers
- Prefer kubebuilder for API definitions and controllers
- Add proper error handling and structured logging
Git Commits
Conventional Commits (type(scope): description) with --signoff, kept atomic and focused. Full format, scopes, AI-attribution trailer, and PR workflow are in contributing.md — the source of truth; do not duplicate them here.
PackageSource CRD upgrade policy
Each component in a PackageSource may set install.upgradeCRDs to control how CRDs from the chart's crds/ directory are handled on HelmRelease upgrades. Allowed values: Skip (default — helm-controller does not touch CRDs on upgrade), Create (create new CRDs only), CreateReplace (create new and overwrite existing).
Set upgradeCRDs: CreateReplace for operators whose upstream regularly adds new CRDs between versions (etcd-operator, cnpg, kubevirt, kamaji). Without it, new CRDs from a chart bump do not land on existing clusters — only fresh installs get them.
Do not set CreateReplace blindly: it overwrites every CRD in crds/ and can cause silent data loss if upstream drops a field from a CRD that has live objects. Only enable it for operators whose schema evolution is additive-only. When in doubt, leave it unset and apply new CRDs manually.
Documentation
Documentation is organized as follows:
docs/- General documentationdocs/agents/- Instructions for AI agentsdocs/changelogs/- Release changelogs- Main website: https://github.com/cozystack/website
Domain references for specific tasks
Before working on these areas, read the relevant doc first:
- Backup subsystem design:
api/backups/v1alpha1/DESIGN.md - A new app package: copy the structure of an existing one (e.g.
packages/apps/postgres/)
Things Agents Should Not Do
Never Edit These
- Do not modify files in
/vendor/(Go dependencies) - Do not edit generated files:
zz_generated.*.go - Do not change
go.mod/go.summanually (usego get) - Do not edit upstream charts in
packages/*/charts/directly (use patches) - Do not modify image digests in
values.yaml(generated by build)
Version Control
- Do not commit built artifacts from
_out - Do not commit test artifacts or temporary files
Git Operations
- Do not force push to main/master
- Do not update git config
- Do not perform destructive operations without explicit request
Core Components
- Do not modify
packages/core/platform/without understanding migration impact