diff --git a/content/en/docs/next/operations/customizer/_index.md b/content/en/docs/next/operations/customizer/_index.md new file mode 100644 index 00000000..b0f2b273 --- /dev/null +++ b/content/en/docs/next/operations/customizer/_index.md @@ -0,0 +1,53 @@ +--- +title: "Customizer — Declarative Cluster Customizations" +linkTitle: "Customizer" +description: "Manage cluster customizations from a git repo you own, using Cozystack's built-in Flux." +weight: 15 +--- + +The **customizer** is an opt-in system package (`cozystack.customizer`) that turns an admin-owned git repo into the source of truth for cluster customizations: Package CR overrides (OIDC enable, MetalLB options), in-house HelmReleases, Keycloak realm imports, NetworkPolicies, and additional `PackageSource`s pointing at the admin's own chart registries. + +It's the supported alternative to running `kubectl patch packages.cozystack.io …` interactively. Same end-state, but every change is a commit in a repo the admin owns — with audit trail, code review, rollback, and DR replay. + +## When to use it + +- Enabling OIDC on the host control plane +- Overriding a system-component value (`metallb.frrk8s.enabled: true`, ingress-nginx config keys, etc.) +- Declaring Keycloak realms / clients as `KeycloakRealmImport` CRs +- Shipping in-house HelmReleases (internal portal, monitoring sidecar) into a namespace the admin owns +- Registering an additional OCI chart registry + `PackageSource` so the admin's own charts get the same Package-CR lifecycle as platform packages +- Cluster-scoped resources the admin needs that the platform doesn't manage (NetworkPolicies, RBAC for ops teams) + +## How it works + +Cozystack runs its own GitOps loop — the platform chart and its child packages reconcile from a fixed OCI/Git source. The customizer adds a **second, parallel loop** scoped to the admin's repo: + +``` + cozy-system + ┌────────────────────────────────────────────────────┐ + cozystack OCI ─► │ PackageSource cozystack.* │ + (chart-managed) │ Package cozystack.* ◄── helm-controller SSA │ + │ └─► HelmRelease (owned, hard-Updated) │ + └────────────────────────────────────────────────────┘ + ▲ + │ SSA patch to spec.components.*.values + │ + admin git repo ──► GitRepository cozystack-customizer-config + │ + ▼ + Kustomization cozystack-customizer + serviceAccountName: cozystack-customizer + │ + ├─► Package CR patches (Server-Side Apply) + ├─► resources in cozy-customizer/ (own & prune) + ├─► extra PackageSources (own & prune) + └─► extra HelmReleases in admin namespaces +``` + +Two field managers (`helm-controller` and `kustomize-controller`) coexist on the same Package CR. helm-controller writes only the chart-rendered fields; kustomize-controller writes only what the admin's repo declares. SSA tracks ownership per field. + +## Read next + +- **[Setup guide]({{< relref "setup.md" >}})** — step-by-step: prerequisites, enable the package, first commit, verify reconciliation. +- **[Repo layout and worked examples]({{< relref "repo-layout.md" >}})** — recommended directory tree plus three end-to-end examples (enable OIDC, override a MetalLB option, ship an in-house HelmRelease). +- **[Field ownership, RBAC, limitations]({{< relref "field-ownership.md" >}})** — what the customizer SA can and can't do; which fields on Package CRs are chart-owned vs admin-owned; the SSA contract. diff --git a/content/en/docs/next/operations/customizer/field-ownership.md b/content/en/docs/next/operations/customizer/field-ownership.md new file mode 100644 index 00000000..caefc6b5 --- /dev/null +++ b/content/en/docs/next/operations/customizer/field-ownership.md @@ -0,0 +1,129 @@ +--- +title: "Field Ownership, RBAC, Limitations" +linkTitle: "Field Ownership & RBAC" +description: "What the customizer ServiceAccount can do, which fields on Package CRs are admin-owned vs chart-owned, and the SSA contract." +weight: 30 +--- + +The customizer Kustomization applies its manifests via Server-Side Apply through a dedicated ServiceAccount with a curated ClusterRole. This page documents what's granted, what isn't, and which fields on Package CRs the customizer is supposed to write. + +## RBAC granted to `cozystack-customizer` + +Cluster-scoped: + +| Resource | Verbs | Notes | +|---|---|---| +| `packages.cozystack.io` | get, list, watch, **patch**, update | No `delete` — disable a Package by adding it to `bundles.disabledPackages` on `cozystack.cozystack-platform` instead. | +| `packagesources.cozystack.io` | full | Customizer authors its own PackageSources. | +| `helmreleases.helm.toolkit.fluxcd.io` (cluster-wide) | get, list, watch | Read-only — chart-managed HelmReleases are off-limits to the customizer. | +| `keycloakrealmimports`, `keycloaks`, `keycloakusers` (`k8s.keycloak.org`) | full | Declarative Keycloak realm management. | +| `*.source.toolkit.fluxcd.io` | full | Additional `GitRepository` / `OCIRepository` / `HelmRepository` / `Bucket` sources. | + +Namespace-scoped (inside `customizer.rbac.ownedNamespaces`, default `cozy-customizer`): + +- `cluster-admin` role bound — full mutate on every namespaced resource kind. HelmReleases, NetworkPolicies, ConfigMaps, Secrets, Services, Ingresses, etc. + +## RBAC explicitly NOT granted + +- `delete` on `packages.cozystack.io` +- Anything on `customresourcedefinitions.apiextensions.k8s.io` +- Anything on the `cozystack-controller` Deployment, ServiceAccount, or its cluster-admin binding +- Anything on `mutatingwebhookconfigurations` / `validatingwebhookconfigurations` +- Anything in `kube-system` + +If you need any of these from a customizer manifest, the answer is "don't" — either restructure the change to use one of the granted paths, or perform it as an out-of-band administrator action. + +## Field ownership on Package CRs + +Each Package CR is shared between two field managers: + +| Field | Owner | Why | +|---|---|---| +| `metadata.name`, `metadata.annotations["helm.sh/resource-policy"]` | helm-controller (platform chart) | Set by the platform bundle template. | +| `spec.variant` | helm-controller (for child Packages); cozystack-operator (for `cozystack.cozystack-platform`) | Set in the platform chart's `_helpers.tpl`. | +| `spec.components.*.values.*` | **customizer** | This is the entire admin-writable surface area for tuning a platform component. | +| `spec.components.*.enabled: false` | **customizer** | Disable a component within a Package. | +| `spec.ignoreDependencies` | **customizer** | Same. | +| `spec.components.platform.values.bundles.{enabled,disabled}Packages` | **customizer** (on `cozystack.cozystack-platform`) | The documented removal path for whole-package disable. | + +The rule of thumb: **patch `spec.components.*.values.*` from the customizer; never write `spec.variant` or chart-rendered metadata.** + +## Known limitation — contract is currently advisory + +`kustomize-controller` hardcodes `client.ForceOwnership=true` on every Server-Side Apply call. When a customizer manifest declares a chart-owned field (most notably `spec.variant`), kustomize-controller **silently transfers ownership** away from helm-controller. Flux's own SSA conflict detection cannot catch this, because the force-ownership flag bypasses the conflict path. + +The planned enforcement is a validating admission webhook on `packages.cozystack.io` that allow-lists which field managers may write the chart-owned fields. Until that ships, this contract is enforced socially, not by the API server. + +### Symptoms of a contract violation + +If a customizer manifest accidentally claims `spec.variant: `: + +- The Package's status flips to `Ready=False`, `reason: VariantNotFound`, `message: "Variant not found in PackageSource cozystack."`. +- helm-controller's `managedFields` entry for `spec.variant` collapses (ownership transferred to kustomize-controller). +- The downstream HelmRelease for that component is not regenerated. + +If a customizer manifest accidentally drops `spec.variant` entirely after having claimed it: + +- The field is deleted from the CR, no manager reclaims it. +- The reconciler falls back to the `default` variant (functionally benign for most components), but the CR is not pristine. + +### Mitigation + +Until the webhook lands: + +1. **Code review** — treat the field-ownership table above as a PR checklist for customizer changes. Reject manifests that declare `spec.variant` on a chart-managed Package. +2. **Audit after enabling** new patches: + ```sh + kubectl get package -o yaml --show-managed-fields | yq '.metadata.managedFields[] | {manager, operation, fields: .fieldsV1}' + ``` + Confirm `helm-controller` still owns `spec.variant` and `kustomize-controller` owns only `spec.components.*.values.*`. + +## Recovery — restore a broken Package CR + +Suppose a customizer patch broke `cozystack.metallb` (set `spec.variant: oidc` by mistake, or left orphan `spec.components.metallb.values`). + +1. Suspend the customizer Kustomization so it stops re-applying the bad patch: + ```sh + kubectl --namespace cozy-system patch kustomization cozystack-customizer \ + --type merge --patch '{"spec":{"suspend":true}}' + ``` +2. Reset the Package CR to a chart-default state: + ```sh + kubectl patch package cozystack.metallb \ + --type merge --patch '{"spec":{"variant":"default","components":null}}' + ``` +3. Confirm: + ```sh + kubectl get package cozystack.metallb + ``` + Should show `Ready=True, reason: ReconciliationSucceeded` within a minute. +4. Fix the customizer manifest in your repo, commit, push, and resume: + ```sh + kubectl --namespace cozy-system patch kustomization cozystack-customizer \ + --type merge --patch '{"spec":{"suspend":false}}' + ``` + +## Disable the customizer entirely + +To turn the customizer off but leave the resources it created in place: + +```sh +kubectl patch packages.cozystack.io cozystack.cozystack-platform --type=merge --patch '{ + "spec": {"components": {"platform": {"values": {"customizer": {"enabled": false}}}}} +}' +``` + +`helm.sh/resource-policy: keep` on the `cozystack.customizer` Package CR means the existing chart resources (GitRepository, Kustomization, SA, RBAC, owned namespaces) are not auto-removed. To fully uninstall: + +```sh +kubectl delete package.cozystack.io cozystack.customizer +helm uninstall customizer --namespace cozy-system +``` + +## Other limitations + +- **Single platform admin per cluster.** One customizer repo, one platform-wide configuration. Per-tenant customizer GitOps is out of scope; it would layer on top of the existing `tenant` Application CRs. +- **No HelmRelease forking for chart-managed components.** Cozystack's Package reconciler uses plain `Update` (not SSA) on its rendered HelmReleases, so a customizer manifest that tries to override a chart-rendered HelmRelease is wiped on the next reconcile. Patch the corresponding Package CR's `spec.components.*.values` instead. +- **Keycloak realm imports only run once unless the spec changes.** Bump a label, annotation, or any field on the `KeycloakRealmImport` to trigger re-import. +- **Keycloak user attributes and sessions are not declarative.** Those genuinely don't fit a GitOps loop; the customizer doesn't try to manage them. +- **No multi-admin authoring model.** The customizer pulls one branch from one repo with one SA. Branch protection and review happen in your git provider, not in the cluster. diff --git a/content/en/docs/next/operations/customizer/repo-layout.md b/content/en/docs/next/operations/customizer/repo-layout.md new file mode 100644 index 00000000..c45664d4 --- /dev/null +++ b/content/en/docs/next/operations/customizer/repo-layout.md @@ -0,0 +1,228 @@ +--- +title: "Repo Layout and Worked Examples" +linkTitle: "Repo Layout" +description: "Recommended customizer-repo directory tree, with three end-to-end examples." +weight: 20 +--- + +This page covers what to put inside the customizer repo once the loop is wired up (see **[Setup]({{< relref "setup.md" >}})** if you haven't enabled the package yet). + +## Recommended layout + +``` +cozystack-customizer/ + README.md + clusters/ + prod/ + kustomization.yaml + platform.yaml # patch for cozystack.cozystack-platform + packages/ + metallb.yaml # patch — spec.components.metallb.values + ingress-nginx.yaml + sources/ + myorg-charts.yaml # extra OCIRepository + packagesources/ + myorg-internal-portal.yaml # extra PackageSource using myorg-charts + keycloak/ + realm-cozy.yaml + apps/ # admin-owned HelmReleases + ns-platform-tools.yaml + my-monitoring-stack.yaml + rbac/ + readonly-engineers.yaml + networkpolicies/ + default-deny.yaml +``` + +The corresponding entry-point `clusters/prod/kustomization.yaml`: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - platform.yaml + - packages/ + - sources/ + - packagesources/ + - keycloak/ + - apps/ + - rbac/ + - networkpolicies/ +``` + +You can use any layout that kustomize accepts. The structure above groups files by what they target (`packages/` = patches to Cozystack Packages, `apps/` = your own HelmReleases, etc.), which makes review of a PR scope obvious. + +For a multi-cluster setup, put one folder per cluster under `clusters/`, share common manifests via a `base/` folder, and point each cluster's customizer at its own `clusters/` path. + +## Example 1 — Enable OIDC on the host + +The in-cluster recipe: + +```sh +kubectl patch packages.cozystack.io cozystack.cozystack-platform --type=merge --patch '{ + "spec": { + "components": { + "platform": { + "values": { + "authentication": { + "oidc": { + "enabled": true, + "keycloakInternalUrl": "http://keycloak-http.cozy-keycloak.svc:8080/realms/cozy" + } + } + } + } + } + } +}' +``` + +becomes a file in the customizer repo — `clusters/prod/platform.yaml`: + +```yaml +apiVersion: cozystack.io/v1alpha1 +kind: Package +metadata: + name: cozystack.cozystack-platform +spec: + components: + platform: + values: + authentication: + oidc: + enabled: true + keycloakInternalUrl: http://keycloak-http.cozy-keycloak.svc:8080/realms/cozy +``` + +Same write to the same Package CR, but as a reviewable commit. The customizer Kustomization patches `spec.components.platform.values.authentication.oidc.*` via SSA — chart-managed fields (`spec.variant`, `metadata.annotations["helm.sh/resource-policy"]`) are untouched. + +To complete OIDC setup, see **[OIDC Server]({{< relref "/docs/next/operations/oidc/enable_oidc.md" >}})** for the wider configuration (API server flags, identity provider, etc.); only the in-cluster `kubectl patch` step is replaced by the customizer manifest above. + +## Example 2 — Override a system-component option + +Turn on `frrk8s` mode in MetalLB. The Package CR for MetalLB doesn't have `spec.components.metallb.values.*` set out of the box, so any keys you add are admin-owned and the chart will not fight them. + +`clusters/prod/packages/metallb.yaml`: + +```yaml +apiVersion: cozystack.io/v1alpha1 +kind: Package +metadata: + name: cozystack.metallb +spec: + components: + metallb: + values: + metallb: + frrk8s: + enabled: true +``` + +Crucially, **do not** include `spec.variant` here. The variant is chart-owned. See **[Field ownership]({{< relref "field-ownership.md" >}})** for why this matters and what the current enforcement gap looks like. + +After the commit lands, watch the underlying HelmRelease pick up the new value: + +```sh +kubectl --namespace cozy-metallb get helmrelease metallb -o yaml | grep -A2 frrk8s +``` + +## Example 3 — Ship an in-house HelmRelease + +The customizer SA is bound to `cluster-admin` inside the `cozy-customizer` namespace (default) — and any other namespace you list under `customizer.rbac.ownedNamespaces` in the Platform Package values. Inside those namespaces, the customizer can create arbitrary resources. + +First make sure the chart source is registered. If your charts live in an OCI registry: + +`clusters/prod/sources/myorg-charts.yaml`: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: myorg-charts + namespace: cozy-customizer +spec: + interval: 5m + url: oci://ghcr.io//cozystack-charts + ref: + tag: v0.4.0 +``` + +Then the HelmRelease itself: + +`clusters/prod/apps/internal-portal.yaml`: + +```yaml +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: internal-portal + namespace: cozy-customizer +spec: + interval: 5m + chartRef: + kind: OCIRepository + name: myorg-charts + namespace: cozy-customizer + values: + host: portal.example.org +``` + +After reconcile: + +```sh +kubectl --namespace cozy-customizer get helmrelease internal-portal +kubectl --namespace cozy-customizer get pods +``` + +## Custom packages — your own charts as Cozystack Packages + +For a more integrated experience (Package-CR lifecycle, dependency tracking, `kubectl get package` visibility, status reporting), you can register your charts as Cozystack `PackageSource`s and treat them like platform packages. + +`clusters/prod/packagesources/myorg-internal-portal.yaml`: + +```yaml +apiVersion: cozystack.io/v1alpha1 +kind: PackageSource +metadata: + name: myorg.internal-portal +spec: + sourceRef: + kind: OCIRepository + name: myorg-charts + namespace: cozy-customizer + path: / + variants: + - name: default + components: + - name: internal-portal + path: internal-portal + install: + namespace: myorg-portal + releaseName: internal-portal +``` + +Plus the `Package` instance: + +```yaml +apiVersion: cozystack.io/v1alpha1 +kind: Package +metadata: + name: myorg.internal-portal +spec: + variant: default + components: + internal-portal: + values: + host: portal.example.org +``` + +Cozystack's reconciler doesn't care whether the PackageSource came from the platform chart or the customizer Kustomization — the same engine, dashboards, and `kubectl get package` apply. + +The `myorg-portal` namespace in the example above is **not** under `customizer.rbac.ownedNamespaces`, so the customizer SA can't create resources directly there. That's fine — the Cozystack reconciler runs as `cozystack-controller` (cluster-admin) and creates the actual HelmRelease into `myorg-portal` on the customizer's behalf, with the values the customizer declared. + +## Operational tips + +- **Always include `kustomize.config.k8s.io/v1beta1` kustomization.yaml at the path** — a missing entry-point file is the most common first-commit failure. +- **`prune: true` is on by default.** Removing a manifest from the kustomization deletes the corresponding resource from the cluster. Test removals on a non-prod cluster first. +- **`wait: true` is on by default.** The Kustomization waits for all applied resources to be `Ready` before reporting success — slow chart installs (cert-manager waiting on issuers, etc.) can make the Kustomization look stuck. Set `customizer.kustomization.wait: false` in the Platform Package values if you find this gets in the way. +- **Force-reconcile is fast:** `flux --namespace cozy-system reconcile kustomization cozystack-customizer --with-source` syncs the GitRepository and the Kustomization in one command. diff --git a/content/en/docs/next/operations/customizer/setup.md b/content/en/docs/next/operations/customizer/setup.md new file mode 100644 index 00000000..39237ec4 --- /dev/null +++ b/content/en/docs/next/operations/customizer/setup.md @@ -0,0 +1,193 @@ +--- +title: "Setup Guide" +linkTitle: "Setup" +description: "Step-by-step: enable the customizer package, point it at your git repo, verify reconciliation." +weight: 10 +--- + +This guide walks through enabling the customizer on a fresh cluster and getting your first commit reconciled. It assumes Cozystack is already installed and healthy, and you have admin access (you can patch the `cozystack.cozystack-platform` Package CR). + +## Prerequisites + +- A running Cozystack cluster on version ≥ 1.5 (the customizer package landed in 1.5). +- `kubectl` configured against the cluster, in the `cozy-system` namespace. +- A git repo you own that the cluster's Flux can reach over HTTPS (or SSH — see the Flux [GitRepository secret docs](https://fluxcd.io/flux/components/source/gitrepositories/#secret-reference) for transport options). The repo doesn't need to exist with content yet — an empty `main` branch is fine for the first reconcile. +- Credentials with read access to that repo. For GitHub HTTPS that's a fine-grained Personal Access Token with `Contents: Read` scope on the repo. + +## Step 1 — Create the git auth Secret + +The customizer chart does **not** generate the git credentials Secret. You create it in `cozy-system` before enabling the package, so the platform never owns admin credentials. + +```sh +kubectl --namespace cozy-system create secret generic cozystack-customizer-git \ + --from-literal=username= \ + --from-literal=password= +``` + +For SSH auth, the Secret takes an `identity` (private key) and `known_hosts` instead — see the upstream Flux docs. + +## Step 2 — Initialize the customizer repo + +In your git repo, create the path the Kustomization will reconcile. The minimum is a Kustomize entry-point file at the path you'll point Flux at. For `path: ./clusters/prod`: + +```sh +mkdir -p clusters/prod +cat > clusters/prod/kustomization.yaml <<'EOF' +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: [] +EOF + +git add clusters/prod/kustomization.yaml +git commit -m "init: empty customizer overlay" +git push origin main +``` + +Starting empty is deliberate — it lets you verify Flux can pull and apply *something* before you start adding patches. + +See **[Repo layout and worked examples]({{< relref "repo-layout.md" >}})** for the recommended directory structure once you're past the smoke test. + +## Step 3 — Enable the customizer in the Platform Package + +Patch `cozystack.cozystack-platform` to flip `customizer.enabled` and point at your repo. Replace `` and adjust the branch/path as needed. + +```sh +kubectl patch packages.cozystack.io cozystack.cozystack-platform --type=merge --patch '{ + "spec": { + "components": { + "platform": { + "values": { + "customizer": { + "enabled": true, + "source": { + "url": "https://github.com//cozystack-customizer.git", + "ref": {"branch": "main"}, + "secretRef": "cozystack-customizer-git" + }, + "kustomization": { + "path": "./clusters/prod" + } + } + } + } + } + } +}' +``` + +> Once the customizer is up and reconciling, this initial patch becomes the **last** in-cluster `kubectl patch` you'll need — subsequent customizer config (more namespaces, switching to SSH auth, changing branches) can be done via the customizer repo itself by patching `cozystack.cozystack-platform` from a manifest. The bootstrap is a chicken-and-egg you have to do once. + +## Step 4 — Verify reconciliation + +The `cozystack.customizer` Package CR should now appear and reconcile to a HelmRelease. Watch for `Ready: True`: + +```sh +kubectl get packages.cozystack.io cozystack.customizer +``` + +Expected: + +``` +NAME VARIANT READY STATUS +cozystack.customizer default True reconciliation succeeded, generated 1 helmrelease(s) +``` + +Then check the resources the chart created: + +```sh +kubectl --namespace cozy-system get serviceaccount cozystack-customizer +kubectl get clusterrole,clusterrolebinding cozystack-customizer +kubectl get namespace cozy-customizer +kubectl --namespace cozy-customizer get rolebinding cozystack-customizer +kubectl --namespace cozy-system get gitrepository cozystack-customizer-config +kubectl --namespace cozy-system get kustomization cozystack-customizer +``` + +The `GitRepository` should report `READY=True` with the latest commit revision: + +``` +NAME URL AGE READY STATUS +cozystack-customizer-config https://github.com//cozystack-customizer.git 30s True stored artifact for revision 'main@sha1:...' +``` + +The `Kustomization` should also be `READY=True`: + +``` +NAME AGE READY STATUS +cozystack-customizer 30s True Applied revision: main@sha1:... +``` + +## Step 5 — Sanity check with a no-op commit + +To confirm reconciliation reacts to new commits, add a labelled ConfigMap to the customizer repo: + +```sh +# in your customizer repo, on the main branch +cat > clusters/prod/sanity-check.yaml <<'EOF' +apiVersion: v1 +kind: ConfigMap +metadata: + name: customizer-sanity-check + namespace: cozy-customizer +data: + reconciled-at: "first-sync" +EOF + +# add it to the kustomization +cat > clusters/prod/kustomization.yaml <<'EOF' +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - sanity-check.yaml +EOF + +git add clusters/prod/ +git commit -m "test: sanity check ConfigMap" +git push origin main +``` + +Trigger an immediate reconcile (or wait up to the GitRepository interval, default 1 min): + +```sh +flux --namespace cozy-system reconcile kustomization cozystack-customizer --with-source +``` + +Then: + +```sh +kubectl --namespace cozy-customizer get configmap customizer-sanity-check +``` + +If the ConfigMap is present, the loop is wired end-to-end. Delete the sanity ConfigMap (commit, push, wait for reconcile with `prune: true` enabled by default) before moving on to real customizations. + +## Troubleshooting first sync + +If the `GitRepository` is not `READY=True`: + +```sh +kubectl --namespace cozy-system describe gitrepository cozystack-customizer-config +``` + +Look for `Status.Conditions`: + +- `failed to checkout and determine revision: ... authentication required` — the auth Secret name doesn't match `secretRef`, or the username/password are wrong. Recreate the Secret with the correct values (the name must equal `customizer.source.secretRef`). +- `unable to clone: ... repository not found` — the URL is wrong, or the PAT doesn't have read access to that repo. +- `no such file or directory: ./clusters/prod/kustomization.yaml` — surfaced on the Kustomization, not the GitRepository. The path inside the repo doesn't exist or is misspelled. + +If the `Kustomization` is not `READY=True`: + +```sh +kubectl --namespace cozy-system describe kustomization cozystack-customizer +``` + +Common causes: + +- `accumulating resources: ... no such file or directory` — a path referenced in your `kustomization.yaml` doesn't exist. +- `failed to ... permission denied` on a specific resource — the customizer SA doesn't have RBAC for that kind. See **[Field ownership and RBAC]({{< relref "field-ownership.md" >}})** for what's granted and what's not. To extend the SA's reach inside an admin-owned namespace, add the namespace to `customizer.rbac.ownedNamespaces` in the Platform Package — the chart will create a RoleBinding to `cluster-admin` there. + +## Where to go next + +You now have a working customizer loop reconciling an empty repo. To turn it into a useful operational tool, see: + +- **[Repo layout and worked examples]({{< relref "repo-layout.md" >}})** — recommended directory tree, plus three end-to-end examples (enable OIDC, override a MetalLB option, ship an in-house HelmRelease). +- **[Field ownership, RBAC, limitations]({{< relref "field-ownership.md" >}})** — important reading **before** you start patching Package CRs.