From 157bcfaf380750a6afa08e41a120cf9c6c9f4949 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 07:54:00 +0000 Subject: [PATCH 01/15] docs(review): add the first draft of the CRD description proposal The proposal as Codex wrote it, before any review: an editorial pass over the ten v1alpha3 CRDs with ten example rewrites. Kept unchanged so the commits that follow show what review changed. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/review/20261025-crd-text-proposal.md | 360 ++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 docs/review/20261025-crd-text-proposal.md diff --git a/docs/review/20261025-crd-text-proposal.md b/docs/review/20261025-crd-text-proposal.md new file mode 100644 index 00000000..e1cd3441 --- /dev/null +++ b/docs/review/20261025-crd-text-proposal.md @@ -0,0 +1,360 @@ +# Proposal: clearer CRD descriptions + +The branch makes the API reference more consistent, but several descriptions still ask the reader +to unpack too many rules at once. Keep the useful detail. Give each sentence one job, and use +bullets where readers need to compare values, check requirements, or follow a precedence order. + +This is a proposal only. No Go comments, generated CRDs, or existing documentation are changed. + +## Scope + +Reviewed `chore/crd-text-sweep` at `0b8334eb`, against its merge base with `origin/main` +(`1bc97904`). The focus is newcomer-facing text in the ten `configbutler.ai/v1alpha3` CRDs and +the branch's changes to user documentation. This is an editorial review with targeted behavior +checks. A full correctness review of the controller changes is outside its scope. + +The examples below are replacement descriptions, without Go comment prefixes. They use the +plain-text conventions required by [AGENTS.md](../../AGENTS.md). Markdown commentary follows the +[documentation style guide](../style-guide.md). + +## What's good + +- Field descriptions usually start by explaining the field's purpose. +- Defaults, namespace resolution, and immutable references are documented near the setting. +- The short descriptions already work. Keep fields such as `GitTarget.spec.path`, + `GitProvider.spec.url`, and `GitTarget.spec.encryption` compact. +- The generated YAML excludes the long type-level design rationale I checked. For example, + `GitTarget.spec.placement` uses the field comment. The internal discussion on + `GitTargetPlacementSpec` stays in the source. Review the generated description before deciding a comment + needs editorial work. + +## Top risks (ranked) + +1. **Some wording changes the reader's understanding of behavior.** + `CommitRequest.spec.message` says the message is used as written, without explaining that + `GitTarget.spec.commit.message.requestTemplate` can wrap it. `GitProvider.status.lastVerifiedAt` + claims to show how long a connection has been broken; it records the last successful check. + Failure may begin later, and `Ready=False` can also reflect signing or configuration failures. + These need corrections before cosmetic edits. +2. **Dense descriptions hide consequential choices.** `serializeNamespace`, `onRefusal`, grant + defaults, and selector pruning all affect what gets written or deleted. A reader should be + able to find each choice without rereading a paragraph of exceptions. +3. **Some references leave readers guessing which object or condition is meant.** Both watch-rule + `objectSelector` descriptions refer to `spec.prune.mode` without naming the `GitTarget`. + `ArgoCDClaim.status.conditions` mixes `True` and `False` with `NotConfigured`, which is a + reason, not a condition status. +4. **Long troubleshooting prose is squeezed into a table.** The new row about repeated + `ClaimedResourceReconciled` events in the Flux guide contains the symptom, implementation + cause, ownership explanation, and remedy. Give that case its own subsection and leave a short + link in the table. + +Evidence for the behavior corrections: [request message rendering](../../internal/git/commit_executor.go), +[provider reconciliation](../../internal/controller/gitprovider_controller.go), and +[Argo CD control conditions](../../internal/controller/argocdclaim_hold.go). + +## What Flux does + +Flux has a [documentation style guide](https://fluxcd.io/contributing/docs/style-guide/), maintained +in its [website source](https://github.com/fluxcd/website/blob/main/content/en/contributing/docs/style-guide.md). +It recommends active voice, present tense, direct wording, and avoiding idioms that make reading +harder for people who speak English as a second language. It recommends grouping related items +into lists and keeping paragraphs below six sentences. These are website guidelines; I found no +separate CRD-comment writing standard in the sources reviewed. + +The source offers useful examples too: + +- In [GitRepository's API types](https://github.com/fluxcd/source-controller/blob/main/api/v1/gitrepository_types.go), + `SecretRef` separates HTTPS and SSH requirements into sentences. `Provider` states its default + separately. `GitRepositoryRef` describes precedence beside each choice. +- In [Kustomization's API types](https://github.com/fluxcd/kustomize-controller/blob/main/api/v1/kustomization_types.go), + `Wait` states the action, its interaction with `HealthChecks`, and its default in separate + sentences. Some other descriptions are still dense, so these are examples to learn from, rather + than a uniform standard to copy. +- The [GitRepository reference](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1/gitrepositories.md) + starts with a manifest and explains it with bullets. It gives authentication methods and branch + selection their own sections. + +My recommendation is to adopt those structural habits while keeping this repository's conventions +for field names and literal values. The recommendation to use bullets inside selected CRD +descriptions is my editorial proposal. Flux's guide does not require it. Sources checked on +October 25, 2026; the source links track their default branches. + +## Recommended changes (actionable) + +### Use a small set of writing rules + +- Lead with what the field controls. Follow with the default or omitted behavior. +- Use bullets for alternatives and independent requirements. Use numbered lists when order matters. +- Keep a short explanation in prose when there is no choice to compare. +- Name the subject: the `GitTarget`, the source cluster, or the referenced `GitBranch`. +- Prefer familiar verbs: `allows`, `uses`, `rejects`, and `restores`. Replace phrases such as + `a written list`, `admits nobody`, and `hands the switch back` with concrete behavior. +- Keep deletion behavior, authorization, failure reasons, and immutability visible. Shorter text + must preserve these details. +- Aim for two to four sentences in ordinary descriptions. Treat the existing 80-word target as + a prompt to restructure longer text. Preserve necessary warnings. + +### 1. Turn namespace choices into bullets + +Location: [ConsumerNamespaces.From](../../api/v1alpha3/access_grants.go). + +The existing sentence puts four enum values and four explanations in one line of thought. +Each value is a separate choice: + +```text +From selects which namespaces this grant allows consumers from: + +- "Same": the namespace of the object that defines the grant. +- "Names": the namespaces listed in names. +- "Selector": namespaces whose labels match selector. +- "All": every namespace. +``` + +### 2. Make the omitted and empty grant lists easy to distinguish + +Location: [GitBranch.spec.allowedGitTargets](../../api/v1alpha3/gitbranch_types.go). + +The distinction is already present, but `a written list is the whole policy` makes the reader +translate it. State each case: + +```text +AllowedGitTargets controls which GitTargets may write through this GitBranch. + +- Omitted: allows GitTargets in this GitBranch's namespace. +- Empty list ([]): allows no GitTargets. +- Nonempty list: allows only GitTargets matched by at least one grant. + +An explicit list replaces the default. Include a grant for this GitBranch's +namespace if GitTargets there still need access. +``` + +Apply the same structure to `KubeProvider.spec.allowedGitTargets` and `allowedApplierClaims`, +preserving their source-namespace and applier-namespace defaults. Keep +`GitProvider.spec.allowedGitBranches` distinct: both omission and an empty list deny access. + +### 3. Separate refusal choices from the restrictions on reapplying + +Location: [GitTarget.spec.onRefusal](../../api/v1alpha3/gittarget_types.go). + +```text +OnRefusal controls what happens when a live edit cannot be written to the folder. +Defaults to "Ignore". A refused edit makes no commit and remains on the cluster +until an applier overwrites it from Git. + +- "Ignore": reports the refusal in conditions and events. +- "Reapply": uses spec.applierClaimRef to reapply Git after pending edits are + published, reverting the refused edit. Requires spec.applierClaimRef. + +Reapply runs only when the document already exists in the folder and the write +removes no documents. It does not run for folder-wide failures, such as invalid +YAML. A refusal that persists triggers one reapply, rather than one per check. +``` + +This is a justified exception to the word-count target: the restrictions prevent a reader from +expecting a reapply to repair every refusal. + +### 4. Present namespace serialization as three choices + +Location: [GitTarget.spec.serializeNamespace](../../api/v1alpha3/gittarget_types.go). + +```text +SerializeNamespace controls whether written documents include metadata.namespace. +It applies to every write of a namespaced resource. Cluster-scoped resources +ignore this setting. + +- Omitted: leaves out metadata.namespace only when the kustomization governing + the document sets the same namespace. +- true: always includes metadata.namespace. Use this for a flat folder applied + directly. +- false: always omits metadata.namespace. The namespace must come from a + kustomization, a Flux Kustomization's targetNamespace, or an Argo CD + Application's destination.namespace. + +With false, the folder accepts only one source namespace. A WatchRule that adds +a second is rejected with GitPathAccepted=False, reason MultipleSourceNamespaces. +``` + +### 5. Explain the message override accurately + +Location: [CommitRequest.spec.message](../../api/v1alpha3/commitrequest_types.go). + +```text +Message supplies the requested commit message. Omitted, the GitTarget's +spec.commit.message.liveTemplate generates the message. + +The text is never evaluated as a template. If the GitTarget configures +spec.commit.message.requestTemplate, that template wraps this text. Otherwise, +the text is used as written. + +The message must contain 1–1024 characters and at least one non-whitespace +character. Newlines are allowed for a subject and body; other control characters +are rejected. +``` + +Short paragraphs work here: the message policy and validation are separate topics, but neither +needs an artificial list. + +### 6. Describe the timestamp without claiming an outage duration + +Location: [GitProvider.status.lastVerifiedAt](../../api/v1alpha3/gitprovider_types.go). + +```text +LastVerifiedAt is the time of the last successful repository connectivity check +using this GitProvider's credentials, or anonymous access when spec.secretRef +is omitted. Failed checks leave this timestamp unchanged. Check status.conditions +for the reason the GitProvider is not ready. +``` + +The controller updates the timestamp only after a successful remote check. The timestamp cannot +identify when a later failure began. + +### 7. Give each receiver type its own line + +Location: [GitReceiver.spec.type](../../api/v1alpha3/gitreceiver_types.go). + +```text +Type selects the push payload and authentication format: + +- "github": GitHub payload with X-Hub-Signature-256. +- "gitlab": GitLab payload with X-Gitlab-Token. +- "gitea": Gitea payload with X-Gitea-Signature. +- "generic-hmac": a JSON payload with repository, branch, and after fields, + authenticated by X-Signature: sha256=. +``` + +This also replaces the JSON-like `{"repository", "branch", "after"}` fragment, which names fields +but is not valid JSON. + +### 8. Keep template fields available, but group them + +Location: [GitTarget.spec.commit.message.liveTemplate](../../api/v1alpha3/gitprovider_types.go). + +The current description combines the template context, per-resource fields, empty-value handling, +and request overrides. Group the context without dropping fields: + +```text +LiveTemplate is the Go template for commit messages generated from live edits. +An empty value uses the built-in template. + +The template receives: + +- Author: the username, which may be empty. +- GitTarget: the target name. +- Count: the edit count after coalescing and before comparison with Git. +- Operations: counts of retained entries by operation kind. +- Resources: the resources in the commit window. + +Each resource provides Operation, Group, Version, Resource, Namespace, Name, +APIVersion, ResourceVersion, and Generation. ResourceVersion identifies the +state written by the commit and may be empty. Generation is 0 for a kind without +a spec. Guard both values with {{with}}. + +A CommitRequest message takes precedence without changing the author. The +requestTemplate field controls whether that message is wrapped. +``` + +Keep examples and longer context definitions in the existing [commit message guide](../commit-messages.md). + +### 9. Distinguish condition status from reason + +Location: [ArgoCDClaim.status.conditions](../../api/v1alpha3/argocdclaim_types.go). + +Replace the paragraph beginning `InControl is True` with: + +```text +InControl reports whether GitOps Reverser controls when the Application syncs: + +- True, reason AutomatedSyncOff: GitOps Reverser controls sync timing. +- False: control is being released or revoked, or no GitTarget references the + claim. It is also False with reason NotConfigured if another field manager + has enabled automated sync. +- Unknown, reason NotConfigured: the claim is not configured to control the + Application. + +A claim with no referencing GitTarget reads no applier and reports Ready=True, +reason NotReferenced. +``` + +This preserves the difference found in `holdArgoCD` and `ownedSwitchCondition`. Apply the same +status/reason distinction when editing the longer `Configured` paragraph and Flux conditions. + +### 10. Move the long Flux troubleshooting row into a subsection + +Location: [Flux troubleshooting](../bi-directional-flux.md#troubleshooting). + +Proposed subsection text: + +> Repeated `ClaimedResourceReconciled` events mean something is undoing the claim's suspension. +> A rising `claimed_resource_reconciled_total` records these corrections. If the claim also reports +> `OperationUnresolved`, inspect the controller that writes the `Kustomization`. +> +> A controller that replaces the whole spec can remove `spec.suspend` by leaving it out. Flux can +> then apply before GitOps Reverser suspends the `Kustomization` again. This removal does not take +> ownership of `spec.suspend`, so `SuspendOwnedElsewhere` does not identify it. +> +> Update that controller to use one of these approaches: +> +> - Preserve the `spec.suspend` value from the object it read when replacing the spec. +> - Use server-side apply and omit `spec.suspend` from the fields it manages. + +Keep the symptom in the table and link to this explanation. The adjacent `Overriding by hand` +bullet also benefits from two sentences: first the unchecked apply, then the claim's response. + +### Remaining pass, in priority order + +| Location | Proposed treatment | +| --- | --- | +| Both watch-rule `objectSelector` fields | Name `GitTarget.spec.prune.mode`; separate pruning and selector conflicts. | +| `GitTarget.spec.placement` | Separate existing-document behavior, path variables, and fallback restrictions. | +| `KubeProvider.spec.attribution.auditRoute` | Separate routing, default, namespace ownership, and conflict behavior. | +| `GitBranch.spec.parentBranch` | Separate default-parent selection from missing-parent behavior. | +| `GitProvider.spec.knownHostsRef` | Use a numbered list for the three sources in precedence order. | +| `GitTarget.spec.prune.mode` | Use one bullet for each mode, with the default first. | +| Both claim kinds' root descriptions | Lead with control of apply timing; separate the trigger cases. | +| `GitTarget.spec.suspend` | Separate stopping new commits, the final push, and resuming from cluster state. | +| Upgrade entry for removed message templates | Separate removal, client behavior, and the migration action. | + +## Contract integrity (spec/status, status subresource, Conditions) + +All ten generated CRDs have a status subresource, `status.observedGeneration`, and condition lists +keyed by `type`. This proposal changes their explanations only. Controller behavior was checked +for the specific wording corrections above; readiness behavior across all controllers was not +retested. + +## Schema correctness & webhooks (validation, defaults, CEL) + +The omitted-versus-empty grant distinction agrees with the generated defaults and +[grant helpers](../../api/v1alpha3/access_grants.go). Preserve enum values, namespace requirements, +and immutable-reference rules. This review proposes no schema, validation, or webhook changes. + +## GitOps/SSA ergonomics (lists, patchability) + +Grant lists are atomic; condition lists are maps keyed by `type`. Prose bullets have no relationship +to these schema list types. Keep both unchanged during the wording pass. + +## Operator UX (printer columns) + +Evaluate proposed bullets in actual `kubectl explain` output before applying them throughout the +API. Check both default terminal output and any generated reference page: Markdown appearance +alone does not establish terminal readability. No rendering test has been performed for these +proposals. Printer-column changes are outside this editorial scope. + +## Compatibility & migration impact (mandatory) + +The proposed wording changes are non-breaking. The existing branch also removes retired fields +and their rejection rules; it is not exclusively a description change. The parsed CRD comparison +against the merge base confirms those removals in `GitTarget`, `GitProvider`, `FluxClaim`, and +`ArgoCDClaim`. Their migration behavior belongs in [the upgrade guide](../UPGRADING.md), separately +from this proposal. + +The ten inspected CRDs serve and store `v1alpha3`. No API version or conversion change is proposed. + +## Minimal improvement plan (PR-sized) + +1. Correct the message, timestamp, condition, and cross-object-reference wording. +2. Apply the enum and grant-list examples, then preview their generated descriptions. +3. Restructure the remaining long descriptions and the Flux troubleshooting case. +4. Regenerate CRDs into a scratch directory and compare with the pre-edit branch snapshot after + stripping every `description` key. Preserve scopes, defaults, validation, and list semantics. +5. Run the repository's required validation for an API-comment implementation. For this proposal + alone, run `task lint-docs` and Vale on this file. From bb8d9ef245a2da032ac50c8224eb17c0c939e41e Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 07:54:13 +0000 Subject: [PATCH 02/15] docs(facts): record how kubectl explain renders a CRD description Measured against an envtest API server with kubectl v1.37.1: controller-gen keeps the Go comment's line breaks, and kubectl explain keeps them too, then wraps each line again at 76 characters of text. A longer line leaves its last words on a line of their own; 616 of the 1,067 current description lines are longer. Bullets render as bullets. Also records behavior a description must not misstate (the requestTemplate wrap, lastVerifiedAt, every InControl status and reason, the grant-list defaults), an envtest recipe to reproduce the rendering, and Flux's documentation style guide as a reference. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/INDEX.md | 6 + .../crd-descriptions-in-kubectl-explain.md | 120 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 docs/facts/crd-descriptions-in-kubectl-explain.md diff --git a/docs/INDEX.md b/docs/INDEX.md index 9793c9a1..a91139a6 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -74,6 +74,12 @@ re-validate spec; a removed field stops being served immediately, not after the migration inventory must be taken BEFORE the upgrade); and a previously-defaulted field cannot be removed by `kubectl apply`, which makes refusing one an upgrade nobody can complete. +**Before you write a CRD description:** +[`facts/crd-descriptions-in-kubectl-explain.md`](facts/crd-descriptions-in-kubectl-explain.md) +covers how `kubectl explain` renders a doc comment (line breaks kept, each line rewrapped at 76 +characters of text, so longer lines leave stray words), behavior a description must not misstate, +and Flux's writing style guide. + ## The contracts — [`spec/`](spec/) The code cites these. Breaking one without updating it is how the next person gets diff --git a/docs/facts/crd-descriptions-in-kubectl-explain.md b/docs/facts/crd-descriptions-in-kubectl-explain.md new file mode 100644 index 00000000..2254eba2 --- /dev/null +++ b/docs/facts/crd-descriptions-in-kubectl-explain.md @@ -0,0 +1,120 @@ +# CRD descriptions in `kubectl explain`: keep each line to 76 characters + +> **reference**: durable background. Index: [`../INDEX.md`](../INDEX.md) +> +> Measured on 2026-10-25 with `kubectl` v1.37.1 against an envtest API server (the binaries in +> `bin/k8s/current`), using this repository's CRDs at commit `0b8334eb`. The behavior facts at the +> end were read from the controller source at that commit. + +## Why this matters + +A CRD field's doc comment is the text an operator reads in `kubectl explain`. How it renders there +depends on where the comment's lines break as well as on what it says. Most of our descriptions +currently render with stray words on their own lines, because their source lines are too long for +the terminal output. Restructuring a description, for example into bullets, does not help until the +line width is right. + +## The facts + +1. **controller-gen keeps the comment's line breaks.** Each line of the Go doc comment becomes a + line of the generated `description`, written as a `|-` block scalar in `config/crd/bases`. A + blank comment line becomes a blank line in the description. + +2. **`kubectl explain` keeps those line breaks, then wraps each line again at 80 columns.** The + output indents the description by 4 spaces, so a line keeps 76 characters of text. A line of 76 + characters prints whole. A line of 77 or more has its last words pushed onto a line of their + own, and the next source line starts on a new line again. Today's + `GitTarget.spec.serializeNamespace` prints like this: + + ```text + SerializeNamespace decides whether a namespaced resource's document carries + metadata.namespace. It applies to every write, not just the first, and is + ignored for + cluster-scoped resources. + ``` + +3. **Lists render as lists.** A line starting with a `-` bullet stays at the start of its own line, so + bullets work in `kubectl explain`. A bullet's continuation lines are subject to the same + 76-character limit as any other line. + +4. **Most of the current text is too wide.** At `0b8334eb`, 616 of the 1,067 description lines + across the ten `configbutler.ai` CRDs are longer than 76 characters. The longest is 138. + +## What follows for writing descriptions + +- Wrap the text of every API doc comment at 76 characters, measured after the `//` prefix and its space. A + shorter line costs nothing in the source and prints whole in the terminal. +- Use a blank comment line to start a paragraph, and `-` bullets for a list of values or cases. +- Check the width mechanically rather than by eye: any line longer than 76 characters in a + `description` under `config/crd/bases` will print badly. +- The house style for the words themselves is in [AGENTS.md](../../AGENTS.md), under API Changes, + and [style-guide.md](../style-guide.md) covers prose in markdown. + +## How to reproduce + +`kubectl explain` needs a server that publishes the CRD's OpenAPI schema; a running cluster is not +required. Start an envtest API server with the CRDs loaded, write an admin kubeconfig, and run +`kubectl explain`. Retry for a few seconds, because the schema is published shortly after the CRD +is created and an early call fails with `couldn't find resource`: + +```go +env := &envtest.Environment{CRDDirectoryPaths: []string{crdDir}, ErrorIfCRDPathMissing: true} +cfg, _ := env.Start() +defer env.Stop() +user, _ := env.AddUser(envtest.User{Name: "admin", Groups: []string{"system:masters"}}, cfg) +kubeconfig, _ := user.KubeConfig() +// write kubeconfig to a file, then retry until it succeeds: +// kubectl --kubeconfig explain gittarget.spec.serializeNamespace +``` + +Run it with `KUBEBUILDER_ASSETS=bin/k8s/current`. To test a draft, edit the `description` in a copy +of the generated CRD and point `crdDir` at the copy. + +## Behavior a description must match + +These came out of the same review. Each one is behavior that a shorter or rearranged description +can easily misstate. + +- **A CommitRequest's message can be wrapped.** The message is never evaluated as a template. + When the GitTarget sets `spec.commit.message.requestTemplate`, that template frames the message + as `RequestMessage`; without one, the message is committed as written. See + [`commit_executor.go`](../../internal/git/commit_executor.go). +- **`GitProvider.status.lastVerifiedAt` records the last success only.** It is set after a + successful connectivity check and never cleared. Every earlier failure in the same reconcile + also leaves it unchanged: a signing-key problem or a missing or malformed Secret stops the + reconcile before the connectivity check. So it bounds when the provider last worked, not when a + failure began. See [`gitprovider_controller.go`](../../internal/controller/gitprovider_controller.go). +- **`ArgoCDClaim` `InControl` uses all three statuses.** See + [`argocdclaim_hold.go`](../../internal/controller/argocdclaim_hold.go): + + | Status | Reason | When | + | --- | --- | --- | + | `True` | `AutomatedSyncOff` | Automated sync is off and GitOps Reverser starts every sync, including when another field manager owns the switch and keeps it off. | + | `False` | `NotReferenced` | No GitTarget references the claim. | + | `False` | `Releasing`, `Revoking` | The claim is handing the Application back. | + | `False` | `ArgoCDVersionNotSupported` | The Application has no `spec.syncPolicy.automated.enabled` (Argo CD before 3.1). | + | `False` | `NotConfigured` | Another field manager owns the switch and has turned automated sync on. | + | `Unknown` | `NotConfigured` | `Configured` is not `True` for any other reason. | + + On a failed read of the Application, the previous `InControl` value is kept. +- **Grant lists: omitted and empty mean different things, except on GitProvider.** On + `GitBranch.spec.allowedGitTargets` and both `KubeProvider` grant lists, omitting the list + defaults it to one grant for the object's own namespace, while `[]` allows nobody. On + `GitProvider.spec.allowedGitBranches` there is no default: omitted and `[]` both deny every + GitBranch, including those in the provider's own namespace. + +## Writing-style references worth borrowing + +- **Flux's [documentation style guide](https://fluxcd.io/contributing/docs/style-guide/)** + (source in [fluxcd/website](https://github.com/fluxcd/website/blob/main/content/en/contributing/docs/style-guide.md)). + It asks for active voice, present tense, and direct wording, and it avoids idioms that are hard + for readers whose first language is not English. It also puts related items that must appear in + a specific order into a list, and keeps paragraphs under six sentences. It is written for + Flux's website, and it fits this repository's docs and CRD descriptions as well. +- **Flux's API types, as worked examples of CRD descriptions.** In + [GitRepository](https://github.com/fluxcd/source-controller/blob/main/api/v1/gitrepository_types.go), + `SecretRef` gives HTTPS and SSH requirements separate sentences, and `Provider` states its + default on its own. In + [Kustomization](https://github.com/fluxcd/kustomize-controller/blob/main/api/v1/kustomization_types.go), + `Wait` gives the action, its interaction with `HealthChecks`, and its default one sentence each. + Not every Flux description is this clean, so treat these as examples rather than a standard. From 2d6736e6561811aadd8c5e0c8abd001d8aea758f Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 07:54:14 +0000 Subject: [PATCH 03/15] docs(review): revise the CRD description proposal after review Codex's revision after Claude's review: adds the missing InControl reason ArgoCDVersionNotSupported, marks the onRefusal and serializeNamespace rewrites optional because those fields already give each value its own paragraph, adds the 76-character wrap as a writing rule ahead of any bullet changes, and drops the generic review sections. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/review/20261025-crd-text-proposal.md | 309 +++++++++++----------- 1 file changed, 151 insertions(+), 158 deletions(-) diff --git a/docs/review/20261025-crd-text-proposal.md b/docs/review/20261025-crd-text-proposal.md index e1cd3441..b6df5117 100644 --- a/docs/review/20261025-crd-text-proposal.md +++ b/docs/review/20261025-crd-text-proposal.md @@ -4,7 +4,7 @@ The branch makes the API reference more consistent, but several descriptions sti to unpack too many rules at once. Keep the useful detail. Give each sentence one job, and use bullets where readers need to compare values, check requirements, or follow a precedence order. -This is a proposal only. No Go comments, generated CRDs, or existing documentation are changed. +This proposal changes wording only; it proposes no schema or controller behavior changes. ## Scope @@ -23,10 +23,12 @@ plain-text conventions required by [AGENTS.md](../../AGENTS.md). Markdown commen - Defaults, namespace resolution, and immutable references are documented near the setting. - The short descriptions already work. Keep fields such as `GitTarget.spec.path`, `GitProvider.spec.url`, and `GitTarget.spec.encryption` compact. +- `onRefusal` and `serializeNamespace` already give each value its own paragraph. Their structure + works; converting those paragraphs to bullets is optional polish. - The generated YAML excludes the long type-level design rationale I checked. For example, `GitTarget.spec.placement` uses the field comment. The internal discussion on - `GitTargetPlacementSpec` stays in the source. Review the generated description before deciding a comment - needs editorial work. + `GitTargetPlacementSpec` stays in the source. Review the generated description before deciding + a comment needs editorial work. ## Top risks (ranked) @@ -36,13 +38,14 @@ plain-text conventions required by [AGENTS.md](../../AGENTS.md). Markdown commen claims to show how long a connection has been broken; it records the last successful check. Failure may begin later, and `Ready=False` can also reflect signing or configuration failures. These need corrections before cosmetic edits. -2. **Dense descriptions hide consequential choices.** `serializeNamespace`, `onRefusal`, grant - defaults, and selector pruning all affect what gets written or deleted. A reader should be - able to find each choice without rereading a paragraph of exceptions. +2. **Grant defaults and selector pruning need clearer separation.** Grant descriptions compress + omission, explicit lists, and combined permissions into a paragraph. Selector descriptions mix + matching, deletion behavior, and conflicting rules. Separate these cases so readers can find + the one that applies to them. 3. **Some references leave readers guessing which object or condition is meant.** Both watch-rule `objectSelector` descriptions refer to `spec.prune.mode` without naming the `GitTarget`. `ArgoCDClaim.status.conditions` mixes `True` and `False` with `NotConfigured`, which is a - reason, not a condition status. + reason, not a condition status. It also omits `False` with reason `ArgoCDVersionNotSupported`. 4. **Long troubleshooting prose is squeezed into a table.** The new row about repeated `ClaimedResourceReconciled` events in the Flux guide contains the symptom, implementation cause, ownership explanation, and remedy. Give that case its own subsection and leave a short @@ -58,8 +61,8 @@ Flux has a [documentation style guide](https://fluxcd.io/contributing/docs/style in its [website source](https://github.com/fluxcd/website/blob/main/content/en/contributing/docs/style-guide.md). It recommends active voice, present tense, direct wording, and avoiding idioms that make reading harder for people who speak English as a second language. It recommends grouping related items -into lists and keeping paragraphs below six sentences. These are website guidelines; I found no -separate CRD-comment writing standard in the sources reviewed. +into lists and keeping paragraphs below six sentences. This is a website writing guide; it does +not prescribe a format for CRD comments. The source offers useful examples too: @@ -77,7 +80,9 @@ The source offers useful examples too: My recommendation is to adopt those structural habits while keeping this repository's conventions for field names and literal values. The recommendation to use bullets inside selected CRD descriptions is my editorial proposal. Flux's guide does not require it. Sources checked on -October 25, 2026; the source links track their default branches. +October 25, 2026, and rechecked when incorporating this feedback. The source links track their +default branches. The observations above describe these examples, not a project-wide enforcement +policy. ## Recommended changes (actionable) @@ -86,6 +91,11 @@ October 25, 2026; the source links track their default branches. - Lead with what the field controls. Follow with the default or omitted behavior. - Use bullets for alternatives and independent requirements. Use numbered lists when order matters. - Keep a short explanation in prose when there is no choice to compare. +- Keep existing per-value paragraphs when they already make the choices clear. +- Wrap description text at 76 characters after the Go comment prefix. The repository's + [rendering measurements](../facts/crd-descriptions-in-kubectl-explain.md) show that longer lines + wrap again in `kubectl explain`, leaving stray words on separate lines. Fix that before optional + paragraph-to-bullet changes. - Name the subject: the `GitTarget`, the source cluster, or the referenced `GitBranch`. - Prefer familiar verbs: `allows`, `uses`, `rejects`, and `restores`. Replace phrases such as `a written list`, `admits nobody`, and `hands the switch back` with concrete behavior. @@ -94,121 +104,109 @@ October 25, 2026; the source links track their default branches. - Aim for two to four sentences in ordinary descriptions. Treat the existing 80-word target as a prompt to restructure longer text. Preserve necessary warnings. -### 1. Turn namespace choices into bullets +### 1. Explain the message override accurately -Location: [ConsumerNamespaces.From](../../api/v1alpha3/access_grants.go). - -The existing sentence puts four enum values and four explanations in one line of thought. -Each value is a separate choice: +Location: [CommitRequest.spec.message](../../api/v1alpha3/commitrequest_types.go). ```text -From selects which namespaces this grant allows consumers from: +Message supplies the requested commit message. Omitted, the GitTarget's +spec.commit.message.liveTemplate generates the message. -- "Same": the namespace of the object that defines the grant. -- "Names": the namespaces listed in names. -- "Selector": namespaces whose labels match selector. -- "All": every namespace. +The text is never evaluated as a template. If the GitTarget configures +spec.commit.message.requestTemplate, that template wraps this text. +Otherwise, the text is used as written. + +The message must contain 1–1024 characters and at least one non-whitespace +character. Newlines are allowed for a subject and body; other control +characters are rejected. ``` -### 2. Make the omitted and empty grant lists easy to distinguish +Short paragraphs work here: the message policy and validation are separate topics, but neither +needs an artificial list. -Location: [GitBranch.spec.allowedGitTargets](../../api/v1alpha3/gitbranch_types.go). +### 2. Describe the timestamp without claiming an outage duration -The distinction is already present, but `a written list is the whole policy` makes the reader -translate it. State each case: +Location: [GitProvider.status.lastVerifiedAt](../../api/v1alpha3/gitprovider_types.go). ```text -AllowedGitTargets controls which GitTargets may write through this GitBranch. - -- Omitted: allows GitTargets in this GitBranch's namespace. -- Empty list ([]): allows no GitTargets. -- Nonempty list: allows only GitTargets matched by at least one grant. - -An explicit list replaces the default. Include a grant for this GitBranch's -namespace if GitTargets there still need access. +LastVerifiedAt is the time of the last successful repository connectivity +check using this GitProvider's credentials, or anonymous access when +spec.secretRef is omitted. Failed checks leave this timestamp unchanged. +Check status.conditions for the reason the GitProvider is not ready. ``` -Apply the same structure to `KubeProvider.spec.allowedGitTargets` and `allowedApplierClaims`, -preserving their source-namespace and applier-namespace defaults. Keep -`GitProvider.spec.allowedGitBranches` distinct: both omission and an empty list deny access. +The controller updates the timestamp only after a successful remote check. The timestamp cannot +identify when a later failure began. -### 3. Separate refusal choices from the restrictions on reapplying +### 3. Distinguish condition status from reason -Location: [GitTarget.spec.onRefusal](../../api/v1alpha3/gittarget_types.go). +Location: [ArgoCDClaim.status.conditions](../../api/v1alpha3/argocdclaim_types.go). + +Replace the paragraph beginning `InControl is True` with: ```text -OnRefusal controls what happens when a live edit cannot be written to the folder. -Defaults to "Ignore". A refused edit makes no commit and remains on the cluster -until an applier overwrites it from Git. +InControl reports whether GitOps Reverser controls when the Application +syncs: -- "Ignore": reports the refusal in conditions and events. -- "Reapply": uses spec.applierClaimRef to reapply Git after pending edits are - published, reverting the refused edit. Requires spec.applierClaimRef. +- True, reason AutomatedSyncOff: GitOps Reverser controls sync timing. +- False: control is being released or revoked, or no GitTarget references + the claim. +- False, reason NotConfigured: another field manager has enabled automated + sync. +- False, reason ArgoCDVersionNotSupported: the Application API does not + support spec.syncPolicy.automated.enabled, so GitOps Reverser cannot + disable automated sync through that field. +- Unknown, reason NotConfigured: the claim is not configured to control the + Application. -Reapply runs only when the document already exists in the folder and the write -removes no documents. It does not run for folder-wide failures, such as invalid -YAML. A refusal that persists triggers one reapply, rather than one per check. +A claim with no referencing GitTarget reads no applier and reports +Ready=True, reason NotReferenced. ``` -This is a justified exception to the word-count target: the restrictions prevent a reader from -expecting a reapply to repair every refusal. - -### 4. Present namespace serialization as three choices - -Location: [GitTarget.spec.serializeNamespace](../../api/v1alpha3/gittarget_types.go). - -```text -SerializeNamespace controls whether written documents include metadata.namespace. -It applies to every write of a namespaced resource. Cluster-scoped resources -ignore this setting. - -- Omitted: leaves out metadata.namespace only when the kustomization governing - the document sets the same namespace. -- true: always includes metadata.namespace. Use this for a flat folder applied - directly. -- false: always omits metadata.namespace. The namespace must come from a - kustomization, a Flux Kustomization's targetNamespace, or an Argo CD - Application's destination.namespace. +These cases come from `holdArgoCD`, `ownedSwitchCondition`, and `switchOff`. The version check in +`switchOff` also catches an Argo CD downgrade after the configuration check. Apply the same +status/reason distinction when editing the longer `Configured` paragraph and Flux conditions. -With false, the folder accepts only one source namespace. A WatchRule that adds -a second is rejected with GitPathAccepted=False, reason MultipleSourceNamespaces. -``` +### 4. Turn namespace choices into bullets -### 5. Explain the message override accurately +Location: [ConsumerNamespaces.From](../../api/v1alpha3/access_grants.go). -Location: [CommitRequest.spec.message](../../api/v1alpha3/commitrequest_types.go). +The existing sentence puts four enum values and four explanations in one line of thought. +Each value is a separate choice: ```text -Message supplies the requested commit message. Omitted, the GitTarget's -spec.commit.message.liveTemplate generates the message. - -The text is never evaluated as a template. If the GitTarget configures -spec.commit.message.requestTemplate, that template wraps this text. Otherwise, -the text is used as written. +From selects which namespaces this grant allows consumers from: -The message must contain 1–1024 characters and at least one non-whitespace -character. Newlines are allowed for a subject and body; other control characters -are rejected. +- "Same": the namespace of the object that defines the grant. +- "Names": the namespaces listed in names. +- "Selector": namespaces whose labels match selector. +- "All": every namespace. ``` -Short paragraphs work here: the message policy and validation are separate topics, but neither -needs an artificial list. +### 5. Make the omitted and empty grant lists easy to distinguish -### 6. Describe the timestamp without claiming an outage duration +Location: [GitBranch.spec.allowedGitTargets](../../api/v1alpha3/gitbranch_types.go). -Location: [GitProvider.status.lastVerifiedAt](../../api/v1alpha3/gitprovider_types.go). +The distinction is already present, but `a written list is the whole policy` makes the reader +translate it. State each case: ```text -LastVerifiedAt is the time of the last successful repository connectivity check -using this GitProvider's credentials, or anonymous access when spec.secretRef -is omitted. Failed checks leave this timestamp unchanged. Check status.conditions -for the reason the GitProvider is not ready. +AllowedGitTargets controls which GitTargets may write through this +GitBranch. + +- Omitted: allows GitTargets in this GitBranch's namespace. +- Empty list ([]): allows no GitTargets. +- Nonempty list: allows only GitTargets matched by at least one grant. + +An explicit list replaces the default. Include a grant for this GitBranch's +namespace if GitTargets there still need access. ``` -The controller updates the timestamp only after a successful remote check. The timestamp cannot -identify when a later failure began. +Apply the same structure to `KubeProvider.spec.allowedGitTargets` and `allowedApplierClaims`, +preserving their source-namespace and applier-namespace defaults. Keep +`GitProvider.spec.allowedGitBranches` distinct: both omission and an empty list deny access. -### 7. Give each receiver type its own line +### 6. Give each receiver type its own line Location: [GitReceiver.spec.type](../../api/v1alpha3/gitreceiver_types.go). @@ -225,7 +223,7 @@ Type selects the push payload and authentication format: This also replaces the JSON-like `{"repository", "branch", "after"}` fragment, which names fields but is not valid JSON. -### 8. Keep template fields available, but group them +### 7. Keep template fields available, but group them Location: [GitTarget.spec.commit.message.liveTemplate](../../api/v1alpha3/gitprovider_types.go). @@ -233,8 +231,8 @@ The current description combines the template context, per-resource fields, empt and request overrides. Group the context without dropping fields: ```text -LiveTemplate is the Go template for commit messages generated from live edits. -An empty value uses the built-in template. +LiveTemplate is the Go template for commit messages generated from live +edits. An empty value uses the built-in template. The template receives: @@ -246,8 +244,8 @@ The template receives: Each resource provides Operation, Group, Version, Resource, Namespace, Name, APIVersion, ResourceVersion, and Generation. ResourceVersion identifies the -state written by the commit and may be empty. Generation is 0 for a kind without -a spec. Guard both values with {{with}}. +state written by the commit and may be empty. Generation is 0 for a kind +without a spec. Guard both values with {{with}}. A CommitRequest message takes precedence without changing the author. The requestTemplate field controls whether that message is wrapped. @@ -255,30 +253,7 @@ requestTemplate field controls whether that message is wrapped. Keep examples and longer context definitions in the existing [commit message guide](../commit-messages.md). -### 9. Distinguish condition status from reason - -Location: [ArgoCDClaim.status.conditions](../../api/v1alpha3/argocdclaim_types.go). - -Replace the paragraph beginning `InControl is True` with: - -```text -InControl reports whether GitOps Reverser controls when the Application syncs: - -- True, reason AutomatedSyncOff: GitOps Reverser controls sync timing. -- False: control is being released or revoked, or no GitTarget references the - claim. It is also False with reason NotConfigured if another field manager - has enabled automated sync. -- Unknown, reason NotConfigured: the claim is not configured to control the - Application. - -A claim with no referencing GitTarget reads no applier and reports Ready=True, -reason NotReferenced. -``` - -This preserves the difference found in `holdArgoCD` and `ownedSwitchCondition`. Apply the same -status/reason distinction when editing the longer `Configured` paragraph and Flux conditions. - -### 10. Move the long Flux troubleshooting row into a subsection +### 8. Move the long Flux troubleshooting row into a subsection Location: [Flux troubleshooting](../bi-directional-flux.md#troubleshooting). @@ -300,6 +275,57 @@ Proposed subsection text: Keep the symptom in the table and link to this explanation. The adjacent `Overriding by hand` bullet also benefits from two sentences: first the unchecked apply, then the claim's response. +### 9. Optional: use bullets for refusal choices + +Location: [GitTarget.spec.onRefusal](../../api/v1alpha3/gittarget_types.go). + +The existing description already separates `Ignore` and `Reapply` into paragraphs. Keep it unless +a rendered comparison shows a benefit. This alternative mainly separates the restrictions on +`Reapply` from the value's immediate effect: + +```text +OnRefusal controls what happens when a live edit cannot be written to the +folder. Defaults to "Ignore". A refused edit makes no commit and remains on +the cluster until an applier overwrites it from Git. + +- "Ignore": reports the refusal in conditions and events. +- "Reapply": uses spec.applierClaimRef to reapply Git after pending edits + are published, reverting the refused edit. Requires spec.applierClaimRef. + +Reapply runs only when the document already exists in the folder and the +write removes no documents. It does not run for folder-wide failures, such +as invalid YAML. A refusal that persists triggers one reapply, rather than +one per check. +``` + +This is a justified exception to the word-count target: the restrictions prevent a reader from +expecting a reapply to repair every refusal. + +### 10. Optional: use bullets for namespace serialization + +Location: [GitTarget.spec.serializeNamespace](../../api/v1alpha3/gittarget_types.go). + +Omission, `true`, and `false` already have separate paragraphs. Bullets may make the alternatives +easier to scan, but this is a lower-priority presentation choice. Correct line wrapping first: + +```text +SerializeNamespace controls whether written documents include +metadata.namespace. It applies to every write of a namespaced resource. +Cluster-scoped resources ignore this setting. + +- Omitted: leaves out metadata.namespace only when the kustomization + governing the document sets the same namespace. +- true: always includes metadata.namespace. Use this for a flat folder + applied directly. +- false: always omits metadata.namespace. The namespace must come from a + kustomization, a Flux Kustomization's targetNamespace, or an Argo CD + Application's destination.namespace. + +With false, the folder accepts only one source namespace. A WatchRule that +adds a second is rejected with GitPathAccepted=False, reason +MultipleSourceNamespaces. +``` + ### Remaining pass, in priority order | Location | Proposed treatment | @@ -314,47 +340,14 @@ bullet also benefits from two sentences: first the unchecked apply, then the cla | `GitTarget.spec.suspend` | Separate stopping new commits, the final push, and resuming from cluster state. | | Upgrade entry for removed message templates | Separate removal, client behavior, and the migration action. | -## Contract integrity (spec/status, status subresource, Conditions) - -All ten generated CRDs have a status subresource, `status.observedGeneration`, and condition lists -keyed by `type`. This proposal changes their explanations only. Controller behavior was checked -for the specific wording corrections above; readiness behavior across all controllers was not -retested. - -## Schema correctness & webhooks (validation, defaults, CEL) - -The omitted-versus-empty grant distinction agrees with the generated defaults and -[grant helpers](../../api/v1alpha3/access_grants.go). Preserve enum values, namespace requirements, -and immutable-reference rules. This review proposes no schema, validation, or webhook changes. - -## GitOps/SSA ergonomics (lists, patchability) - -Grant lists are atomic; condition lists are maps keyed by `type`. Prose bullets have no relationship -to these schema list types. Keep both unchanged during the wording pass. - -## Operator UX (printer columns) - -Evaluate proposed bullets in actual `kubectl explain` output before applying them throughout the -API. Check both default terminal output and any generated reference page: Markdown appearance -alone does not establish terminal readability. No rendering test has been performed for these -proposals. Printer-column changes are outside this editorial scope. - -## Compatibility & migration impact (mandatory) - -The proposed wording changes are non-breaking. The existing branch also removes retired fields -and their rejection rules; it is not exclusively a description change. The parsed CRD comparison -against the merge base confirms those removals in `GitTarget`, `GitProvider`, `FluxClaim`, and -`ArgoCDClaim`. Their migration behavior belongs in [the upgrade guide](../UPGRADING.md), separately -from this proposal. - -The ten inspected CRDs serve and store `v1alpha3`. No API version or conversion change is proposed. - -## Minimal improvement plan (PR-sized) +## Implementation order 1. Correct the message, timestamp, condition, and cross-object-reference wording. -2. Apply the enum and grant-list examples, then preview their generated descriptions. +2. Fix description line widths, then apply the enum and grant-list examples. 3. Restructure the remaining long descriptions and the Flux troubleshooting case. -4. Regenerate CRDs into a scratch directory and compare with the pre-edit branch snapshot after +4. Preview `kubectl explain` output. Consider the optional changes to `onRefusal` and + `serializeNamespace` only if they improve that output. +5. Regenerate CRDs into a scratch directory and compare with the pre-edit branch snapshot after stripping every `description` key. Preserve scopes, defaults, validation, and list semantics. -5. Run the repository's required validation for an API-comment implementation. For this proposal +6. Run the repository's required validation for an API-comment implementation. For this proposal alone, run `task lint-docs` and Vale on this file. From 3e96d2a59432cdee18efad9ef501991c3bc4f733 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 07:54:15 +0000 Subject: [PATCH 04/15] docs(review): finish the CRD description proposal Ranks the kubectl explain line width as risk 2, since it affects more of the text than any other finding. Adds Kind, Labels, and the Label accessor to the liveTemplate rewrite, which claimed to keep every field but dropped them. Names Argo CD before 3.1 in the version reason, quotes Flux's list advice as written (ordered items only), and makes the rewrap its own commit with a lint check so it stays fixed. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/review/20261025-crd-text-proposal.md | 76 +++++++++++++---------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/docs/review/20261025-crd-text-proposal.md b/docs/review/20261025-crd-text-proposal.md index b6df5117..e1854687 100644 --- a/docs/review/20261025-crd-text-proposal.md +++ b/docs/review/20261025-crd-text-proposal.md @@ -4,7 +4,8 @@ The branch makes the API reference more consistent, but several descriptions sti to unpack too many rules at once. Keep the useful detail. Give each sentence one job, and use bullets where readers need to compare values, check requirements, or follow a precedence order. -This proposal changes wording only; it proposes no schema or controller behavior changes. +This proposal changes wording and line wrapping, and adds one lint check for line width. It proposes +no schema or controller behavior changes. ## Scope @@ -38,15 +39,20 @@ plain-text conventions required by [AGENTS.md](../../AGENTS.md). Markdown commen claims to show how long a connection has been broken; it records the last successful check. Failure may begin later, and `Ready=False` can also reflect signing or configuration failures. These need corrections before cosmetic edits. -2. **Grant defaults and selector pruning need clearer separation.** Grant descriptions compress +2. **Most descriptions render badly in `kubectl explain`.** It keeps each source line break and + wraps every line again at 76 characters of text, so a longer line leaves its last words on a + line of their own. 616 of the 1,067 description lines are longer than that. See the + [rendering measurements](../facts/crd-descriptions-in-kubectl-explain.md). Restructuring a + description does not help until its lines fit. +3. **Grant defaults and selector pruning need clearer separation.** Grant descriptions compress omission, explicit lists, and combined permissions into a paragraph. Selector descriptions mix matching, deletion behavior, and conflicting rules. Separate these cases so readers can find the one that applies to them. -3. **Some references leave readers guessing which object or condition is meant.** Both watch-rule +4. **Some references leave readers guessing which object or condition is meant.** Both watch-rule `objectSelector` descriptions refer to `spec.prune.mode` without naming the `GitTarget`. `ArgoCDClaim.status.conditions` mixes `True` and `False` with `NotConfigured`, which is a reason, not a condition status. It also omits `False` with reason `ArgoCDVersionNotSupported`. -4. **Long troubleshooting prose is squeezed into a table.** The new row about repeated +5. **Long troubleshooting prose is squeezed into a table.** The new row about repeated `ClaimedResourceReconciled` events in the Flux guide contains the symptom, implementation cause, ownership explanation, and remedy. Give that case its own subsection and leave a short link in the table. @@ -60,9 +66,9 @@ Evidence for the behavior corrections: [request message rendering](../../interna Flux has a [documentation style guide](https://fluxcd.io/contributing/docs/style-guide/), maintained in its [website source](https://github.com/fluxcd/website/blob/main/content/en/contributing/docs/style-guide.md). It recommends active voice, present tense, direct wording, and avoiding idioms that make reading -harder for people who speak English as a second language. It recommends grouping related items -into lists and keeping paragraphs below six sentences. This is a website writing guide; it does -not prescribe a format for CRD comments. +harder for people who speak English as a second language. It puts related items that must appear +in a specific order into a list, and keeps paragraphs under six sentences. This is a website +writing guide; it does not prescribe a format for CRD comments. The source offers useful examples too: @@ -77,12 +83,10 @@ The source offers useful examples too: starts with a manifest and explains it with bullets. It gives authentication methods and branch selection their own sections. -My recommendation is to adopt those structural habits while keeping this repository's conventions -for field names and literal values. The recommendation to use bullets inside selected CRD -descriptions is my editorial proposal. Flux's guide does not require it. Sources checked on -October 25, 2026, and rechecked when incorporating this feedback. The source links track their -default branches. The observations above describe these examples, not a project-wide enforcement -policy. +Adopt those structural habits, and keep this repository's conventions for field names and literal +values. Flux's guide supports numbered lists for ordered items, such as a precedence order. Bullets +for unordered choices are this proposal's own recommendation. Sources checked on October 25, 2026; +the links track their default branches. ## Recommended changes (actionable) @@ -92,10 +96,8 @@ policy. - Use bullets for alternatives and independent requirements. Use numbered lists when order matters. - Keep a short explanation in prose when there is no choice to compare. - Keep existing per-value paragraphs when they already make the choices clear. -- Wrap description text at 76 characters after the Go comment prefix. The repository's - [rendering measurements](../facts/crd-descriptions-in-kubectl-explain.md) show that longer lines - wrap again in `kubectl explain`, leaving stray words on separate lines. Fix that before optional - paragraph-to-bullet changes. +- Wrap description text at 76 characters after the Go comment prefix (risk 2). Fix that before + optional paragraph-to-bullet changes. - Name the subject: the `GitTarget`, the source cluster, or the referenced `GitBranch`. - Prefer familiar verbs: `allows`, `uses`, `rejects`, and `restores`. Replace phrases such as `a written list`, `admits nobody`, and `hands the switch back` with concrete behavior. @@ -153,9 +155,9 @@ syncs: the claim. - False, reason NotConfigured: another field manager has enabled automated sync. -- False, reason ArgoCDVersionNotSupported: the Application API does not - support spec.syncPolicy.automated.enabled, so GitOps Reverser cannot - disable automated sync through that field. +- False, reason ArgoCDVersionNotSupported: the Application has no + spec.syncPolicy.automated.enabled field (Argo CD before 3.1), so GitOps + Reverser cannot turn automated sync off. - Unknown, reason NotConfigured: the claim is not configured to control the Application. @@ -228,7 +230,9 @@ but is not valid JSON. Location: [GitTarget.spec.commit.message.liveTemplate](../../api/v1alpha3/gitprovider_types.go). The current description combines the template context, per-resource fields, empty-value handling, -and request overrides. Group the context without dropping fields: +and request overrides. It also omits Kind, Labels, and the Label accessor, which the +[commit message guide](../commit-messages.md) documents. Group the context and add the missing +fields: ```text LiveTemplate is the Go template for commit messages generated from live @@ -242,10 +246,12 @@ The template receives: - Operations: counts of retained entries by operation kind. - Resources: the resources in the commit window. -Each resource provides Operation, Group, Version, Resource, Namespace, Name, -APIVersion, ResourceVersion, and Generation. ResourceVersion identifies the -state written by the commit and may be empty. Generation is 0 for a kind -without a spec. Guard both values with {{with}}. +Each resource provides Operation, Group, Version, Resource, Kind, Namespace, +Name, APIVersion, ResourceVersion, Generation, and Labels. Read one label +with {{.Label "key"}}. ResourceVersion identifies the state written by the +commit and may be empty. Kind and Labels are empty for a deletion. +Generation is 0 for a kind without a spec. Guard Kind, ResourceVersion, and +Generation with {{with}}. A CommitRequest message takes precedence without changing the author. The requestTemplate field controls whether that message is wrapped. @@ -342,12 +348,18 @@ MultipleSourceNamespaces. ## Implementation order -1. Correct the message, timestamp, condition, and cross-object-reference wording. -2. Fix description line widths, then apply the enum and grant-list examples. -3. Restructure the remaining long descriptions and the Flux troubleshooting case. -4. Preview `kubectl explain` output. Consider the optional changes to `onRefusal` and - `serializeNamespace` only if they improve that output. -5. Regenerate CRDs into a scratch directory and compare with the pre-edit branch snapshot after +1. Correct the message, timestamp, condition, template-field, and cross-object-reference wording. +2. Rewrap every description at 76 characters, in a commit of its own so the diff stays mechanical. + In the same change, add a `task lint` check that fails on any `description` line longer than 76 + characters under `config/crd/bases`, and add the limit to the CRD house style in + [AGENTS.md](../../AGENTS.md). Without the check, the next edit brings long lines back. +3. Apply the enum and grant-list examples. +4. Restructure the remaining long descriptions and the Flux troubleshooting case. +5. Preview `kubectl explain` output, using the envtest recipe in the + [rendering measurements](../facts/crd-descriptions-in-kubectl-explain.md#how-to-reproduce). + Consider the optional changes to `onRefusal` and `serializeNamespace` only if they improve + that output. +6. Regenerate CRDs into a scratch directory and compare with the pre-edit branch snapshot after stripping every `description` key. Preserve scopes, defaults, validation, and list semantics. -6. Run the repository's required validation for an API-comment implementation. For this proposal +7. Run the repository's required validation for an API-comment implementation. For this proposal alone, run `task lint-docs` and Vale on this file. From 18a1f6ebefee35e9ac54e0c8afea716aaa25bbea Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 07:54:15 +0000 Subject: [PATCH 05/15] chore(devcontainer): pin the Codex extension to 26.1007.21434 On 26.1021.22027 the Codex webview never renders in the devcontainer: every start logs renderer_ready_timeout after 30s. The panel last rendered about six hours before that release was published, and it rendered again right after rolling back to 26.1007.21434. A bare openai.chatgpt installs the latest release on every container start, so a rebuild would bring the broken one back. Co-Authored-By: Claude Opus 5.5 (1M context) --- .devcontainer/devcontainer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c4a454e2..9e1cd6e4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -83,7 +83,9 @@ "golang.go", "ms-kubernetes-tools.vscode-kubernetes-tools", "ms-azuretools.vscode-docker", - "openai.chatgpt", + // Pinned: 26.1021.22027's webview never renders in the devcontainer + // (renderer_ready_timeout). Unpin once a newer release is confirmed working. + "openai.chatgpt@26.1007.21434", "anthropic.claude-code" ] } From 9a7cb0c46010ac1987ef862049b4bf154ec0d5d2 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 08:14:23 +0000 Subject: [PATCH 06/15] docs(api): correct the CRD descriptions that misstate behavior CommitRequest.spec.message now says a GitTarget's requestTemplate can wrap it. GitProvider.status.lastVerifiedAt no longer claims to show how long a connection has been broken: it records the last success only. Both claim kinds list InControl by status and reason. ArgoCDClaim gains the False/ArgoCDVersionNotSupported and False/NotConfigured cases it omitted. liveTemplate lists Kind, Labels, the Label accessor, and the LabelValues and LabelValue accessors. Both watch-rule objectSelector fields name the GitTarget's spec.prune.mode and the CollectionOverlap reason. Co-Authored-By: Claude Opus 5.5 (1M context) --- api/v1alpha3/argocdclaim_types.go | 23 +++++++++-- api/v1alpha3/clusterwatchrule_types.go | 20 ++++++---- api/v1alpha3/commitrequest_types.go | 14 +++++-- api/v1alpha3/fluxclaim_types.go | 20 ++++++++-- api/v1alpha3/gitprovider_types.go | 38 ++++++++++++++----- api/v1alpha3/watchrule_types.go | 20 ++++++---- .../bases/configbutler.ai_argocdclaims.yaml | 23 +++++++++-- .../configbutler.ai_clusterwatchrules.yaml | 20 ++++++---- .../bases/configbutler.ai_commitrequests.yaml | 14 +++++-- .../crd/bases/configbutler.ai_fluxclaims.yaml | 20 ++++++++-- .../bases/configbutler.ai_gitproviders.yaml | 7 +++- .../crd/bases/configbutler.ai_gittargets.yaml | 31 +++++++++++---- .../crd/bases/configbutler.ai_watchrules.yaml | 20 ++++++---- docs/configuration.md | 6 ++- 14 files changed, 204 insertions(+), 72 deletions(-) diff --git a/api/v1alpha3/argocdclaim_types.go b/api/v1alpha3/argocdclaim_types.go index 0c8f0a47..0bb92b12 100644 --- a/api/v1alpha3/argocdclaim_types.go +++ b/api/v1alpha3/argocdclaim_types.go @@ -66,10 +66,25 @@ type ArgoCDClaimStatus struct { // several ArgoCDClaims for one Application, the one holding it already holds it, then the // oldest; the others are Configured=False, reason HeldByAnotherClaim. // - // InControl is True (AutomatedSyncOff) while GitOps Reverser decides when the Application - // syncs, False while it does not (Releasing, Revoking, NotReferenced), and NotConfigured while - // Configured is not True or automated sync was switched on. An ArgoCDClaim no GitTarget names - // reads nothing and is Ready=True, reason NotReferenced. + // InControl says whether GitOps Reverser decides when the Application + // syncs: + // + // - True, reason AutomatedSyncOff: automated sync is off and GitOps + // Reverser starts every sync, also when another field manager owns the + // switch and keeps it off. + // - False, reason Releasing or Revoking: the ArgoCDClaim is handing the + // Application back. + // - False, reason NotReferenced: no GitTarget names this ArgoCDClaim. + // - False, reason NotConfigured: another field manager owns the switch and + // has turned automated sync on. + // - False, reason ArgoCDVersionNotSupported: the Application has no + // spec.syncPolicy.automated.enabled field (Argo CD before 3.1). + // - Unknown, reason NotConfigured: Configured is not True, or another + // ArgoCDClaim holds the Application. + // + // A failed read of the Application keeps the previous InControl value. An + // ArgoCDClaim no GitTarget names reads nothing and is Ready=True, reason + // NotReferenced. // +optional // +listType=map // +listMapKey=type diff --git a/api/v1alpha3/clusterwatchrule_types.go b/api/v1alpha3/clusterwatchrule_types.go index 638a7839..dea942fa 100644 --- a/api/v1alpha3/clusterwatchrule_types.go +++ b/api/v1alpha3/clusterwatchrule_types.go @@ -64,13 +64,19 @@ type ClusterResourceRule struct { // +kubebuilder:validation:items:Pattern=`^[^/]*$` Resources []string `json:"resources"` - // ObjectSelector mirrors only the objects whose labels match, as a standard label selector. - // Omitted or {} selects every object. An object that stops matching leaves the mirror as a - // deleted one does, and spec.prune.mode decides what happens to its document. Under prune mode - // "Always", a resync also removes documents of the type that the selector never matched. Two - // rules on one GitTarget that select the same type must use the same selector; the newer rule - // is refused. A selector that does not parse refuses the whole rule (ResourcesResolved=False, - // reason InvalidObjectSelector). + // ObjectSelector mirrors only the objects whose labels match, as a + // standard label selector. Omitted or {} selects every object. + // + // An object that stops matching is handled like a deleted one: the + // GitTarget's spec.prune.mode decides whether its document is removed. + // With prune mode "Always", a resync also removes documents of the type + // that the selector does not match. + // + // Two rules on one GitTarget that select the same type must use the same + // selector. The older rule keeps the type, and the newer one is refused + // with ResourcesResolved=False, reason CollectionOverlap. A selector that + // does not parse refuses the whole rule with ResourcesResolved=False, + // reason InvalidObjectSelector. // +optional ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` } diff --git a/api/v1alpha3/commitrequest_types.go b/api/v1alpha3/commitrequest_types.go index 9327a381..b5ed5016 100644 --- a/api/v1alpha3/commitrequest_types.go +++ b/api/v1alpha3/commitrequest_types.go @@ -29,10 +29,16 @@ type CommitRequestSpec struct { // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.gitTargetRef.name must not be empty" GitTargetRef meta.LocalObjectReference `json:"gitTargetRef"` - // Message is the commit message, used as written: template syntax in it stays literal. It is - // 1–1024 characters, may contain newlines for a subject and body, and may not be only - // whitespace or contain any other control character. Omitted, the GitTarget's liveTemplate - // words the commit. + // Message is the commit message this request asks for. Omitted, the + // GitTarget's spec.commit.message.liveTemplate words the commit. + // + // The text is never evaluated as a template. If the GitTarget sets + // spec.commit.message.requestTemplate, that template wraps this text; + // otherwise the text is committed as written. + // + // It is 1–1024 characters and must contain a non-whitespace character. + // Newlines are allowed, for a subject and body; tabs and other control + // characters are rejected. // +optional // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=1024 diff --git a/api/v1alpha3/fluxclaim_types.go b/api/v1alpha3/fluxclaim_types.go index 2a1f9fe1..73eee5dd 100644 --- a/api/v1alpha3/fluxclaim_types.go +++ b/api/v1alpha3/fluxclaim_types.go @@ -59,10 +59,22 @@ type FluxClaimStatus struct { // one with an apply in flight holds it, then the one holding it already, then the oldest; the // others are Configured=False, reason HeldByAnotherClaim. // - // InControl is True while GitOps Reverser decides when the Kustomization applies (Suspended, - // Applying), False while it does not (Suspending, Releasing, Revoking, NotReferenced), and - // Unknown (NotConfigured) while Configured is not True. A FluxClaim no GitTarget names reads - // nothing and is Ready=True, reason NotReferenced. + // InControl says whether GitOps Reverser decides when the Kustomization + // applies: + // + // - True, reason Suspended: the Kustomization is suspended, and GitOps + // Reverser starts every apply. + // - True, reason Applying: an apply GitOps Reverser started is running; + // the Kustomization is suspended again when it ends. + // - False, reason Suspending: Flux has not yet handled the suspend. + // - False, reason Releasing or Revoking: the FluxClaim is handing the + // Kustomization back. + // - False, reason NotReferenced: no GitTarget names this FluxClaim. + // - Unknown, reason NotConfigured: Configured is not True, or another + // FluxClaim holds the Kustomization. + // + // A FluxClaim no GitTarget names reads nothing and is Ready=True, reason + // NotReferenced. // +optional // +listType=map // +listMapKey=type diff --git a/api/v1alpha3/gitprovider_types.go b/api/v1alpha3/gitprovider_types.go index 6c05bd58..7fc2e4b9 100644 --- a/api/v1alpha3/gitprovider_types.go +++ b/api/v1alpha3/gitprovider_types.go @@ -150,8 +150,11 @@ type GitProviderStatus struct { // The check runs on the steady reconcile interval, so a provider's resourceVersion moves at // that cadence. Providers are few and the check was already running; this adds no request. - // LastVerifiedAt is when the remote last answered this GitProvider's credential with its refs. - // It is never cleared: next to Ready=False, it says how long the connection has been broken. + // LastVerifiedAt is the time of the last successful connectivity check + // against the repository, using this GitProvider's credentials, or + // anonymous access when spec.secretRef is omitted. It is never cleared, and + // a failure leaves it unchanged, so it does not show when a failure began. + // status.conditions gives the reason the GitProvider is not ready. // +optional LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"` } @@ -185,13 +188,30 @@ type CommitterSpec struct { // CommitMessageSpec configures commit message formatting, on GitTarget.spec.commit.message. type CommitMessageSpec struct { - // LiveTemplate is the Go template for the message of a commit made from live edits, whatever - // its size. Fields: Author (the username, possibly empty), GitTarget, Count (edits after - // coalescing, before comparing with Git), Operations and Resources. Each resource has - // Operation, Group, Version, Resource, Namespace, Name, APIVersion, ResourceVersion and - // Generation; ResourceVersion is the state this commit wrote and can be empty, and Generation - // is 0 for a kind without a spec, so guard both with {{with}}. A CommitRequest's message - // replaces this template without changing the author. + // LiveTemplate is the Go template for the message of a commit made from + // live edits, whatever its size. Omitted, a built-in template is used. + // + // The template receives: + // + // - Author: the username, which may be empty. + // - GitTarget: the GitTarget's name. + // - Count: the number of edits after coalescing, before comparing with Git. + // - Operations: the number of edits per operation. + // - Resources: one entry per resource in the commit. + // - LabelValues "key": the distinct values of one label across Resources. + // - LabelValue "key": the value of one label that every resource shares, + // or empty when they differ. + // + // Each resource has Operation, Group, Version, Resource, Kind, Namespace, + // Name, APIVersion, ResourceVersion, Generation and Labels. Namespace is + // "_cluster" for a cluster-scoped resource. Read one label with + // {{.Label "key"}}; {{.Labels.key}} fails the commit for a resource without + // that label. Kind and Labels are empty for a deletion, ResourceVersion + // can be empty, and Generation is 0 for a kind without a spec, so guard + // them with {{with}}. + // + // A CommitRequest's message replaces this template without changing the + // author; spec.commit.message.requestTemplate can wrap that message. // +optional LiveTemplate string `json:"liveTemplate,omitempty"` diff --git a/api/v1alpha3/watchrule_types.go b/api/v1alpha3/watchrule_types.go index ef55b4a8..744729df 100644 --- a/api/v1alpha3/watchrule_types.go +++ b/api/v1alpha3/watchrule_types.go @@ -51,13 +51,19 @@ type ResourceRule struct { // +kubebuilder:validation:items:Pattern=`^[^/]*$` Resources []string `json:"resources"` - // ObjectSelector mirrors only the objects whose labels match, as a standard label selector. - // Omitted or {} selects every object. An object that stops matching leaves the mirror as a - // deleted one does, and spec.prune.mode decides what happens to its document. Under prune mode - // "Always", a resync also removes documents of the type that the selector never matched. Two - // rules on one GitTarget that select the same type in overlapping namespaces must use the same - // selector; the newer rule is refused. A selector that does not parse refuses the whole rule - // (ResourcesResolved=False, reason InvalidObjectSelector). + // ObjectSelector mirrors only the objects whose labels match, as a + // standard label selector. Omitted or {} selects every object. + // + // An object that stops matching is handled like a deleted one: the + // GitTarget's spec.prune.mode decides whether its document is removed. + // With prune mode "Always", a resync also removes documents of the type + // that the selector does not match. + // + // Two rules on one GitTarget that select the same type in overlapping + // namespaces must use the same selector. The older rule keeps the type, + // and the newer one is refused with ResourcesResolved=False, reason + // CollectionOverlap. A selector that does not parse refuses the whole rule + // with ResourcesResolved=False, reason InvalidObjectSelector. // +optional ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` diff --git a/config/crd/bases/configbutler.ai_argocdclaims.yaml b/config/crd/bases/configbutler.ai_argocdclaims.yaml index 07bc43d6..e96ba3f2 100644 --- a/config/crd/bases/configbutler.ai_argocdclaims.yaml +++ b/config/crd/bases/configbutler.ai_argocdclaims.yaml @@ -157,10 +157,25 @@ spec: several ArgoCDClaims for one Application, the one holding it already holds it, then the oldest; the others are Configured=False, reason HeldByAnotherClaim. - InControl is True (AutomatedSyncOff) while GitOps Reverser decides when the Application - syncs, False while it does not (Releasing, Revoking, NotReferenced), and NotConfigured while - Configured is not True or automated sync was switched on. An ArgoCDClaim no GitTarget names - reads nothing and is Ready=True, reason NotReferenced. + InControl says whether GitOps Reverser decides when the Application + syncs: + + - True, reason AutomatedSyncOff: automated sync is off and GitOps + Reverser starts every sync, also when another field manager owns the + switch and keeps it off. + - False, reason Releasing or Revoking: the ArgoCDClaim is handing the + Application back. + - False, reason NotReferenced: no GitTarget names this ArgoCDClaim. + - False, reason NotConfigured: another field manager owns the switch and + has turned automated sync on. + - False, reason ArgoCDVersionNotSupported: the Application has no + spec.syncPolicy.automated.enabled field (Argo CD before 3.1). + - Unknown, reason NotConfigured: Configured is not True, or another + ArgoCDClaim holds the Application. + + A failed read of the Application keeps the previous InControl value. An + ArgoCDClaim no GitTarget names reads nothing and is Ready=True, reason + NotReferenced. items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/bases/configbutler.ai_clusterwatchrules.yaml b/config/crd/bases/configbutler.ai_clusterwatchrules.yaml index c2e690c9..051c8f68 100644 --- a/config/crd/bases/configbutler.ai_clusterwatchrules.yaml +++ b/config/crd/bases/configbutler.ai_clusterwatchrules.yaml @@ -118,13 +118,19 @@ spec: type: array objectSelector: description: |- - ObjectSelector mirrors only the objects whose labels match, as a standard label selector. - Omitted or {} selects every object. An object that stops matching leaves the mirror as a - deleted one does, and spec.prune.mode decides what happens to its document. Under prune mode - "Always", a resync also removes documents of the type that the selector never matched. Two - rules on one GitTarget that select the same type must use the same selector; the newer rule - is refused. A selector that does not parse refuses the whole rule (ResourcesResolved=False, - reason InvalidObjectSelector). + ObjectSelector mirrors only the objects whose labels match, as a + standard label selector. Omitted or {} selects every object. + + An object that stops matching is handled like a deleted one: the + GitTarget's spec.prune.mode decides whether its document is removed. + With prune mode "Always", a resync also removes documents of the type + that the selector does not match. + + Two rules on one GitTarget that select the same type must use the same + selector. The older rule keeps the type, and the newer one is refused + with ResourcesResolved=False, reason CollectionOverlap. A selector that + does not parse refuses the whole rule with ResourcesResolved=False, + reason InvalidObjectSelector. properties: matchExpressions: description: matchExpressions is a list of label selector diff --git a/config/crd/bases/configbutler.ai_commitrequests.yaml b/config/crd/bases/configbutler.ai_commitrequests.yaml index 9cb8ea53..f0dbe161 100644 --- a/config/crd/bases/configbutler.ai_commitrequests.yaml +++ b/config/crd/bases/configbutler.ai_commitrequests.yaml @@ -91,10 +91,16 @@ spec: rule: self.name != '' message: description: |- - Message is the commit message, used as written: template syntax in it stays literal. It is - 1–1024 characters, may contain newlines for a subject and body, and may not be only - whitespace or contain any other control character. Omitted, the GitTarget's liveTemplate - words the commit. + Message is the commit message this request asks for. Omitted, the + GitTarget's spec.commit.message.liveTemplate words the commit. + + The text is never evaluated as a template. If the GitTarget sets + spec.commit.message.requestTemplate, that template wraps this text; + otherwise the text is committed as written. + + It is 1–1024 characters and must contain a non-whitespace character. + Newlines are allowed, for a subject and body; tabs and other control + characters are rejected. maxLength: 1024 minLength: 1 pattern: ^[^\x00-\x09\x0B-\x1F\x7F]*$ diff --git a/config/crd/bases/configbutler.ai_fluxclaims.yaml b/config/crd/bases/configbutler.ai_fluxclaims.yaml index 113a3e4d..ce266f85 100644 --- a/config/crd/bases/configbutler.ai_fluxclaims.yaml +++ b/config/crd/bases/configbutler.ai_fluxclaims.yaml @@ -149,10 +149,22 @@ spec: one with an apply in flight holds it, then the one holding it already, then the oldest; the others are Configured=False, reason HeldByAnotherClaim. - InControl is True while GitOps Reverser decides when the Kustomization applies (Suspended, - Applying), False while it does not (Suspending, Releasing, Revoking, NotReferenced), and - Unknown (NotConfigured) while Configured is not True. A FluxClaim no GitTarget names reads - nothing and is Ready=True, reason NotReferenced. + InControl says whether GitOps Reverser decides when the Kustomization + applies: + + - True, reason Suspended: the Kustomization is suspended, and GitOps + Reverser starts every apply. + - True, reason Applying: an apply GitOps Reverser started is running; + the Kustomization is suspended again when it ends. + - False, reason Suspending: Flux has not yet handled the suspend. + - False, reason Releasing or Revoking: the FluxClaim is handing the + Kustomization back. + - False, reason NotReferenced: no GitTarget names this FluxClaim. + - Unknown, reason NotConfigured: Configured is not True, or another + FluxClaim holds the Kustomization. + + A FluxClaim no GitTarget names reads nothing and is Ready=True, reason + NotReferenced. items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/bases/configbutler.ai_gitproviders.yaml b/config/crd/bases/configbutler.ai_gitproviders.yaml index e9ce641a..594689fc 100644 --- a/config/crd/bases/configbutler.ai_gitproviders.yaml +++ b/config/crd/bases/configbutler.ai_gitproviders.yaml @@ -405,8 +405,11 @@ spec: x-kubernetes-list-type: map lastVerifiedAt: description: |- - LastVerifiedAt is when the remote last answered this GitProvider's credential with its refs. - It is never cleared: next to Ready=False, it says how long the connection has been broken. + LastVerifiedAt is the time of the last successful connectivity check + against the repository, using this GitProvider's credentials, or + anonymous access when spec.secretRef is omitted. It is never cleared, and + a failure leaves it unchanged, so it does not show when a failure began. + status.conditions gives the reason the GitProvider is not ready. format: date-time type: string observedGeneration: diff --git a/config/crd/bases/configbutler.ai_gittargets.yaml b/config/crd/bases/configbutler.ai_gittargets.yaml index 25187d0c..81eaefdb 100644 --- a/config/crd/bases/configbutler.ai_gittargets.yaml +++ b/config/crd/bases/configbutler.ai_gittargets.yaml @@ -143,13 +143,30 @@ spec: properties: liveTemplate: description: |- - LiveTemplate is the Go template for the message of a commit made from live edits, whatever - its size. Fields: Author (the username, possibly empty), GitTarget, Count (edits after - coalescing, before comparing with Git), Operations and Resources. Each resource has - Operation, Group, Version, Resource, Namespace, Name, APIVersion, ResourceVersion and - Generation; ResourceVersion is the state this commit wrote and can be empty, and Generation - is 0 for a kind without a spec, so guard both with {{with}}. A CommitRequest's message - replaces this template without changing the author. + LiveTemplate is the Go template for the message of a commit made from + live edits, whatever its size. Omitted, a built-in template is used. + + The template receives: + + - Author: the username, which may be empty. + - GitTarget: the GitTarget's name. + - Count: the number of edits after coalescing, before comparing with Git. + - Operations: the number of edits per operation. + - Resources: one entry per resource in the commit. + - LabelValues "key": the distinct values of one label across Resources. + - LabelValue "key": the value of one label that every resource shares, + or empty when they differ. + + Each resource has Operation, Group, Version, Resource, Kind, Namespace, + Name, APIVersion, ResourceVersion, Generation and Labels. Namespace is + "_cluster" for a cluster-scoped resource. Read one label with + {{.Label "key"}}; {{.Labels.key}} fails the commit for a resource without + that label. Kind and Labels are empty for a deletion, ResourceVersion + can be empty, and Generation is 0 for a kind without a spec, so guard + them with {{with}}. + + A CommitRequest's message replaces this template without changing the + author; spec.commit.message.requestTemplate can wrap that message. type: string reconcileTemplate: description: |- diff --git a/config/crd/bases/configbutler.ai_watchrules.yaml b/config/crd/bases/configbutler.ai_watchrules.yaml index 7d435aad..e1adbfcc 100644 --- a/config/crd/bases/configbutler.ai_watchrules.yaml +++ b/config/crd/bases/configbutler.ai_watchrules.yaml @@ -117,13 +117,19 @@ spec: type: array objectSelector: description: |- - ObjectSelector mirrors only the objects whose labels match, as a standard label selector. - Omitted or {} selects every object. An object that stops matching leaves the mirror as a - deleted one does, and spec.prune.mode decides what happens to its document. Under prune mode - "Always", a resync also removes documents of the type that the selector never matched. Two - rules on one GitTarget that select the same type in overlapping namespaces must use the same - selector; the newer rule is refused. A selector that does not parse refuses the whole rule - (ResourcesResolved=False, reason InvalidObjectSelector). + ObjectSelector mirrors only the objects whose labels match, as a + standard label selector. Omitted or {} selects every object. + + An object that stops matching is handled like a deleted one: the + GitTarget's spec.prune.mode decides whether its document is removed. + With prune mode "Always", a resync also removes documents of the type + that the selector does not match. + + Two rules on one GitTarget that select the same type in overlapping + namespaces must use the same selector. The older rule keeps the type, + and the newer one is refused with ResourcesResolved=False, reason + CollectionOverlap. A selector that does not parse refuses the whole rule + with ResourcesResolved=False, reason InvalidObjectSelector. properties: matchExpressions: description: matchExpressions is a list of label selector diff --git a/docs/configuration.md b/docs/configuration.md index 6b021fc0..e9fe7924 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1723,8 +1723,10 @@ anything about the branch itself: an unreachable remote yields no observation at exactly what the pair `commit` + `lastVerifiedAt` is there to say. `GitProvider.status.lastVerifiedAt` is the same word for the connection rather than the branch: -when the credential and the repository were last proved together. It is never cleared, so -`Ready=False` beside it reads as "broken for this long". +when the credential and the repository were last proved together. It is never cleared, and a +failure leaves it unchanged, so it tells you when the provider last worked. +A `Ready=False` beside it can come from a later connectivity failure, or from a signing-key or +Secret problem that stops the check from running at all; the condition's reason says which. ### Keeping an idle target fresh (`--git-refresh-interval`) From 86a0ed9b2d4bbfe9fe2154dc4c74209dbbf43c9e Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 08:16:59 +0000 Subject: [PATCH 07/15] docs(api): rewrap the CRD descriptions at 76 characters kubectl explain keeps each line break of a description and wraps every line again at 76 characters of text, so a longer line leaves its last words on a line of their own. This rewraps every type and field doc comment in api/v1alpha3 that had a longer line, and changes no words: git diff --word-diff shows only moved line breaks. Co-Authored-By: Claude Opus 5.5 (1M context) --- api/v1alpha3/access_grants.go | 158 ++++---- api/v1alpha3/applierclaim_types.go | 60 +-- api/v1alpha3/argocdclaim_types.go | 98 ++--- api/v1alpha3/clusterwatchrule_types.go | 55 +-- api/v1alpha3/commitrequest_types.go | 135 ++++--- api/v1alpha3/fluxclaim_types.go | 90 +++-- api/v1alpha3/gitbranch_types.go | 142 +++---- api/v1alpha3/gitprovider_types.go | 138 +++---- api/v1alpha3/gitreceiver_types.go | 45 ++- api/v1alpha3/gittarget_types.go | 351 ++++++++++-------- api/v1alpha3/kubeprovider_types.go | 144 +++---- api/v1alpha3/prune_policy.go | 14 +- api/v1alpha3/shared_types.go | 8 +- api/v1alpha3/watchrule_types.go | 66 ++-- .../bases/configbutler.ai_argocdclaims.yaml | 137 +++---- .../configbutler.ai_clusterwatchrules.yaml | 50 +-- .../bases/configbutler.ai_commitrequests.yaml | 78 ++-- .../crd/bases/configbutler.ai_fluxclaims.yaml | 131 ++++--- .../bases/configbutler.ai_gitbranches.yaml | 136 +++---- .../bases/configbutler.ai_gitproviders.yaml | 112 +++--- .../bases/configbutler.ai_gitreceivers.yaml | 42 ++- .../crd/bases/configbutler.ai_gittargets.yaml | 336 +++++++++-------- .../bases/configbutler.ai_kubeproviders.yaml | 234 ++++++------ .../crd/bases/configbutler.ai_watchrules.yaml | 70 ++-- 24 files changed, 1554 insertions(+), 1276 deletions(-) diff --git a/api/v1alpha3/access_grants.go b/api/v1alpha3/access_grants.go index d13113bb..529728fa 100644 --- a/api/v1alpha3/access_grants.go +++ b/api/v1alpha3/access_grants.go @@ -44,22 +44,24 @@ const ConsumerNamespaceVariable = "{consumer.namespace}" // name bounds nothing on its own; beside the namespace it is a naming convention. const ConsumerNameVariable = "{consumer.name}" -// GrantNamespace is a namespace a grant admits, by exact name. A variable and "*" are refused. +// GrantNamespace is a namespace a grant admits, by exact name. A variable and +// "*" are refused. // +kubebuilder:validation:MaxLength=63 // +kubebuilder:validation:XValidation:rule="self != '{consumer.namespace}' && self != '{consumer.name}'",message="names: a variable is refused, because names are matched against the consumer's namespace and '{consumer.namespace}' would admit every consumer; use from: All to admit every namespace, or from: Same for the namespace of the object holding the grant" // +kubebuilder:validation:XValidation:rule="self != '*'",message="names: '*' is refused; use from: All to admit every namespace" // +kubebuilder:validation:XValidation:rule="self == '{consumer.namespace}' || self == '{consumer.name}' || self == '*' || self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')",message="names holds namespace names (DNS labels)" type GrantNamespace string -// ScopeNamespace is a namespace in a grant's scope: a name, "{consumer.namespace}" for the -// namespace named like the consumer's, or "*" for every namespace. +// ScopeNamespace is a namespace in a grant's scope: a name, +// "{consumer.namespace}" for the namespace named like the consumer's, or "*" +// for every namespace. // +kubebuilder:validation:MaxLength=63 // +kubebuilder:validation:XValidation:rule="self != '{consumer.name}'",message="'{consumer.name}' is refused in a namespace scope: the consumer picks its own name, so any namespace could be reached by naming an object after it; use '{consumer.namespace}', namespace names, or '*'" // +kubebuilder:validation:XValidation:rule="self == '*' || self == '{consumer.namespace}' || self == '{consumer.name}' || self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')",message="a namespace scope holds a namespace name, '{consumer.namespace}' alone, or '*' alone" type ScopeNamespace string -// ClusterResourceIdentity is a cluster-scoped resource type: resource.group, a bare resource for -// the core group, or "*" for every type. +// ClusterResourceIdentity is a cluster-scoped resource type: resource.group, a +// bare resource for the core group, or "*" for every type. // +kubebuilder:validation:MaxLength=317 // +kubebuilder:validation:Pattern=`^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$` type ClusterResourceIdentity string @@ -68,9 +70,10 @@ type ClusterResourceIdentity string // OpenAPI pattern: git check-ref-format refuses all of these in a branch name, so a pattern holding // one could never match, and with no '?' or '[' nothing in a pattern ever needs escaping. -// BranchPattern matches a whole branch name: literal text, "*" for any characters including "/", -// and "{consumer.namespace}" and "{consumer.name}" as whole path segments. "{consumer.name}" -// needs "{consumer.namespace}" in the same pattern. +// BranchPattern matches a whole branch name: literal text, "*" for any +// characters including "/", and "{consumer.namespace}" and "{consumer.name}" +// as whole path segments. "{consumer.name}" needs "{consumer.namespace}" in +// the same pattern. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=255 // +kubebuilder:validation:Pattern=`^[^\s~^:?\[\\]*$` @@ -85,17 +88,17 @@ type BranchPattern string // sit on the type, and an inline-embedded struct's type-level rules do not carry over to the struct // embedding it. -// ConsumerNamespaces says which control-cluster namespaces a grant admits consumers from, for the act -// the list holding the grant names. +// ConsumerNamespaces says which control-cluster namespaces a grant admits +// consumers from, for the act the list holding the grant names. // +kubebuilder:validation:XValidation:rule="self.from != 'Names' || has(self.names)",message="from: Names needs names" // +kubebuilder:validation:XValidation:rule="!has(self.names) || self.from == 'Names'",message="names goes only with from: Names" // +kubebuilder:validation:XValidation:rule="self.from != 'Selector' || has(self.selector)",message="from: Selector needs a selector; to admit every namespace, use from: All" // +kubebuilder:validation:XValidation:rule="!has(self.selector) || self.from == 'Selector'",message="selector goes only with from: Selector" // +kubebuilder:validation:XValidation:rule="!has(self.selector) || (has(self.selector.matchLabels) && size(self.selector.matchLabels) > 0) || (has(self.selector.matchExpressions) && size(self.selector.matchExpressions) > 0)",message="selector: {} is refused; to admit every namespace, use from: All" type ConsumerNamespaces struct { - // From is where this grant admits consumers from: "Same" (the namespace of the object that - // holds the grant), "Names" (the namespaces in names), "Selector" (the namespaces whose labels - // match selector) or "All" (every namespace). + // From is where this grant admits consumers from: "Same" (the namespace of the + // object that holds the grant), "Names" (the namespaces in names), "Selector" + // (the namespaces whose labels match selector) or "All" (every namespace). // +required From GrantFrom `json:"from"` @@ -106,26 +109,28 @@ type ConsumerNamespaces struct { // +kubebuilder:validation:MaxItems=64 Names []GrantNamespace `json:"names,omitempty"` - // Selector matches the labels of the namespaces admitted with from: "Selector". It must not be - // empty: to admit every namespace, use from: "All". + // Selector matches the labels of the namespaces admitted with from: + // "Selector". It must not be empty: to admit every namespace, use from: "All". // +optional Selector *metav1.LabelSelector `json:"selector,omitempty"` } -// GitProviderGitBranchGrant admits the GitBranches of some namespaces to claim, through a -// GitProvider, the branches its patterns match. +// GitProviderGitBranchGrant admits the GitBranches of some namespaces to +// claim, through a GitProvider, the branches its patterns match. type GitProviderGitBranchGrant struct { - // ConsumerNamespaces are the namespaces whose GitBranches this grant admits. from: "Same" is - // this GitProvider's namespace. + // ConsumerNamespaces are the namespaces whose GitBranches this grant admits. + // from: "Same" is this GitProvider's namespace. // +required ConsumerNamespaces ConsumerNamespaces `json:"consumerNamespaces"` - // BranchPatterns are the branches an admitted GitBranch may claim, each matched against the - // whole branch name. "*" matches any characters, "/" included, so "*" alone is every branch and - // "main" is only main. "{consumer.namespace}" and "{consumer.name}" are the GitBranch's - // namespace and name, each a whole path segment: "teams/{consumer.namespace}/*". - // "{consumer.name}" needs "{consumer.namespace}" in the same pattern, because a GitBranch picks - // its own name. There is no escaping, "?" or character class. + // BranchPatterns are the branches an admitted GitBranch may claim, each + // matched against the whole branch name. "*" matches any characters, "/" + // included, so "*" alone is every branch and "main" is only main. + // "{consumer.namespace}" and "{consumer.name}" are the GitBranch's namespace + // and name, each a whole path segment: "teams/{consumer.namespace}/*". + // "{consumer.name}" needs "{consumer.namespace}" in the same pattern, because + // a GitBranch picks its own name. There is no escaping, "?" or character + // class. // +required // +listType=set // +kubebuilder:validation:MinItems=1 @@ -133,47 +138,53 @@ type GitProviderGitBranchGrant struct { BranchPatterns []BranchPattern `json:"branchPatterns"` } -// GitBranchGitTargetGrant admits the GitTargets of some namespaces to write through a GitBranch. +// GitBranchGitTargetGrant admits the GitTargets of some namespaces to write +// through a GitBranch. type GitBranchGitTargetGrant struct { - // ConsumerNamespaces are the namespaces whose GitTargets this grant admits. from: "Same" is - // this GitBranch's namespace. + // ConsumerNamespaces are the namespaces whose GitTargets this grant admits. + // from: "Same" is this GitBranch's namespace. // +required ConsumerNamespaces ConsumerNamespaces `json:"consumerNamespaces"` } -// KubeProviderGitTargetGrant admits the GitTargets of some namespaces to mirror through a -// KubeProvider, and bounds what they may read. It sets sourceNamespaces, clusterResources, or both. +// KubeProviderGitTargetGrant admits the GitTargets of some namespaces to +// mirror through a KubeProvider, and bounds what they may read. It sets +// sourceNamespaces, clusterResources, or both. // +kubebuilder:validation:XValidation:rule="has(self.sourceNamespaces) || has(self.clusterResources)",message="a grant in allowedGitTargets sets sourceNamespaces, clusterResources, or both; one that grants neither grants nothing" type KubeProviderGitTargetGrant struct { - // ConsumerNamespaces are the namespaces whose GitTargets this grant admits. from: "Same" is - // this KubeProvider's namespace. + // ConsumerNamespaces are the namespaces whose GitTargets this grant admits. + // from: "Same" is this KubeProvider's namespace. // +required ConsumerNamespaces ConsumerNamespaces `json:"consumerNamespaces"` - // SourceNamespaces are the source-cluster namespaces that the WatchRules of an admitted - // GitTarget may read, in rules[].sourceNamespace: exact names, "{consumer.namespace}" for the - // namespace named like the GitTarget's, or "*". Omitted, this grant gives none. + // SourceNamespaces are the source-cluster namespaces that the WatchRules of an + // admitted GitTarget may read, in rules[].sourceNamespace: exact names, + // "{consumer.namespace}" for the namespace named like the GitTarget's, or "*". + // Omitted, this grant gives none. // - // The scope is the union over every grant admitting the GitTarget. It permits and selects - // nothing: a namespace is read only when a WatchRule names it. A rule with sourceNamespace "*" - // needs "*" in a grant, and is refused, not narrowed, without it. A WatchRule with any rule - // outside the scope is refused as a whole (SourceNamespaceNotAllowed) when it reconciles or a - // grant changes. The credential's RBAC still applies inside the scope. + // The scope is the union over every grant admitting the GitTarget. It permits + // and selects nothing: a namespace is read only when a WatchRule names it. A + // rule with sourceNamespace "*" needs "*" in a grant, and is refused, not + // narrowed, without it. A WatchRule with any rule outside the scope is refused + // as a whole (SourceNamespaceNotAllowed) when it reconciles or a grant + // changes. The credential's RBAC still applies inside the scope. // +optional // +listType=set // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=64 SourceNamespaces []ScopeNamespace `json:"sourceNamespaces,omitempty"` - // ClusterResources are the cluster-scoped types that the ClusterWatchRules of an admitted - // GitTarget may mirror: resource.group ("clusterroles.rbac.authorization.k8s.io"), a bare - // resource for the core group ("nodes"), or "*" for every type, including CRDs installed - // later. Each covers every served version. Omitted, this grant gives none. + // ClusterResources are the cluster-scoped types that the ClusterWatchRules of + // an admitted GitTarget may mirror: resource.group + // ("clusterroles.rbac.authorization.k8s.io"), a bare resource for the core + // group ("nodes"), or "*" for every type, including CRDs installed later. Each + // covers every served version. Omitted, this grant gives none. // - // The scope is the union over every grant admitting the GitTarget, and mirrors nothing until a - // ClusterWatchRule asks for a type. A rule with "*" in apiGroups or resources needs "*" in a - // grant, and is refused, not narrowed, without it. A ClusterWatchRule asking for any type - // outside the scope is refused as a whole (ClusterResourceNotAllowed) when it reconciles. + // The scope is the union over every grant admitting the GitTarget, and mirrors + // nothing until a ClusterWatchRule asks for a type. A rule with "*" in + // apiGroups or resources needs "*" in a grant, and is refused, not narrowed, + // without it. A ClusterWatchRule asking for any type outside the scope is + // refused as a whole (ClusterResourceNotAllowed) when it reconciles. // +optional // +listType=set // +kubebuilder:validation:MinItems=1 @@ -181,20 +192,22 @@ type KubeProviderGitTargetGrant struct { ClusterResources []ClusterResourceIdentity `json:"clusterResources,omitempty"` } -// KubeProviderApplierClaimGrant admits the FluxClaims and ArgoCDClaims of some namespaces to hold -// an applier through a KubeProvider, and bounds the namespaces that applier may be in. +// KubeProviderApplierClaimGrant admits the FluxClaims and ArgoCDClaims of some +// namespaces to hold an applier through a KubeProvider, and bounds the +// namespaces that applier may be in. type KubeProviderApplierClaimGrant struct { - // ConsumerNamespaces are the namespaces whose FluxClaims and ArgoCDClaims this grant admits. - // from: "Same" is this KubeProvider's namespace. + // ConsumerNamespaces are the namespaces whose FluxClaims and ArgoCDClaims this + // grant admits. from: "Same" is this KubeProvider's namespace. // +required ConsumerNamespaces ConsumerNamespaces `json:"consumerNamespaces"` - // ApplierNamespaces are the namespaces, in this KubeProvider's cluster, where the Kustomization - // or Application of an admitted applier claim may be: exact names, "{consumer.namespace}" for - // the namespace named like the claim's, or "*" for every namespace. The scope is the union over - // every grant admitting the claim, checked when the claim reconciles and when a grant changes. - // An applier outside it is refused (ApplierNamespaceNotAllowed), and a claim holding it hands - // it back. + // ApplierNamespaces are the namespaces, in this KubeProvider's cluster, where + // the Kustomization or Application of an admitted applier claim may be: exact + // names, "{consumer.namespace}" for the namespace named like the claim's, or + // "*" for every namespace. The scope is the union over every grant admitting + // the claim, checked when the claim reconciles and when a grant changes. An + // applier outside it is refused (ApplierNamespaceNotAllowed), and a claim + // holding it hands it back. // +required // +listType=set // +kubebuilder:validation:MinItems=1 @@ -428,15 +441,17 @@ func ClusterResourceIdentityFor(group, resource string) string { return resource + "." + group } -// GitBranchAdmission is what a GitProvider's allowedGitBranches give a GitBranch. +// GitBranchAdmission is what a GitProvider's allowedGitBranches give a +// GitBranch. // +kubebuilder:object:generate=false type GitBranchAdmission struct { // Admitted reports whether some grant admits the GitBranch's namespace. Admitted bool - // BranchPatterns are the patterns the admitting grants give the GitBranch, consumer variables - // expanded. + // BranchPatterns are the patterns the admitting grants give the GitBranch, + // consumer variables expanded. BranchPatterns []string - // Allowed reports whether the namespace is admitted and the branch matches one of BranchPatterns. + // Allowed reports whether the namespace is admitted and the branch matches one + // of BranchPatterns. Allowed bool } @@ -481,17 +496,17 @@ func (b *GitBranch) DescribeAllowedGitTargets() string { return describeAdmitted(consumersOf(b.gitTargetGrants()), b.Namespace) } -// KubeProviderGitTargetScope is what a KubeProvider's allowedGitTargets give a GitTarget: whether it may mirror at -// all, and what. +// KubeProviderGitTargetScope is what a KubeProvider's allowedGitTargets give a +// GitTarget: whether it may mirror at all, and what. // +kubebuilder:object:generate=false type KubeProviderGitTargetScope struct { // Admitted reports whether some grant admits the GitTarget's namespace. Admitted bool - // SourceNamespaces is the union of the admitting grants' sourceNamespaces, consumer variables - // expanded. "*" is every namespace. + // SourceNamespaces is the union of the admitting grants' sourceNamespaces, + // consumer variables expanded. "*" is every namespace. SourceNamespaces []string - // ClusterResources is the union of the admitting grants' clusterResources. "*" is every - // cluster-scoped resource. + // ClusterResources is the union of the admitting grants' clusterResources. "*" + // is every cluster-scoped resource. ClusterResources []string } @@ -549,13 +564,14 @@ func (p *KubeProvider) DescribeAllowedGitTargets() string { return describeAdmitted(consumersOf(p.gitTargetGrants()), p.Namespace) } -// KubeProviderApplierClaimScope is what a KubeProvider's allowedApplierClaims give an applier claim. +// KubeProviderApplierClaimScope is what a KubeProvider's allowedApplierClaims +// give an applier claim. // +kubebuilder:object:generate=false type KubeProviderApplierClaimScope struct { // Admitted reports whether some grant admits the applier claim's namespace. Admitted bool - // ApplierNamespaces is the union of the admitting grants' applierNamespaces, consumer variables - // expanded. "*" is every namespace. + // ApplierNamespaces is the union of the admitting grants' applierNamespaces, + // consumer variables expanded. "*" is every namespace. ApplierNamespaces []string } diff --git a/api/v1alpha3/applierclaim_types.go b/api/v1alpha3/applierclaim_types.go index c2ef0b6b..5b678f17 100644 --- a/api/v1alpha3/applierclaim_types.go +++ b/api/v1alpha3/applierclaim_types.go @@ -36,7 +36,8 @@ const ( // KubeProvider admits the namespace it is in. A GitTarget that could name an // applier claim elsewhere would drive an applier that its own namespace was never admitted to. -// ApplierClaimReference names an applier claim in the GitTarget's own namespace. +// ApplierClaimReference names an applier claim in the GitTarget's own +// namespace. type ApplierClaimReference struct { // Kind is the applier claim's kind: "FluxClaim" or "ArgoCDClaim". // +required @@ -67,8 +68,8 @@ const ( ApplyReasonCatchUp ApplyReason = "CatchUp" ) -// SuspendState is whether an applier applied on its own. An Argo CD Application with automated sync -// off counts as Suspended. +// SuspendState is whether an applier applied on its own. An Argo CD +// Application with automated sync off counts as Suspended. // +kubebuilder:validation:Enum=Suspended;NotSuspended type SuspendState string @@ -108,42 +109,47 @@ func (p ApplyOperationPhase) Completed() bool { return p == ApplyOperationSucceeded || p == ApplyOperationFailed } -// ApplyOperation is an apply GitOps Reverser started on the applier claim's applier: the one in flight, -// or the last one, once it completed. The branch worker writes it on every transition. +// ApplyOperation is an apply GitOps Reverser started on the applier claim's +// applier: the one in flight, or the last one, once it completed. The branch +// worker writes it on every transition. type ApplyOperation struct { - // Phase is where the operation stands. Requested, Running, Settling (Flux only: applied, and - // the Kustomization not yet suspended again) and Unresolved (overdue, still followed) are in - // flight: the applier can still revert an edit to an object it applies, and is asked for - // nothing else. Succeeded and Failed are complete; Failed may have applied part of the commit. + // Phase is where the operation stands. Requested, Running, Settling (Flux + // only: applied, and the Kustomization not yet suspended again) and Unresolved + // (overdue, still followed) are in flight: the applier can still revert an + // edit to an object it applies, and is asked for nothing else. Succeeded and + // Failed are complete; Failed may have applied part of the commit. // +required Phase ApplyOperationPhase `json:"phase"` - // Reason is why the apply was asked for: ForeignCommit (a commit GitOps Reverser did not push), - // Refusal (a refused edit under spec.onRefusal: "Reapply"), Repair (an earlier apply reverted - // a newer API edit), or CatchUp (adopting objects the API created, and once at startup or when - // a GitTarget first names the claim). With several GitTargets on the claim, it is the strongest - // of their reasons. + // Reason is why the apply was asked for: ForeignCommit (a commit GitOps + // Reverser did not push), Refusal (a refused edit under spec.onRefusal: + // "Reapply"), Repair (an earlier apply reverted a newer API edit), or CatchUp + // (adopting objects the API created, and once at startup or when a GitTarget + // first names the claim). With several GitTargets on the claim, it is the + // strongest of their reasons. // +optional Reason ApplyReason `json:"reason,omitempty"` - // Commit is the commit the applier was asked to apply. Empty when the remote did not carry the - // branch. + // Commit is the commit the applier was asked to apply. Empty when the remote + // did not carry the branch. // +optional Commit string `json:"commit,omitempty"` - // Token identifies the request in the applier's own status: the reconcile.fluxcd.io/requestedAt - // value on a Flux object, or the "gitops-reverser request" entry in an Argo CD operation's info. + // Token identifies the request in the applier's own status: the + // reconcile.fluxcd.io/requestedAt value on a Flux object, or the + // "gitops-reverser request" entry in an Argo CD operation's info. // +optional Token string `json:"token,omitempty"` - // SuspendToken is the reconcile.fluxcd.io/requestedAt value of the suspend that follows the - // apply. Once the Kustomization reports it handled, nothing started before the suspend is still - // running. Flux only. + // SuspendToken is the reconcile.fluxcd.io/requestedAt value of the suspend + // that follows the apply. Once the Kustomization reports it handled, nothing + // started before the suspend is still running. Flux only. // +optional SuspendToken string `json:"suspendToken,omitempty"` - // AppliedRevision is the revision the applier reports applying when the operation completes. - // It can differ from commit when the branch moved between the check and the apply. + // AppliedRevision is the revision the applier reports applying when the + // operation completes. It can differ from commit when the branch moved between + // the check and the apply. // +optional AppliedRevision string `json:"appliedRevision,omitempty"` @@ -155,13 +161,15 @@ type ApplyOperation struct { // +optional FinishedAt *metav1.Time `json:"finishedAt,omitempty"` - // Message says why a Failed operation failed, or why an Unresolved one is overdue. + // Message says why a Failed operation failed, or why an Unresolved one is + // overdue. // +optional Message string `json:"message,omitempty"` } -// ApplierClaim is what every applier claim kind answers, so the controllers, the branch workers and the -// dispatcher handle a FluxClaim and an ArgoCDClaim alike. +// ApplierClaim is what every applier claim kind answers, so the controllers, +// the branch workers and the dispatcher handle a FluxClaim and an ArgoCDClaim +// alike. // +kubebuilder:object:generate=false type ApplierClaim interface { metav1.Object diff --git a/api/v1alpha3/argocdclaim_types.go b/api/v1alpha3/argocdclaim_types.go index 0bb92b12..23df7ff9 100644 --- a/api/v1alpha3/argocdclaim_types.go +++ b/api/v1alpha3/argocdclaim_types.go @@ -18,53 +18,61 @@ import ( // applies) has none. const ArgoCDClaimFinalizer = "configbutler.ai/argocd-claim" -// ArgoCDClaimSpec names the Argo CD Application the applier claim holds, and the cluster it is in. +// ArgoCDClaimSpec names the Argo CD Application the applier claim holds, and +// the cluster it is in. type ArgoCDClaimSpec struct { // Immutable for the reason FluxClaimSpec's references are: the applier claim holds what it names. - // KubeProviderRef names the KubeProvider of the cluster that runs the Application, such as a - // management cluster running Argo CD. An omitted namespace means this ArgoCDClaim's namespace. - // The KubeProvider's spec.allowedApplierClaims must admit this namespace, with the - // Application's namespace in applierNamespaces, and its identity needs get, update and patch on - // the Application. Immutable: delete and recreate the ArgoCDClaim to change it. + // KubeProviderRef names the KubeProvider of the cluster that runs the + // Application, such as a management cluster running Argo CD. An omitted + // namespace means this ArgoCDClaim's namespace. The KubeProvider's + // spec.allowedApplierClaims must admit this namespace, with the Application's + // namespace in applierNamespaces, and its identity needs get, update and patch + // on the Application. Immutable: delete and recreate the ArgoCDClaim to change + // it. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.kubeProviderRef.name must not be empty" // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.kubeProviderRef is immutable; delete and recreate the ArgoCDClaim to hold an Application elsewhere" KubeProviderRef meta.NamespacedObjectReference `json:"kubeProviderRef"` - // ApplicationRef names the Argo CD Application that syncs the folders of the GitTargets naming - // this ArgoCDClaim. It must have a single spec.source whose targetRevision names a branch. + // ApplicationRef names the Argo CD Application that syncs the folders of the + // GitTargets naming this ArgoCDClaim. It must have a single spec.source whose + // targetRevision names a branch. // - // The ArgoCDClaim keeps its automated sync off by applying spec.syncPolicy.automated.enabled: - // false, switches it off again when somebody switches it on, and gives the switch up when it - // lets go; automated.prune still applies to the syncs GitOps Reverser starts. An Application - // that an ApplicationSet generates, or whose switch another field manager applies, must have - // automated sync off already. Each sync is pinned to the commit GitOps Reverser checked, and a - // running operation is never replaced. + // The ArgoCDClaim keeps its automated sync off by applying + // spec.syncPolicy.automated.enabled: false, switches it off again when + // somebody switches it on, and gives the switch up when it lets go; + // automated.prune still applies to the syncs GitOps Reverser starts. An + // Application that an ApplicationSet generates, or whose switch another field + // manager applies, must have automated sync off already. Each sync is pinned + // to the commit GitOps Reverser checked, and a running operation is never + // replaced. // - // An omitted namespace means this ArgoCDClaim's namespace. Immutable: delete and recreate the - // ArgoCDClaim to change it. + // An omitted namespace means this ArgoCDClaim's namespace. Immutable: delete + // and recreate the ArgoCDClaim to change it. // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.applicationRef is immutable; delete and recreate the ArgoCDClaim to hold another Application" // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.applicationRef.name must not be empty" ApplicationRef meta.NamespacedObjectReference `json:"applicationRef"` } -// ArgoCDClaimStatus is what the applier claim knows about its Application, and the sync it started. +// ArgoCDClaimStatus is what the applier claim knows about its Application, and +// the sync it started. type ArgoCDClaimStatus struct { // ObservedGeneration is the latest generation observed by the controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this ArgoCDClaim's state. Ready, Reconciling and - // Stalled follow kstatus. + // Conditions are the latest observations of this ArgoCDClaim's state. Ready, + // Reconciling and Stalled follow kstatus. // - // Configured is True when an eligible GitTarget names this ArgoCDClaim, and the Application - // exists, syncs a single source, has automated sync off or a switch this ArgoCDClaim may hold, - // may be written, and no other ArgoCDClaim holds it. An eligible GitTarget is in this - // namespace, is admitted by its KubeProviders, and writes the branch the Application syncs. Of - // several ArgoCDClaims for one Application, the one holding it already holds it, then the - // oldest; the others are Configured=False, reason HeldByAnotherClaim. + // Configured is True when an eligible GitTarget names this ArgoCDClaim, and + // the Application exists, syncs a single source, has automated sync off or a + // switch this ArgoCDClaim may hold, may be written, and no other ArgoCDClaim + // holds it. An eligible GitTarget is in this namespace, is admitted by its + // KubeProviders, and writes the branch the Application syncs. Of several + // ArgoCDClaims for one Application, the one holding it already holds it, then + // the oldest; the others are Configured=False, reason HeldByAnotherClaim. // // InControl says whether GitOps Reverser decides when the Application // syncs: @@ -92,27 +100,28 @@ type ArgoCDClaimStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Source is the Application's spec.source, as last read. It is published while an eligible - // GitTarget names this ArgoCDClaim, and that GitTarget must write the repository it syncs, on - // the branch its targetRevision names. + // Source is the Application's spec.source, as last read. It is published while + // an eligible GitTarget names this ArgoCDClaim, and that GitTarget must write + // the repository it syncs, on the branch its targetRevision names. // +optional Source *ArgoCDClaimSource `json:"source,omitempty"` - // Operation is the sync GitOps Reverser started on the Application: the one in flight, or the - // last one. Absent until the first. + // Operation is the sync GitOps Reverser started on the Application: the one in + // flight, or the last one. Absent until the first. // +optional Operation *ApplyOperation `json:"operation,omitempty"` - // RepairPending is true while a Repair sync is pending: a sync ran a commit older than an edit - // made through the API and reverted it on the cluster. Git holds the edit, and the cluster runs - // the older value until the next sync. + // RepairPending is true while a Repair sync is pending: a sync ran a commit + // older than an edit made through the API and reverted it on the cluster. Git + // holds the edit, and the cluster runs the older value until the next sync. // +optional RepairPending bool `json:"repairPending,omitempty"` - // PreviousSuspendState is how the Application stood before this ArgoCDClaim first switched its - // automated sync off, and what handing it back restores: NotSuspended (automated sync on) or - // Suspended (off). It is cleared once the Application is handed back. Absent, handing it back - // leaves what the Application's own spec says. A switch that an ApplicationSet or another + // PreviousSuspendState is how the Application stood before this ArgoCDClaim + // first switched its automated sync off, and what handing it back restores: + // NotSuspended (automated sync on) or Suspended (off). It is cleared once the + // Application is handed back. Absent, handing it back leaves what the + // Application's own spec says. A switch that an ApplicationSet or another // field manager owns keeps that owner's value either way. // +optional PreviousSuspendState SuspendState `json:"previousSuspendState,omitempty"` @@ -124,8 +133,8 @@ type ArgoCDClaimSource struct { // +optional RepoURL string `json:"repoURL,omitempty"` - // TargetRevision is the Application's spec.source.targetRevision, as written: a branch name, - // or refs/heads/ and the name. Empty means Argo CD's HEAD. + // TargetRevision is the Application's spec.source.targetRevision, as written: + // a branch name, or refs/heads/ and the name. Empty means Argo CD's HEAD. // +optional TargetRevision string `json:"targetRevision,omitempty"` } @@ -144,11 +153,12 @@ type ArgoCDClaimSource struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// ArgoCDClaim hands control of an Argo CD Application to GitOps Reverser. The Application's -// automated sync stays off, and GitOps Reverser starts each sync, always after the edits pending -// on the branch are published: after a commit it did not push, a refused or reverted edit, or an -// object the API created, and once at startup or when a GitTarget first names it. GitTargets name -// it with spec.applierClaimRef. +// ArgoCDClaim hands control of an Argo CD Application to GitOps Reverser. The +// Application's automated sync stays off, and GitOps Reverser starts each +// sync, always after the edits pending on the branch are published: after a +// commit it did not push, a refused or reverted edit, or an object the API +// created, and once at startup or when a GitTarget first names it. GitTargets +// name it with spec.applierClaimRef. type ArgoCDClaim struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/clusterwatchrule_types.go b/api/v1alpha3/clusterwatchrule_types.go index dea942fa..5013dbf3 100644 --- a/api/v1alpha3/clusterwatchrule_types.go +++ b/api/v1alpha3/clusterwatchrule_types.go @@ -7,10 +7,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// ResourceScope names a Kubernetes resource's scope. It is a purely INTERNAL matching vocabulary -// with no field in any CRD: the resolver uses both constants to align a rule's selector with the -// discovered scope of each type. A WatchRule always resolves Namespaced records, a ClusterWatchRule -// always Cluster ones, and which kind you write is the whole of how scope is chosen. +// ResourceScope names a Kubernetes resource's scope. It is a purely INTERNAL +// matching vocabulary with no field in any CRD: the resolver uses both +// constants to align a rule's selector with the discovered scope of each type. +// A WatchRule always resolves Namespaced records, a ClusterWatchRule always +// Cluster ones, and which kind you write is the whole of how scope is chosen. type ResourceScope string const ( @@ -23,16 +24,17 @@ const ( // ClusterWatchRuleSpec defines the desired state of ClusterWatchRule. type ClusterWatchRuleSpec struct { - // GitTargetRef names the GitTarget that mirrors what this ClusterWatchRule selects. The - // namespace is required, because a ClusterWatchRule has none to default to. + // GitTargetRef names the GitTarget that mirrors what this ClusterWatchRule + // selects. The namespace is required, because a ClusterWatchRule has none to + // default to. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.gitTargetRef.name must not be empty" // +kubebuilder:validation:XValidation:rule="has(self.namespace) && self.namespace != ''",message="spec.gitTargetRef.namespace is required: a ClusterWatchRule has no namespace to default to" GitTargetRef meta.NamespacedObjectReference `json:"gitTargetRef"` - // Rules select the cluster-scoped resources to mirror. A resource matching any rule is - // mirrored. A rule that resolves to no cluster-scoped type mirrors nothing; use a WatchRule for - // namespaced resources. + // Rules select the cluster-scoped resources to mirror. A resource matching any + // rule is mirrored. A rule that resolves to no cluster-scoped type mirrors + // nothing; use a WatchRule for namespaced resources. // +required // +kubebuilder:validation:MinItems=1 Rules []ClusterResourceRule `json:"rules"` @@ -41,23 +43,25 @@ type ClusterWatchRuleSpec struct { // There is no sourceNamespace: cluster-scoped objects have no namespace, so there is nothing to // select. -// ClusterResourceRule selects cluster-scoped resources by type, and optionally by API group, -// version and label. +// ClusterResourceRule selects cluster-scoped resources by type, and optionally +// by API group, version and label. type ClusterResourceRule struct { - // APIGroups are the API groups to match: "" is the core group, and "*" is every group, as in - // ["rbac.authorization.k8s.io"] or ["*"]. Omitted, each resource name is looked up across the - // served groups and must be served by exactly one. + // APIGroups are the API groups to match: "" is the core group, and "*" is + // every group, as in ["rbac.authorization.k8s.io"] or ["*"]. Omitted, each + // resource name is looked up across the served groups and must be served by + // exactly one. // +optional APIGroups []string `json:"apiGroups,omitempty"` - // APIVersions are the versions to match, or "*" for every served version. Omitted, each - // resource's preferred version is used. + // APIVersions are the versions to match, or "*" for every served version. + // Omitted, each resource's preferred version is used. // +optional APIVersions []string `json:"apiVersions,omitempty"` - // Resources are the resource types to match, by plural name ("nodes", "clusterroles"), or "*" - // for every type. Matching is exact and case-insensitive; partial wildcards such as "cluster*" - // match nothing, and subresources ("nodes/status") are rejected. + // Resources are the resource types to match, by plural name ("nodes", + // "clusterroles"), or "*" for every type. Matching is exact and + // case-insensitive; partial wildcards such as "cluster*" match nothing, and + // subresources ("nodes/status") are rejected. // +required // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:items:MinLength=1 @@ -87,9 +91,9 @@ type ClusterWatchRuleStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this ClusterWatchRule's state. Ready, Reconciling - // and Stalled follow kstatus; ResourcesResolved and StreamsRunning say which check holds the - // rule back. + // Conditions are the latest observations of this ClusterWatchRule's state. + // Ready, Reconciling and Stalled follow kstatus; ResourcesResolved and + // StreamsRunning say which check holds the rule back. // +optional // +listType=map // +listMapKey=type @@ -118,9 +122,10 @@ type ClusterWatchRuleStatus struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// ClusterWatchRule selects cluster-scoped resources, such as Nodes, ClusterRoles and CRDs, for a -// GitTarget to mirror. The KubeProvider's grant admitting that GitTarget must list each type in -// clusterResources. Use a WatchRule for namespaced resources. +// ClusterWatchRule selects cluster-scoped resources, such as Nodes, +// ClusterRoles and CRDs, for a GitTarget to mirror. The KubeProvider's grant +// admitting that GitTarget must list each type in clusterResources. Use a +// WatchRule for namespaced resources. type ClusterWatchRule struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/commitrequest_types.go b/api/v1alpha3/commitrequest_types.go index b5ed5016..a67819c9 100644 --- a/api/v1alpha3/commitrequest_types.go +++ b/api/v1alpha3/commitrequest_types.go @@ -12,19 +12,21 @@ import ( // changes it, so a delayed audit event always acts on the spec the object was // created with. // -// Immutability is field-by-field rather than `self == oldSelf`, and the reason is the one thing to -// remember when adding a field here: ADD IT TO THIS RULE TOO, or it becomes quietly mutable. +// Immutability is field-by-field rather than `self == oldSelf`, and the reason +// is the one thing to remember when adding a field here: ADD IT TO THIS RULE +// TOO, or it becomes quietly mutable. // -// A whole-object comparison compares a duration as a STRING, and a duration does not round-trip as -// one: a request created with "1m" reads into Go as a time.Duration and serializes back as "1m0s", -// so every typed update — including one that only touches metadata — was rejected as a spec -// change. CommitRequestWindow compares its durations as durations, which compares what the fields -// mean instead of how they were spelled. +// A whole-object comparison compares a duration as a STRING, and a duration +// does not round-trip as one: a request created with "1m" reads into Go as a +// time.Duration and serializes back as "1m0s", so every typed update — +// including one that only touches metadata — was rejected as a spec change. +// CommitRequestWindow compares its durations as durations, which compares what +// the fields mean instead of how they were spelled. // +kubebuilder:validation:XValidation:rule="self.gitTargetRef == oldSelf.gitTargetRef && has(self.message) == has(oldSelf.message) && (!has(self.message) || self.message == oldSelf.message) && has(self.window) == has(oldSelf.window) && has(self.whenNothingToCommit) == has(oldSelf.whenNothingToCommit) && (!has(self.whenNothingToCommit) || self.whenNothingToCommit == oldSelf.whenNothingToCommit)",message="CommitRequest spec is immutable after creation" // +kubebuilder:validation:XValidation:rule="!has(self.whenNothingToCommit) || self.whenNothingToCommit != 'CommitEmpty' || has(self.message)",message="whenNothingToCommit: CommitEmpty requires spec.message, because a message is all an empty commit records" type CommitRequestSpec struct { - // GitTargetRef names the GitTarget, in this namespace, whose commit window this request - // attaches to. + // GitTargetRef names the GitTarget, in this namespace, whose commit window + // this request attaches to. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.gitTargetRef.name must not be empty" GitTargetRef meta.LocalObjectReference `json:"gitTargetRef"` @@ -49,27 +51,30 @@ type CommitRequestSpec struct { // The default is spelled out rather than {} because the API server checks a default against // the block's CEL rules without applying the nested field defaults first. - // Window says which commit window this request attaches to, how long it waits for one, and - // the timers that close the window once it has. They replace the GitTarget's timers for that - // window. A request never opens a window: only a write does. Omitted, the request attaches to - // the author's current or next window within 2s and closes it 2s later. + // Window says which commit window this request attaches to, how long it waits + // for one, and the timers that close the window once it has. They replace the + // GitTarget's timers for that window. A request never opens a window: only a + // write does. Omitted, the request attaches to the author's current or next + // window within 2s and closes it 2s later. // +optional // +kubebuilder:default={attach: "CurrentOrNext", attachTimeout: "2s", maxDuration: "2s"} Window *CommitRequestWindow `json:"window,omitempty"` - // WhenNothingToCommit decides what happens when the request ends with nothing to commit: - // attachTimeout ran out with no window, or the window closed without changing Git. "Resolve" - // finishes without a commit; "CommitEmpty" records spec.message in an empty commit. The Ready - // reason says which case happened. A window that belonged to another author never gets an - // empty commit, and a request on a GitTarget that may not be written (suspended, or render - // fidelity not established) fails either way. Defaults to "Resolve". + // WhenNothingToCommit decides what happens when the request ends with nothing + // to commit: attachTimeout ran out with no window, or the window closed + // without changing Git. "Resolve" finishes without a commit; "CommitEmpty" + // records spec.message in an empty commit. The Ready reason says which case + // happened. A window that belonged to another author never gets an empty + // commit, and a request on a GitTarget that may not be written (suspended, or + // render fidelity not established) fails either way. Defaults to "Resolve". // +optional // +kubebuilder:validation:Enum=Resolve;CommitEmpty // +kubebuilder:default=Resolve WhenNothingToCommit NothingToCommitAction `json:"whenNothingToCommit,omitempty"` } -// NothingToCommitAction is what a CommitRequest does when it ends with nothing to commit. +// NothingToCommitAction is what a CommitRequest does when it ends with nothing +// to commit. type NothingToCommitAction string const ( @@ -92,29 +97,32 @@ const ( AttachNext AttachPolicy = "Next" ) -// CommitRequestWindow is how a CommitRequest attaches to a commit window and when that window -// closes. A window is only ever opened by a write; a request waits for one and never opens one. +// CommitRequestWindow is how a CommitRequest attaches to a commit window and +// when that window closes. A window is only ever opened by a write; a request +// waits for one and never opens one. // -// Every duration shares the shape GitTargetCommitSpec documents: a Go duration string behind a -// pattern and a CEL bound. The immutability rule compares them as durations, for the reason -// CommitRequestSpec gives. +// Every duration shares the shape GitTargetCommitSpec documents: a Go duration +// string behind a pattern and a CEL bound. The immutability rule compares them +// as durations, for the reason CommitRequestSpec gives. // +kubebuilder:validation:XValidation:rule="self.attach == oldSelf.attach && duration(self.attachTimeout) == duration(oldSelf.attachTimeout) && duration(self.maxDuration) == duration(oldSelf.maxDuration) && has(self.idleTimeout) == has(oldSelf.idleTimeout) && (!has(self.idleTimeout) || duration(self.idleTimeout) == duration(oldSelf.idleTimeout))",message="CommitRequest spec is immutable after creation" // +kubebuilder:validation:XValidation:rule="!has(self.idleTimeout) || duration(self.idleTimeout) <= duration(self.maxDuration)",message="spec.window.idleTimeout must not exceed maxDuration" // +kubebuilder:validation:XValidation:rule="self.attach != 'Next' || duration(self.attachTimeout) > duration('0s')",message="attach: Next needs a positive attachTimeout; with 0s it would close the author's window and give up in the same instant" type CommitRequestWindow struct { - // Attach selects the window. "CurrentOrNext" attaches to the author's open window, or to the - // next one a write opens. "Next" closes the author's open window under its own message first, - // and attaches to the next one; it separates earlier writes from later ones, but cannot prove - // a write was made after the request. Defaults to "CurrentOrNext". + // Attach selects the window. "CurrentOrNext" attaches to the author's open + // window, or to the next one a write opens. "Next" closes the author's open + // window under its own message first, and attaches to the next one; it + // separates earlier writes from later ones, but cannot prove a write was made + // after the request. Defaults to "CurrentOrNext". // +optional // +kubebuilder:validation:Enum=CurrentOrNext;Next // +kubebuilder:default=CurrentOrNext Attach AttachPolicy `json:"attach,omitempty"` - // AttachTimeout is how long the request waits for a window, as a Go duration, from when GitOps - // Reverser picks the request up. "0s" attaches only to a window already open. A request whose - // wait ran out never takes a later window. At most "5m". Defaults to "2s", which covers a - // write's wait for its audit event. + // AttachTimeout is how long the request waits for a window, as a Go duration, + // from when GitOps Reverser picks the request up. "0s" attaches only to a + // window already open. A request whose wait ran out never takes a later + // window. At most "5m". Defaults to "2s", which covers a write's wait for its + // audit event. // +optional // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$" @@ -122,17 +130,18 @@ type CommitRequestWindow struct { // +kubebuilder:validation:XValidation:rule="duration(self) <= duration('5m')",message="spec.window.attachTimeout must not exceed 5m" AttachTimeout *metav1.Duration `json:"attachTimeout,omitempty"` - // IdleTimeout closes the attached window this long after its last write, as a Go duration. - // Omitted, only maxDuration closes it. At most "5m", and not more than maxDuration. + // IdleTimeout closes the attached window this long after its last write, as a + // Go duration. Omitted, only maxDuration closes it. At most "5m", and not more + // than maxDuration. // +optional // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$" // +kubebuilder:validation:XValidation:rule="duration(self) <= duration('5m')",message="spec.window.idleTimeout must not exceed 5m" IdleTimeout *metav1.Duration `json:"idleTimeout,omitempty"` - // MaxDuration closes the attached window this long after the request attached, however many - // writes keep arriving, as a Go duration. "0s" commits right after attaching. At most "5m". - // Defaults to "2s". + // MaxDuration closes the attached window this long after the request attached, + // however many writes keep arriving, as a Go duration. "0s" commits right + // after attaching. At most "5m". Defaults to "2s". // +optional // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$" @@ -141,32 +150,36 @@ type CommitRequestWindow struct { MaxDuration *metav1.Duration `json:"maxDuration,omitempty"` } -// CommitRequestStatus defines the observed state of CommitRequest. Progress and -// outcome are reported entirely through conditions (kstatus-compatible), so the -// object carries no lifecycle phase string: +// CommitRequestStatus defines the observed state of CommitRequest. Progress +// and outcome are reported entirely through conditions (kstatus-compatible), +// so the object carries no lifecycle phase string: // -// - Ready (summary): True once the request reached a terminal outcome that is not -// an error — a pushed commit, or a benign no-commit (nothing to save, already -// present, or a foreign open window). False while in progress or when it failed. -// - Reconciling / Stalled: the kstatus progress / blocked pair. Reconciling=True -// while finalizing; Stalled=True when the finalize failed and needs attention. +// - Ready (summary): True once the request reached a terminal outcome that +// is not an error — a pushed commit, or a benign no-commit (nothing to +// save, already present, or a foreign open window). False while in +// progress or when it failed. +// - Reconciling / Stalled: the kstatus progress / blocked pair. +// Reconciling=True while finalizing; Stalled=True when the finalize failed +// and needs attention. // - AuthorAttributed (domain): binary and settled immediately. True -// (AttributedFromAdmission) when the submitter captured at admission named the -// commit author; False (CommitterFallback) when capture ran but no admission record -// exists, or False (AuthorCaptureDisabled) when capture is disabled. In either False -// case the request claims no actor. False is not a failure and does not affect Ready; -// the final Git author remains the matching watch window's author. +// (AttributedFromAdmission) when the submitter captured at admission named +// the commit author; False (CommitterFallback) when capture ran but no +// admission record exists, or False (AuthorCaptureDisabled) when capture +// is disabled. In either False case the request claims no actor. False is +// not a failure and does not affect Ready; the final Git author remains +// the matching watch window's author. // - Pushed (domain): True once the commit is in the remote repository. type CommitRequestStatus struct { // ObservedGeneration is the latest generation observed by the controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this CommitRequest's state. Ready, Reconciling - // and Stalled follow kstatus: Ready is True once the request ended without error, with a - // pushed commit or with nothing to save. Pushed is True once the commit is on the remote. - // AuthorAttributed says whether the request's creator, recorded at admission, names the - // commit author; False is not a failure and does not affect Ready. + // Conditions are the latest observations of this CommitRequest's state. Ready, + // Reconciling and Stalled follow kstatus: Ready is True once the request ended + // without error, with a pushed commit or with nothing to save. Pushed is True + // once the commit is on the remote. AuthorAttributed says whether the + // request's creator, recorded at admission, names the commit author; False is + // not a failure and does not affect Ready. // +optional // +listType=map // +listMapKey=type @@ -195,9 +208,10 @@ type CommitRequestStatus struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// CommitRequest asks for a save: it attaches to its author's current or next commit window on a -// GitTarget, closes that window on its own timers, and gives the commit its message. Its spec is -// immutable, and status.commit reports the commit once it is on the remote. +// CommitRequest asks for a save: it attaches to its author's current or next +// commit window on a GitTarget, closes that window on its own timers, and +// gives the commit its message. Its spec is immutable, and status.commit +// reports the commit once it is on the remote. type CommitRequest struct { metav1.TypeMeta `json:",inline"` @@ -205,7 +219,8 @@ type CommitRequest struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the desired state of CommitRequest. It is immutable after creation. + // spec defines the desired state of CommitRequest. It is immutable after + // creation. // +required Spec CommitRequestSpec `json:"spec"` diff --git a/api/v1alpha3/fluxclaim_types.go b/api/v1alpha3/fluxclaim_types.go index 73eee5dd..f6211dcb 100644 --- a/api/v1alpha3/fluxclaim_types.go +++ b/api/v1alpha3/fluxclaim_types.go @@ -17,47 +17,53 @@ import ( // Kustomization has none. const FluxClaimFinalizer = "configbutler.ai/flux-claim" -// FluxClaimSpec names the Flux Kustomization the applier claim holds, and the cluster it is in. +// FluxClaimSpec names the Flux Kustomization the applier claim holds, and the +// cluster it is in. type FluxClaimSpec struct { // Why immutable: the applier claim holds the Kustomization it names (it suspends it while idle and // hands it back when deleted), so pointing it elsewhere would leave the first one held by // nothing. Delete and recreate the applier claim instead; its finalizer hands the old one back. - // KubeProviderRef names the KubeProvider of the cluster that runs the Kustomization, such as a - // management cluster running Flux. An omitted namespace means this FluxClaim's namespace. The - // KubeProvider's spec.allowedApplierClaims must admit this namespace, with the Kustomization's - // namespace in applierNamespaces, and its identity needs get and patch on the Kustomization and - // its GitRepository. Immutable: delete and recreate the FluxClaim to change it. + // KubeProviderRef names the KubeProvider of the cluster that runs the + // Kustomization, such as a management cluster running Flux. An omitted + // namespace means this FluxClaim's namespace. The KubeProvider's + // spec.allowedApplierClaims must admit this namespace, with the + // Kustomization's namespace in applierNamespaces, and its identity needs get + // and patch on the Kustomization and its GitRepository. Immutable: delete and + // recreate the FluxClaim to change it. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.kubeProviderRef.name must not be empty" // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.kubeProviderRef is immutable; delete and recreate the FluxClaim to hold a Kustomization elsewhere" KubeProviderRef meta.NamespacedObjectReference `json:"kubeProviderRef"` - // KustomizationRef names the Flux Kustomization that applies the folders of the GitTargets - // naming this FluxClaim. Its spec.sourceRef must be a GitRepository that follows a branch. An - // omitted namespace means this FluxClaim's namespace. Immutable: delete and recreate the - // FluxClaim to change it. + // KustomizationRef names the Flux Kustomization that applies the folders of + // the GitTargets naming this FluxClaim. Its spec.sourceRef must be a + // GitRepository that follows a branch. An omitted namespace means this + // FluxClaim's namespace. Immutable: delete and recreate the FluxClaim to + // change it. // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.kustomizationRef is immutable; delete and recreate the FluxClaim to hold another Kustomization" // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.kustomizationRef.name must not be empty" KustomizationRef meta.NamespacedObjectReference `json:"kustomizationRef"` } -// FluxClaimStatus is what the applier claim knows about its Kustomization, and the apply it started. +// FluxClaimStatus is what the applier claim knows about its Kustomization, and +// the apply it started. type FluxClaimStatus struct { // ObservedGeneration is the latest generation observed by the controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this FluxClaim's state. Ready, Reconciling and - // Stalled follow kstatus. + // Conditions are the latest observations of this FluxClaim's state. Ready, + // Reconciling and Stalled follow kstatus. // - // Configured is True when an eligible GitTarget names this FluxClaim, and the Kustomization - // exists, applies a GitRepository that follows a branch, may be patched, and no other FluxClaim - // holds it. An eligible GitTarget is in this namespace, is admitted by its KubeProviders, and - // writes the branch the GitRepository fetches. Of several FluxClaims for one Kustomization, the - // one with an apply in flight holds it, then the one holding it already, then the oldest; the - // others are Configured=False, reason HeldByAnotherClaim. + // Configured is True when an eligible GitTarget names this FluxClaim, and the + // Kustomization exists, applies a GitRepository that follows a branch, may be + // patched, and no other FluxClaim holds it. An eligible GitTarget is in this + // namespace, is admitted by its KubeProviders, and writes the branch the + // GitRepository fetches. Of several FluxClaims for one Kustomization, the one + // with an apply in flight holds it, then the one holding it already, then the + // oldest; the others are Configured=False, reason HeldByAnotherClaim. // // InControl says whether GitOps Reverser decides when the Kustomization // applies: @@ -82,34 +88,36 @@ type FluxClaimStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Source is the GitRepository the Kustomization applies, as last read. It is published while an - // eligible GitTarget names this FluxClaim, and that GitTarget must write the repository and - // branch it fetches. + // Source is the GitRepository the Kustomization applies, as last read. It is + // published while an eligible GitTarget names this FluxClaim, and that + // GitTarget must write the repository and branch it fetches. // +optional Source *FluxClaimSource `json:"source,omitempty"` - // Operation is the apply GitOps Reverser started on the Kustomization: the one in flight, or - // the last one. Absent until the first. + // Operation is the apply GitOps Reverser started on the Kustomization: the one + // in flight, or the last one. Absent until the first. // +optional Operation *ApplyOperation `json:"operation,omitempty"` - // RepairPending is true while a Repair apply is pending: an apply ran a commit older than an - // edit made through the API and reverted it on the cluster. Git holds the edit, and the cluster - // runs the older value until the next apply. + // RepairPending is true while a Repair apply is pending: an apply ran a commit + // older than an edit made through the API and reverted it on the cluster. Git + // holds the edit, and the cluster runs the older value until the next apply. // +optional RepairPending bool `json:"repairPending,omitempty"` - // PreviousSuspendState is how the Kustomization stood before this FluxClaim first suspended - // it, and what handing it back restores: one that was Suspended stays suspended, one that was - // NotSuspended is resumed. It is cleared once the Kustomization is handed back. Absent, - // handing it back resumes the Kustomization. + // PreviousSuspendState is how the Kustomization stood before this FluxClaim + // first suspended it, and what handing it back restores: one that was + // Suspended stays suspended, one that was NotSuspended is resumed. It is + // cleared once the Kustomization is handed back. Absent, handing it back + // resumes the Kustomization. // +optional PreviousSuspendState SuspendState `json:"previousSuspendState,omitempty"` } // FluxClaimSource is the GitRepository a Kustomization applies. type FluxClaimSource struct { - // GitRepository is the Kustomization's spec.sourceRef, with its namespace filled in. + // GitRepository is the Kustomization's spec.sourceRef, with its namespace + // filled in. // +required GitRepository meta.NamespacedObjectReference `json:"gitRepository"` @@ -125,9 +133,10 @@ type FluxClaimSource struct { // GitRepository for several Kustomizations is a normal setup, and an object no applier claim names // applies a folder that has no promise of protection anyway. - // AlsoAppliedBy lists the other objects that apply this GitRepository and that no FluxClaim - // holds: Kustomizations, and HelmReleases whose chart comes from it. Each applies a new commit - // as soon as Flux fetches it. Information only; it never affects Ready. + // AlsoAppliedBy lists the other objects that apply this GitRepository and that + // no FluxClaim holds: Kustomizations, and HelmReleases whose chart comes from + // it. Each applies a new commit as soon as Flux fetches it. Information only; + // it never affects Ready. // +optional // +listType=atomic AlsoAppliedBy []meta.NamespacedObjectKindReference `json:"alsoAppliedBy,omitempty"` @@ -147,11 +156,12 @@ type FluxClaimSource struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// FluxClaim hands control of a Flux Kustomization to GitOps Reverser. The Kustomization stays -// suspended, and GitOps Reverser resumes it for each apply it starts, always after the edits -// pending on the branch are published: after a commit it did not push, a refused or reverted -// edit, or an object the API created, and once at startup or when a GitTarget first names it. -// GitTargets name it with spec.applierClaimRef. +// FluxClaim hands control of a Flux Kustomization to GitOps Reverser. The +// Kustomization stays suspended, and GitOps Reverser resumes it for each apply +// it starts, always after the edits pending on the branch are published: after +// a commit it did not push, a refused or reverted edit, or an object the API +// created, and once at startup or when a GitTarget first names it. GitTargets +// name it with spec.applierClaimRef. type FluxClaim struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/gitbranch_types.go b/api/v1alpha3/gitbranch_types.go index ee380666..937c7a66 100644 --- a/api/v1alpha3/gitbranch_types.go +++ b/api/v1alpha3/gitbranch_types.go @@ -10,29 +10,32 @@ import ( // GitBranchSpec defines the desired state of GitBranch. // -// gitProviderRef, branch and parentBranch are immutable: together they are the repository branch -// this object claims, and a claim that could be repointed would move a branch worker's checkout, -// credential and pending writes to another branch under the GitTargets that reference it. Delete -// and recreate instead. allowedGitTargets is the branch owner's policy and stays mutable. +// gitProviderRef, branch and parentBranch are immutable: together they are the +// repository branch this object claims, and a claim that could be repointed +// would move a branch worker's checkout, credential and pending writes to +// another branch under the GitTargets that reference it. Delete and recreate +// instead. allowedGitTargets is the branch owner's policy and stays mutable. // -// CEL cannot read metadata.namespace, so the reference is compared as written: spelling out the -// GitBranch's own namespace where it was omitted is refused too. +// CEL cannot read metadata.namespace, so the reference is compared as written: +// spelling out the GitBranch's own namespace where it was omitted is refused +// too. // +kubebuilder:validation:XValidation:rule="self.gitProviderRef == oldSelf.gitProviderRef",message="spec.gitProviderRef is immutable; delete and recreate the GitBranch to change it" // +kubebuilder:validation:XValidation:rule="self.branch == oldSelf.branch",message="spec.branch is immutable; delete and recreate the GitBranch to change it" // +kubebuilder:validation:XValidation:rule="has(self.parentBranch) == has(oldSelf.parentBranch) && (!has(self.parentBranch) || self.parentBranch == oldSelf.parentBranch)",message="spec.parentBranch is immutable; delete and recreate the GitBranch to change it" type GitBranchSpec struct { - // GitProviderRef names the GitProvider whose repository and credential write this branch. An - // omitted namespace means this GitBranch's namespace. The GitProvider's spec.allowedGitBranches - // must admit this namespace and spec.branch. Immutable: delete and recreate the GitBranch to - // change it. + // GitProviderRef names the GitProvider whose repository and credential write + // this branch. An omitted namespace means this GitBranch's namespace. The + // GitProvider's spec.allowedGitBranches must admit this namespace and + // spec.branch. Immutable: delete and recreate the GitBranch to change it. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.gitProviderRef.name must not be empty" GitProviderRef meta.NamespacedObjectReference `json:"gitProviderRef"` - // Branch is the branch on the remote that this GitBranch writes. It must match a branch pattern - // that the GitProvider's spec.allowedGitBranches give this namespace. Only one GitBranch holds - // a repository branch: the oldest admitted one whose credential can open a push session. Any - // other reports Stalled, reason BranchConflict. Immutable: delete and recreate the GitBranch to + // Branch is the branch on the remote that this GitBranch writes. It must match + // a branch pattern that the GitProvider's spec.allowedGitBranches give this + // namespace. Only one GitBranch holds a repository branch: the oldest admitted + // one whose credential can open a push session. Any other reports Stalled, + // reason BranchConflict. Immutable: delete and recreate the GitBranch to // change it. // +required // +kubebuilder:validation:MinLength=1 @@ -43,23 +46,24 @@ type GitBranchSpec struct { // parent changed under a worker holding writes is the one case that needed its own replay, so it // is immutable. It can never hold a commit or tag: a pinned starting point would be a sibling. - // ParentBranch is the branch spec.branch is created from, and compared against while - // spec.branch does not exist on the remote. Omitted, it is the remote's default branch, or no - // history at all in an empty repository. When set, it must exist on the remote for spec.branch - // to be created; otherwise this GitBranch reports ParentBranchNotFound and writes nothing. Once - // spec.branch exists, the parent branch is no longer followed. It is only read, so - // spec.allowedGitBranches need not allow it. Immutable, including adding or removing it: - // delete and recreate the GitBranch to change it. + // ParentBranch is the branch spec.branch is created from, and compared against + // while spec.branch does not exist on the remote. Omitted, it is the remote's + // default branch, or no history at all in an empty repository. When set, it + // must exist on the remote for spec.branch to be created; otherwise this + // GitBranch reports ParentBranchNotFound and writes nothing. Once spec.branch + // exists, the parent branch is no longer followed. It is only read, so + // spec.allowedGitBranches need not allow it. Immutable, including adding or + // removing it: delete and recreate the GitBranch to change it. // +optional // +kubebuilder:validation:MinLength=1 ParentBranch string `json:"parentBranch,omitempty"` // Mutable, unlike the fields above: claiming a branch includes the right to share it. - // AllowedGitTargets says which GitTargets may write through this GitBranch. Omitted, it is one - // grant for this namespace (consumerNamespaces: {from: Same}). A written list is the whole - // policy, so it admits this namespace only if a grant says so, and [] admits nobody. Grants - // combine by union. + // AllowedGitTargets says which GitTargets may write through this GitBranch. + // Omitted, it is one grant for this namespace (consumerNamespaces: {from: + // Same}). A written list is the whole policy, so it admits this namespace only + // if a grant says so, and [] admits nobody. Grants combine by union. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 @@ -73,8 +77,9 @@ type GitBranchStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this GitBranch's state: whether it holds its - // repository branch and can write it. Ready, Reconciling and Stalled follow kstatus. + // Conditions are the latest observations of this GitBranch's state: whether it + // holds its repository branch and can write it. Ready, Reconciling and Stalled + // follow kstatus. // +optional // +listType=map // +listMapKey=type @@ -82,40 +87,46 @@ type GitBranchStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Repository is the GitProvider's repository URL, normalized to host and path with no scheme, - // user or ".git" suffix. With spec.branch it is what only one GitBranch may hold, so two - // GitProviders that spell one repository differently still conflict. + // Repository is the GitProvider's repository URL, normalized to host and path + // with no scheme, user or ".git" suffix. With spec.branch it is what only one + // GitBranch may hold, so two GitProviders that spell one repository + // differently still conflict. // +optional Repository string `json:"repository,omitempty"` - // LastPushNoticeAt is when a GitReceiver last delivered a push for this branch. It records that - // the delivery arrived; status.remote says what the remote holds. Absent means no delivery has - // reached this branch: no GitReceiver is set up or reachable, the delivery names the repository - // by a URL other than the GitProvider's spec.url, or no GitTarget references this GitBranch. It - // advances at most once a minute. + // LastPushNoticeAt is when a GitReceiver last delivered a push for this + // branch. It records that the delivery arrived; status.remote says what the + // remote holds. Absent means no delivery has reached this branch: no + // GitReceiver is set up or reachable, the delivery names the repository by a + // URL other than the GitProvider's spec.url, or no GitTarget references this + // GitBranch. It advances at most once a minute. // +optional LastPushNoticeAt *metav1.Time `json:"lastPushNoticeAt,omitempty"` - // Remote is where this branch is on the Git remote, and when that was last checked by a push or - // a fetch. It is updated at most once a minute. Absent until the first check, and again after - // the GitProvider is recreated for another repository. + // Remote is where this branch is on the Git remote, and when that was last + // checked by a push or a fetch. It is updated at most once a minute. Absent + // until the first check, and again after the GitProvider is recreated for + // another repository. // +optional Remote *GitBranchRemoteStatus `json:"remote,omitempty"` - // GitTargetCount is how many GitTargets, in any namespace, reference this GitBranch. Each one - // reports its own folder's health in its own conditions. + // GitTargetCount is how many GitTargets, in any namespace, reference this + // GitBranch. Each one reports its own folder's health in its own conditions. // +optional GitTargetCount int32 `json:"gitTargetCount"` } -// GitBranchRemoteStatus is the answer to "where is my branch, and when did we last prove it". +// GitBranchRemoteStatus is the answer to "where is my branch, and when did we +// last prove it". // -// It is sampled when the GitBranch reconciles, from what its branch worker last proved about the -// branch, and written at most once a minute. The first observation, and a correction after the -// repository changed under the branch, are written at once. +// It is sampled when the GitBranch reconciles, from what its branch worker +// last proved about the branch, and written at most once a minute. The first +// observation, and a correction after the repository changed under the branch, +// are written at once. type GitBranchRemoteStatus struct { - // Commit is the commit hash the branch is at on the remote. Empty means the branch is not on - // the remote yet, which is not an error: a branch nothing has written to does not exist. + // Commit is the commit hash the branch is at on the remote. Empty means the + // branch is not on the remote yet, which is not an error: a branch nothing has + // written to does not exist. // +optional Commit string `json:"commit,omitempty"` @@ -123,20 +134,22 @@ type GitBranchRemoteStatus struct { // +optional LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"` - // VerifiedBy is how commit was learned: "Push" means the remote accepted a push from GitOps - // Reverser, and "Fetch" means a fetch found it. "Fetch" next to a commit GitOps Reverser did not - // make means somebody else pushed to the branch. + // VerifiedBy is how commit was learned: "Push" means the remote accepted a + // push from GitOps Reverser, and "Fetch" means a fetch found it. "Fetch" next + // to a commit GitOps Reverser did not make means somebody else pushed to the + // branch. // +optional // +kubebuilder:validation:Enum=Push;Fetch VerifiedBy string `json:"verifiedBy,omitempty"` - // Parent is what the branch would be created from. Set only while the branch is not on the - // remote. + // Parent is what the branch would be created from. Set only while the branch + // is not on the remote. // +optional Parent *GitBranchParentStatus `json:"parent,omitempty"` } -// GitBranchParentState is what a branch that does not exist yet would be created from. +// GitBranchParentState is what a branch that does not exist yet would be +// created from. // +kubebuilder:validation:Enum=Found;Missing;Unborn type GitBranchParentState string @@ -153,21 +166,24 @@ const ( GitBranchParentUnborn GitBranchParentState = "Unborn" ) -// GitBranchParentStatus is the parent branch of a branch that does not exist yet. +// GitBranchParentStatus is the parent branch of a branch that does not exist +// yet. type GitBranchParentStatus struct { - // State is what the branch would be created from: "Found" (the parent branch's tip), "Missing" - // (the parent branch is not on the remote, so nothing is written) or "Unborn" (an empty - // repository and no spec.parentBranch, so the first commit has no history). + // State is what the branch would be created from: "Found" (the parent branch's + // tip), "Missing" (the parent branch is not on the remote, so nothing is + // written) or "Unborn" (an empty repository and no spec.parentBranch, so the + // first commit has no history). // +required State GitBranchParentState `json:"state"` - // Branch is the parent branch: spec.parentBranch, or the remote's default branch when that is - // omitted. Empty when state is "Unborn", and when the remote names no default branch. + // Branch is the parent branch: spec.parentBranch, or the remote's default + // branch when that is omitted. Empty when state is "Unborn", and when the + // remote names no default branch. // +optional Branch string `json:"branch,omitempty"` - // Commit is the commit hash the parent branch is at on the remote, which the branch's first - // commit would build on. Set only when state is "Found". + // Commit is the commit hash the parent branch is at on the remote, which the + // branch's first commit would build on. Set only when state is "Found". // +optional Commit string `json:"commit,omitempty"` } @@ -186,9 +202,9 @@ type GitBranchParentStatus struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// GitBranch is one branch of a Git repository, written with a GitProvider's credential. Every -// GitTarget that references it writes through it, and only one GitBranch may hold a repository -// branch at a time. +// GitBranch is one branch of a Git repository, written with a GitProvider's +// credential. Every GitTarget that references it writes through it, and only +// one GitBranch may hold a repository branch at a time. type GitBranch struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/gitprovider_types.go b/api/v1alpha3/gitprovider_types.go index 7fc2e4b9..cda08cd3 100644 --- a/api/v1alpha3/gitprovider_types.go +++ b/api/v1alpha3/gitprovider_types.go @@ -9,19 +9,20 @@ import ( // GitProviderSpec defines the desired state of GitProvider. // -// Only the repository URL is immutable. The URL is the destination identity that every -// referencing GitTarget materializes into; changing it would silently point those -// targets at a different repository and orphan their existing materialization (the same -// reason a GitTarget's destination is immutable). To repoint, delete and recreate the -// GitProvider. Everything else here is operational and deliberately stays mutable — -// notably allowedGitBranches (widening or narrowing who may claim which branches is a -// normal change that must not require tearing down every GitBranch), plus auth and -// commit identity/signing. +// Only the repository URL is immutable. The URL is the destination identity +// that every referencing GitTarget materializes into; changing it would +// silently point those targets at a different repository and orphan their +// existing materialization (the same reason a GitTarget's destination is +// immutable). To repoint, delete and recreate the GitProvider. Everything else +// here is operational and deliberately stays mutable — notably +// allowedGitBranches (widening or narrowing who may claim which branches is a +// normal change that must not require tearing down every GitBranch), plus auth +// and commit identity/signing. // // +kubebuilder:validation:XValidation:rule="self.url == oldSelf.url",message="spec.url is immutable; delete and recreate the GitProvider to point at a different repository" type GitProviderSpec struct { - // URL is the repository's URL, over HTTPS or SSH. Immutable: delete and recreate the - // GitProvider to change it. + // URL is the repository's URL, over HTTPS or SSH. Immutable: delete and + // recreate the GitProvider to change it. // +kubebuilder:validation:MinLength=1 URL string `json:"url"` @@ -29,39 +30,41 @@ type GitProviderSpec struct { // downgrade a private repository to anonymous silently, and surface as an authentication error // against the remote rather than as a mistake in this object. - // SecretRef names the Secret, in this namespace, that holds the credentials for the repository. - // Omitted means anonymous access. + // SecretRef names the Secret, in this namespace, that holds the credentials + // for the repository. Omitted means anonymous access. // +optional // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.secretRef.name must not be empty; omit secretRef entirely for anonymous access" SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` - // KnownHostsRef names a ConfigMap or Secret, in this namespace, holding SSH known_hosts, so - // several GitProviders on one host can share them. SSH only. Host keys come from the - // credentials Secret's own known_hosts first, then this reference, then the installation's - // default; without valid keys, SSH fails. + // KnownHostsRef names a ConfigMap or Secret, in this namespace, holding SSH + // known_hosts, so several GitProviders on one host can share them. SSH only. + // Host keys come from the credentials Secret's own known_hosts first, then + // this reference, then the installation's default; without valid keys, SSH + // fails. // +optional KnownHostsRef *KnownHostsReference `json:"knownHostsRef,omitempty"` - // AllowedGitBranches says which GitBranches may claim a branch of this repository through this - // GitProvider: a grant must admit the GitBranch's namespace, and one of its branchPatterns must - // match the GitBranch's spec.branch. Grants combine by union. Unlike the grant lists of - // GitBranch and KubeProvider it has no default: omitted or empty, no GitBranch may claim + // AllowedGitBranches says which GitBranches may claim a branch of this + // repository through this GitProvider: a grant must admit the GitBranch's + // namespace, and one of its branchPatterns must match the GitBranch's + // spec.branch. Grants combine by union. Unlike the grant lists of GitBranch + // and KubeProvider it has no default: omitted or empty, no GitBranch may claim // through it, including in this namespace. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 AllowedGitBranches []GitProviderGitBranchGrant `json:"allowedGitBranches,omitempty"` - // Commit sets the committer identity and commit signing. Commit messages are set on the - // GitTarget, in spec.commit.message. + // Commit sets the committer identity and commit signing. Commit messages are + // set on the GitTarget, in spec.commit.message. // +optional Commit *CommitSpec `json:"commit,omitempty"` } -// KnownHostsReference points at a namespace-local ConfigMap or Secret that holds SSH known_hosts -// host-trust material. The data is read from the "known_hosts" key, falling back to -// "ssh_known_hosts" (the key Argo CD's argocd-ssh-known-hosts-cm ConfigMap uses, for host keys -// copied out of it). +// KnownHostsReference points at a namespace-local ConfigMap or Secret that +// holds SSH known_hosts host-trust material. The data is read from the +// "known_hosts" key, falling back to "ssh_known_hosts" (the key Argo CD's +// argocd-ssh-known-hosts-cm ConfigMap uses, for host keys copied out of it). type KnownHostsReference struct { // Kind is "ConfigMap" or "Secret". Defaults to "ConfigMap". // +optional @@ -69,8 +72,9 @@ type KnownHostsReference struct { // +kubebuilder:default=ConfigMap Kind string `json:"kind,omitempty"` - // Name is the name of the ConfigMap or Secret. Its "known_hosts" key is read, or - // "ssh_known_hosts" (the key Argo CD's argocd-ssh-known-hosts-cm uses) when that is absent. + // Name is the name of the ConfigMap or Secret. Its "known_hosts" key is read, + // or "ssh_known_hosts" (the key Argo CD's argocd-ssh-known-hosts-cm uses) when + // that is absent. // +required // +kubebuilder:validation:MinLength=1 Name string `json:"name"` @@ -78,12 +82,14 @@ type KnownHostsReference struct { // EncryptionSpec configures Secret encryption behavior for git writes. type EncryptionSpec struct { - // Provider is the encryption tool. Only "sops" is supported. Defaults to "sops". + // Provider is the encryption tool. Only "sops" is supported. Defaults to + // "sops". // +kubebuilder:default=sops // +kubebuilder:validation:Enum=sops Provider string `json:"provider"` - // SecretRef names the Secret, in this namespace, that holds the provider's keys. + // SecretRef names the Secret, in this namespace, that holds the provider's + // keys. // +kubebuilder:validation:XValidation:rule="self.name != ''",message="encryption.secretRef.name must not be empty" // +optional SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"` @@ -95,8 +101,8 @@ type EncryptionSpec struct { // AgeEncryptionSpec configures age recipient resolution behavior. type AgeEncryptionSpec struct { - // Enabled turns on age encryption: resolving recipients, and creating a key when asked. - // Defaults to false. + // Enabled turns on age encryption: resolving recipients, and creating a key + // when asked. Defaults to false. // +optional // +kubebuilder:default=false Enabled bool `json:"enabled,omitempty"` @@ -113,14 +119,14 @@ type AgeRecipientsSpec struct { // +kubebuilder:validation:items:MinLength=1 PublicKeys []string `json:"publicKeys,omitempty"` - // ExtractFromSecret adds the recipient of every *.agekey entry in encryption.secretRef. - // Defaults to false. + // ExtractFromSecret adds the recipient of every *.agekey entry in + // encryption.secretRef. Defaults to false. // +optional // +kubebuilder:default=false ExtractFromSecret bool `json:"extractFromSecret,omitempty"` - // GenerateWhenMissing creates a date-named *.agekey entry in encryption.secretRef when it has - // none. Defaults to false. + // GenerateWhenMissing creates a date-named *.agekey entry in + // encryption.secretRef when it has none. Defaults to false. // +optional // +kubebuilder:default=false GenerateWhenMissing bool `json:"generateWhenMissing,omitempty"` @@ -132,9 +138,9 @@ type GitProviderStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this GitProvider's state: whether the repository - // answers with its credential, and whether commit signing is ready. Ready, Reconciling and - // Stalled follow kstatus. + // Conditions are the latest observations of this GitProvider's state: whether + // the repository answers with its credential, and whether commit signing is + // ready. Ready, Reconciling and Stalled follow kstatus. // +optional // +listType=map // +listMapKey=type @@ -142,9 +148,9 @@ type GitProviderStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // SigningPublicKey is the SSH public key commits are signed with, in authorized_keys format. - // Register it as a signing key with your Git host. Set only when spec.commit.signing is - // configured and its key is available. + // SigningPublicKey is the SSH public key commits are signed with, in + // authorized_keys format. Register it as a signing key with your Git host. Set + // only when spec.commit.signing is configured and its key is available. // +optional SigningPublicKey string `json:"signingPublicKey,omitempty"` // The check runs on the steady reconcile interval, so a provider's resourceVersion moves at @@ -159,12 +165,13 @@ type GitProviderStatus struct { LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"` } -// CommitSpec configures the commit identity and signing a GitProvider uses. Message formatting -// lives on the GitTarget (spec.commit.message), because it describes the folder rather than the -// connection. +// CommitSpec configures the commit identity and signing a GitProvider uses. +// Message formatting lives on the GitTarget (spec.commit.message), because it +// describes the folder rather than the connection. type CommitSpec struct { - // Committer is the identity written as each commit's committer. With signing enabled, its - // email must be a verified address of the account that owns the signing key. + // Committer is the identity written as each commit's committer. With signing + // enabled, its email must be a verified address of the account that owns the + // signing key. // +optional Committer *CommitterSpec `json:"committer,omitempty"` @@ -186,7 +193,8 @@ type CommitterSpec struct { Email string `json:"email,omitempty"` } -// CommitMessageSpec configures commit message formatting, on GitTarget.spec.commit.message. +// CommitMessageSpec configures commit message formatting, on +// GitTarget.spec.commit.message. type CommitMessageSpec struct { // LiveTemplate is the Go template for the message of a commit made from // live edits, whatever its size. Omitted, a built-in template is used. @@ -223,18 +231,20 @@ type CommitMessageSpec struct { // renders RequestMessage is what keeps the split honest in the other direction: silently // discarding a requester's stated reason would be worse than having no template at all. - // RequestTemplate is the Go template that frames a CommitRequest's message. It is used only for - // a commit whose CommitRequest supplied a message, and gets liveTemplate's fields plus - // RequestMessage, the message as written; template syntax inside the message stays literal. - // Omitted, the message is committed as written. A template that never renders RequestMessage - // is rejected, and one that fails at commit time commits the message as written. + // RequestTemplate is the Go template that frames a CommitRequest's message. It + // is used only for a commit whose CommitRequest supplied a message, and gets + // liveTemplate's fields plus RequestMessage, the message as written; template + // syntax inside the message stays literal. Omitted, the message is committed + // as written. A template that never renders RequestMessage is rejected, and + // one that fails at commit time commits the message as written. // +optional RequestTemplate string `json:"requestTemplate,omitempty"` - // ReconcileTemplate is the Go template for the message of a snapshot or resync commit. Fields: - // Count, GitTarget, Group, Version, Resource, APIVersion, Namespace and ResourceVersion (the - // list's, not one object's). The type and namespace fields are empty for a snapshot of the - // whole folder, and ResourceVersion can be empty, so guard them. A template naming Revision, + // ReconcileTemplate is the Go template for the message of a snapshot or resync + // commit. Fields: Count, GitTarget, Group, Version, Resource, APIVersion, + // Namespace and ResourceVersion (the list's, not one object's). The type and + // namespace fields are empty for a snapshot of the whole folder, and + // ResourceVersion can be empty, so guard them. A template naming Revision, // renamed to ResourceVersion, is rejected. // +optional ReconcileTemplate string `json:"reconcileTemplate,omitempty"` @@ -242,12 +252,13 @@ type CommitMessageSpec struct { // CommitSigningSpec configures commit signing. type CommitSigningSpec struct { - // SecretRef names the Secret, in this namespace, that holds the SSH signing key. + // SecretRef names the Secret, in this namespace, that holds the SSH signing + // key. // +kubebuilder:validation:XValidation:rule="self.name != ''",message="commit.signing.secretRef.name must not be empty" SecretRef meta.LocalObjectReference `json:"secretRef"` - // GenerateWhenMissing creates a signing key in the Secret when it holds none. Defaults to - // false. + // GenerateWhenMissing creates a signing key in the Secret when it holds none. + // Defaults to false. // +optional // +kubebuilder:default=false GenerateWhenMissing bool `json:"generateWhenMissing,omitempty"` @@ -262,9 +273,10 @@ type CommitSigningSpec struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// GitProvider is a connection to one Git repository: its URL, the credential that reaches it, -// the committer identity and commit signing. GitBranches claim branches of the repository -// through it, as its spec.allowedGitBranches allow. +// GitProvider is a connection to one Git repository: its URL, the credential +// that reaches it, the committer identity and commit signing. GitBranches +// claim branches of the repository through it, as its spec.allowedGitBranches +// allow. type GitProvider struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/gitreceiver_types.go b/api/v1alpha3/gitreceiver_types.go index 2d697bd2..56b6f623 100644 --- a/api/v1alpha3/gitreceiver_types.go +++ b/api/v1alpha3/gitreceiver_types.go @@ -17,23 +17,25 @@ import ( // GitReceiverSpec defines the desired state of GitReceiver. type GitReceiverSpec struct { - // Type is the payload and signature the Git host sends: "github" (X-Hub-Signature-256), - // "gitlab" (X-Gitlab-Token), "gitea" (X-Gitea-Signature), or "generic-hmac" for a relay of - // your own (X-Signature: sha256=, over a {"repository", "branch", - // "after"} body). + // Type is the payload and signature the Git host sends: "github" + // (X-Hub-Signature-256), "gitlab" (X-Gitlab-Token), "gitea" + // (X-Gitea-Signature), or "generic-hmac" for a relay of your own (X-Signature: + // sha256=, over a {"repository", "branch", "after"} + // body). // +required // +kubebuilder:validation:Enum=github;gitlab;gitea;generic-hmac Type ReceiverType `json:"type"` - // SecretRef names the Secret, in this namespace, whose "token" key is the webhook secret. The - // token also determines status.webhookPath, so rotating it moves the path: register the new - // path with the new secret. + // SecretRef names the Secret, in this namespace, whose "token" key is the + // webhook secret. The token also determines status.webhookPath, so rotating it + // moves the path: register the new path with the new secret. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.secretRef.name must not be empty" SecretRef meta.LocalObjectReference `json:"secretRef"` } -// ReceiverType is the payload format a receiver accepts. The values are Flux's Receiver types. +// ReceiverType is the payload format a receiver accepts. The values are Flux's +// Receiver types. type ReceiverType string const ( @@ -73,9 +75,9 @@ type GitReceiverStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this GitReceiver's state. Ready, Reconciling and - // Stalled follow kstatus; Ready is True once the token is readable and the receiver listener - // is on. + // Conditions are the latest observations of this GitReceiver's state. Ready, + // Reconciling and Stalled follow kstatus; Ready is True once the token is + // readable and the receiver listener is on. // +optional // +listType=map // +listMapKey=type @@ -83,9 +85,10 @@ type GitReceiverStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // WebhookPath is the path this GitReceiver is served on, under the receiver listener: /git-push/ - // and an HMAC of its namespace and name, keyed by the token. Point the Git host's push webhook - // at the listener's address plus this path. Empty while the token is missing. + // WebhookPath is the path this GitReceiver is served on, under the receiver + // listener: /git-push/ and an HMAC of its namespace and name, keyed by the + // token. Point the Git host's push webhook at the listener's address plus this + // path. Empty while the token is missing. // +optional WebhookPath string `json:"webhookPath,omitempty"` } @@ -107,12 +110,14 @@ type GitReceiverStatus struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// GitReceiver is the endpoint a Git host notifies when it receives a push. A delivery wakes every -// GitBranch of every GitProvider, in any namespace, whose spec.url names the delivery's -// repository, so pushes are picked up at once instead of at the next refresh. A GitProvider that -// reaches the host through another host name or port is not matched. A delivery is only a hint: -// the branch is then read with its own GitProvider's credential, so a delivery cannot choose a -// URL, a credential or a commit. GitBranch.status.lastPushNoticeAt shows deliveries arriving. +// GitReceiver is the endpoint a Git host notifies when it receives a push. A +// delivery wakes every GitBranch of every GitProvider, in any namespace, whose +// spec.url names the delivery's repository, so pushes are picked up at once +// instead of at the next refresh. A GitProvider that reaches the host through +// another host name or port is not matched. A delivery is only a hint: the +// branch is then read with its own GitProvider's credential, so a delivery +// cannot choose a URL, a credential or a commit. +// GitBranch.status.lastPushNoticeAt shows deliveries arriving. type GitReceiver struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/gittarget_types.go b/api/v1alpha3/gittarget_types.go index 5336aa91..b08ed7bb 100644 --- a/api/v1alpha3/gittarget_types.go +++ b/api/v1alpha3/gittarget_types.go @@ -10,23 +10,24 @@ import ( // GitTargetSpec defines the desired state of GitTarget. // -// The destination fields — gitBranchRef and path — are immutable. A -// GitTarget materializes the watched resources at exactly one (repository branch, -// folder); changing where it writes would orphan the old materialization and require -// migrating manifests between repositories/branches/folders. Instead of reconciling -// that move, the destination is fixed: to relocate a GitTarget, delete it and create a -// new one. This keeps the one-owner-per-folder invariant and the initial-snapshot gate -// simple — a successful snapshot can never be silently invalidated by a destination -// change. +// The destination fields — gitBranchRef and path — are immutable. A GitTarget +// materializes the watched resources at exactly one (repository branch, +// folder); changing where it writes would orphan the old materialization and +// require migrating manifests between repositories/branches/folders. Instead +// of reconciling that move, the destination is fixed: to relocate a GitTarget, +// delete it and create a new one. This keeps the one-owner-per-folder +// invariant and the initial-snapshot gate simple — a successful snapshot can +// never be silently invalidated by a destination change. // // +kubebuilder:validation:XValidation:rule="self.gitBranchRef == oldSelf.gitBranchRef",message="spec.gitBranchRef is immutable; delete and recreate the GitTarget to change its destination" // +kubebuilder:validation:XValidation:rule="self.path == oldSelf.path",message="spec.path is immutable; delete and recreate the GitTarget to change its destination" // +kubebuilder:validation:XValidation:rule="!has(self.onRefusal) || self.onRefusal != 'Reapply' || has(self.applierClaimRef)",message="spec.onRefusal: Reapply needs spec.applierClaimRef, the applier claim asked to reapply" type GitTargetSpec struct { - // GitBranchRef names the GitBranch this GitTarget writes to: the repository branch and the - // credential that writes it. An omitted namespace means this GitTarget's namespace. The - // GitBranch's spec.allowedGitTargets must admit this namespace. Several GitTargets may share - // one GitBranch. Immutable: delete and recreate the GitTarget to change it. + // GitBranchRef names the GitBranch this GitTarget writes to: the repository + // branch and the credential that writes it. An omitted namespace means this + // GitTarget's namespace. The GitBranch's spec.allowedGitTargets must admit + // this namespace. Several GitTargets may share one GitBranch. Immutable: + // delete and recreate the GitTarget to change it. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.gitBranchRef.name must not be empty" GitBranchRef meta.NamespacedObjectReference `json:"gitBranchRef"` @@ -34,14 +35,16 @@ type GitTargetSpec struct { // No default, and "" is rejected: a path left blank by accident must not mean the repository // root. "." is the Argo CD and Flux spelling of a deliberate root. - // Path is the folder this GitTarget writes, relative to the repository root. Use "." for the - // root itself. A leading "/" and ".." are rejected, and a trailing "/" is dropped. Immutable: - // delete and recreate the GitTarget to change it. + // Path is the folder this GitTarget writes, relative to the repository root. + // Use "." for the root itself. A leading "/" and ".." are rejected, and a + // trailing "/" is dropped. Immutable: delete and recreate the GitTarget to + // change it. // +required // +kubebuilder:validation:MinLength=1 Path string `json:"path"` - // Encryption configures how Secrets are encrypted before they are written to Git. + // Encryption configures how Secrets are encrypted before they are written to + // Git. // +optional Encryption *EncryptionSpec `json:"encryption,omitempty"` @@ -49,12 +52,14 @@ type GitTargetSpec struct { // be guessing on Flux's behalf, and the guess would drive a write. A reference the user declares // claims nothing. See docs/design/apply-trigger.md. - // ApplierClaimRef names the FluxClaim or ArgoCDClaim, in this namespace, that holds the applier - // of this folder: the Flux Kustomization or Argo CD Application that applies it. With it set, - // GitOps Reverser decides when that applier runs, and runs it only after the edits it holds are - // published. Objects another applier owns are not mirrored into the folder. The applier must - // apply this GitTarget's repository and branch; the ApplierClaimReady condition reports whether - // it does. Several GitTargets may name one applier claim. + // ApplierClaimRef names the FluxClaim or ArgoCDClaim, in this namespace, that + // holds the applier of this folder: the Flux Kustomization or Argo CD + // Application that applies it. With it set, GitOps Reverser decides when that + // applier runs, and runs it only after the edits it holds are published. + // Objects another applier owns are not mirrored into the folder. The applier + // must apply this GitTarget's repository and branch; the ApplierClaimReady + // condition reports whether it does. Several GitTargets may name one applier + // claim. // +optional ApplierClaimRef *ApplierClaimReference `json:"applierClaimRef,omitempty"` @@ -62,17 +67,19 @@ type GitTargetSpec struct { // document: re-applying otherwise prunes, or puts back, an object somebody else is deleting. A // folder-level refusal never fires it, because re-applying Git cannot repair a folder. - // OnRefusal decides what happens when a live edit is refused because it has no valid place in - // the folder. A refusal makes no commit, so the edit stays on the cluster until something - // applies Git over it. + // OnRefusal decides what happens when a live edit is refused because it has no + // valid place in the folder. A refusal makes no commit, so the edit stays on + // the cluster until something applies Git over it. // - // "Ignore" (the default) reports the refusal in conditions and events and does nothing else. + // "Ignore" (the default) reports the refusal in conditions and events and does + // nothing else. // - // "Reapply" asks the applier in spec.applierClaimRef, which it requires, to apply the branch - // once the pending edits are published, which reverts the refused edit. It fires only for an - // edit to a document the folder already holds, in a write that removes no document, and never - // for a refusal of the whole folder, such as unparseable YAML. A refusal that stands is - // reapplied once, not on every recheck. + // "Reapply" asks the applier in spec.applierClaimRef, which it requires, to + // apply the branch once the pending edits are published, which reverts the + // refused edit. It fires only for an edit to a document the folder already + // holds, in a write that removes no document, and never for a refusal of the + // whole folder, such as unparseable YAML. A refusal that stands is reapplied + // once, not on every recheck. // +optional // +kubebuilder:validation:Enum=Ignore;Reapply // +kubebuilder:default=Ignore @@ -100,19 +107,21 @@ type GitTargetSpec struct { // identity inside a file and the write-time guards still refuse a sensitive document in a // shared one. - // Placement decides where a new resource is written: one with no document in the folder yet. - // A resource that already has a document is updated where that document is, so a change here - // affects only resources created afterward, and labeling a resource later never moves its file. + // Placement decides where a new resource is written: one with no document in + // the folder yet. A resource that already has a document is updated where that + // document is, so a change here affects only resources created afterward, and + // labeling a resource later never moves its file. // - // byType and default are path templates over the resource's identity ({namespace}, {name}, - // {group}, {version}, {resource}, {kind} and more), and may read a label: - // "{label:app.kubernetes.io/instance}/configmaps.yaml". "{label:key}" renders "_unlabeled" for a - // resource without the label, and "{namespace}" renders "_cluster" for a cluster-scoped one. - // Append "|fallback" to either to choose that value: "{label:team|unassigned}", - // "{namespace|_global}". A fallback is up to 63 characters of [A-Za-z0-9._-], is not "." or - // "..", and may be empty to drop the path segment. A {namespace} fallback that names a real - // namespace files cluster-scoped resources in that namespace's folder. A fallback on any other - // variable is rejected. + // byType and default are path templates over the resource's identity + // ({namespace}, {name}, {group}, {version}, {resource}, {kind} and more), and + // may read a label: "{label:app.kubernetes.io/instance}/configmaps.yaml". + // "{label:key}" renders "_unlabeled" for a resource without the label, and + // "{namespace}" renders "_cluster" for a cluster-scoped one. Append + // "|fallback" to either to choose that value: "{label:team|unassigned}", + // "{namespace|_global}". A fallback is up to 63 characters of [A-Za-z0-9._-], + // is not "." or "..", and may be empty to drop the path segment. A {namespace} + // fallback that names a real namespace files cluster-scoped resources in that + // namespace's folder. A fallback on any other variable is rejected. // +optional Placement *GitTargetPlacementSpec `json:"placement,omitempty"` @@ -122,26 +131,29 @@ type GitTargetSpec struct { // line into a kustomize folder that already supplies one. // SerializeNamespace decides whether a namespaced resource's document carries - // metadata.namespace. It applies to every write, not just the first, and is ignored for - // cluster-scoped resources. + // metadata.namespace. It applies to every write, not just the first, and is + // ignored for cluster-scoped resources. // - // Omitted, it is decided per document: the namespace is left out only when the kustomization - // governing the document's path sets namespace: to that same namespace. This suits a folder of - // nested kustomize roots that each set their own namespace. + // Omitted, it is decided per document: the namespace is left out only when the + // kustomization governing the document's path sets namespace: to that same + // namespace. This suits a folder of nested kustomize roots that each set their + // own namespace. // // true always writes it. Use it for a flat folder applied as is. // - // false never writes it, for a folder whose namespace something else supplies: a Flux - // Kustomization's targetNamespace, an Argo CD Application's destination.namespace, or a - // kustomization. Such a folder holds one source namespace; a WatchRule bringing a second one is - // refused with GitPathAccepted=False, reason MultipleSourceNamespaces. + // false never writes it, for a folder whose namespace something else supplies: + // a Flux Kustomization's targetNamespace, an Argo CD Application's + // destination.namespace, or a kustomization. Such a folder holds one source + // namespace; a WatchRule bringing a second one is refused with + // GitPathAccepted=False, reason MultipleSourceNamespaces. // +optional SerializeNamespace *bool `json:"serializeNamespace,omitempty"` - // KubeProviderRef names the KubeProvider this GitTarget mirrors from: the source cluster and the - // credential that reads it. An omitted namespace means this GitTarget's namespace. The - // KubeProvider's spec.allowedGitTargets must admit this namespace. Immutable: delete and - // recreate the GitTarget to change it. + // KubeProviderRef names the KubeProvider this GitTarget mirrors from: the + // source cluster and the credential that reads it. An omitted namespace means + // this GitTarget's namespace. The KubeProvider's spec.allowedGitTargets must + // admit this namespace. Immutable: delete and recreate the GitTarget to change + // it. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.kubeProviderRef.name must not be empty" // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.kubeProviderRef is immutable; delete and recreate the GitTarget to change the cluster it mirrors" @@ -150,18 +162,20 @@ type GitTargetSpec struct { // Mutable, unlike the destination fields above: recovering from a scope mistake must not // require recreating the GitTarget, which is the one operation that loses the folder's history. - // Prune decides which deletions remove documents from this folder: a delete observed on the - // source cluster, and a document a resync finds no resource for. Omitted, mode is "OnEvent": - // observed deletes are mirrored, and a resync removes nothing. + // Prune decides which deletions remove documents from this folder: a delete + // observed on the source cluster, and a document a resync finds no resource + // for. Omitted, mode is "OnEvent": observed deletes are mirrored, and a resync + // removes nothing. // +optional Prune *PrunePolicy `json:"prune,omitempty"` // Batching and phrasing describe the folder, so they live here; committer and signing describe // the identity talking to the remote and stay on GitProvider. Migration: docs/UPGRADING.md. - // Commit configures how this GitTarget's writes are batched into commits and how the commits - // are worded. Omitted, a commit window closes 5s after its last write or 1m after it opened, - // and the built-in message templates are used. + // Commit configures how this GitTarget's writes are batched into commits and + // how the commits are worded. Omitted, a commit window closes 5s after its + // last write or 1m after it opened, and the built-in message templates are + // used. // +optional Commit *GitTargetCommitSpec `json:"commit,omitempty"` @@ -172,12 +186,13 @@ type GitTargetSpec struct { // out of order, on resume. status.retention is withheld because nothing is swept, and zero // would read as converged. - // Suspend stops this GitTarget from making new commits, without deleting it. Watches keep - // running and status.placement stays current; status.retention is not published while it is - // set. A commit already made locally is still pushed, so one more commit can appear seconds - // after it is set: suspend stops the next write and does not undo the last one. Clearing it - // resumes from the current cluster state at the next resync; writes skipped while suspended - // are not replayed. Defaults to false. + // Suspend stops this GitTarget from making new commits, without deleting it. + // Watches keep running and status.placement stays current; status.retention is + // not published while it is set. A commit already made locally is still + // pushed, so one more commit can appear seconds after it is set: suspend stops + // the next write and does not undo the last one. Clearing it resumes from the + // current cluster state at the next resync; writes skipped while suspended are + // not replayed. Defaults to false. // +optional Suspend bool `json:"suspend,omitempty"` } @@ -198,24 +213,28 @@ type GitTargetCommitSpec struct { // overflows time.ParseDuration — and one stored value that no typed client can decode breaks // GET and LIST for the whole kind, taking the GitTarget informer down with it. - // Window sets when this GitTarget's commit windows close. A window opens on an author's first - // write and collects that author's writes until idleTimeout passes without one, or maxDuration - // after it opened, whichever comes first. A change applies to windows opened afterward. A - // CommitRequest attached to a window replaces both timers for that window. + // Window sets when this GitTarget's commit windows close. A window opens on an + // author's first write and collects that author's writes until idleTimeout + // passes without one, or maxDuration after it opened, whichever comes first. A + // change applies to windows opened afterward. A CommitRequest attached to a + // window replaces both timers for that window. // +optional Window *CommitWindow `json:"window,omitempty"` - // Message sets the commit message templates. A template left empty uses the built-in one. + // Message sets the commit message templates. A template left empty uses the + // built-in one. // +optional Message *CommitMessageSpec `json:"message,omitempty"` } -// CommitWindow holds the two timers that close a GitTarget's commit windows. A change to them -// applies to windows that open afterward; a window already open keeps the timers it opened with. +// CommitWindow holds the two timers that close a GitTarget's commit windows. A +// change to them applies to windows that open afterward; a window already open +// keeps the timers it opened with. // +kubebuilder:validation:XValidation:rule="!has(self.idleTimeout) || !has(self.maxDuration) || duration(self.idleTimeout) <= duration(self.maxDuration)",message="spec.commit.window.idleTimeout must not exceed maxDuration" type CommitWindow struct { - // IdleTimeout closes the window this long after its last write, as a Go duration ("5s", - // "750ms"). "0s" makes one commit per write. At most "24h". Defaults to "5s". + // IdleTimeout closes the window this long after its last write, as a Go + // duration ("5s", "750ms"). "0s" makes one commit per write. At most "24h". + // Defaults to "5s". // +optional // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$" @@ -223,9 +242,9 @@ type CommitWindow struct { // +kubebuilder:validation:XValidation:rule="duration(self) <= duration('24h')",message="spec.commit.window.idleTimeout must be a Go duration of at most 24h" IdleTimeout *metav1.Duration `json:"idleTimeout,omitempty"` - // MaxDuration closes the window this long after it opened, however many writes keep arriving, - // as a Go duration ("1m", "90s"). "0s" closes it right after the write that opened it. At most - // "24h". Defaults to "1m". + // MaxDuration closes the window this long after it opened, however many writes + // keep arriving, as a Go duration ("1m", "90s"). "0s" closes it right after + // the write that opened it. At most "24h". Defaults to "1m". // +optional // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$" @@ -234,39 +253,42 @@ type CommitWindow struct { MaxDuration *metav1.Duration `json:"maxDuration,omitempty"` } -// GitTargetPlacementSpec declares where NEW resources are written when no document -// for their identity exists yet in Git — one exact-type map plus a fallback -// default template (Option B2 of -// docs/layout/new-file-placement-rules.md). There is -// deliberately no separate "sensitive" placement block: sensitivity is a -// write-safety classification the controller owns (encrypt the content, keep the -// path identity-complete, never append or co-mingle), not a second placement -// namespace the user has to configure. A user routes Secrets the same way they -// route anything else — by naming their type in ByType. When a resource's type -// has no ByType entry and no Default, the new document goes beside the folder's -// kustomization when the whole folder is governed by exactly one supported -// kustomization (so the file is reachable from a render root instead of being -// written where kustomize would never build it), and otherwise at the built-in -// versionless {namespace}/{group}/{resource}/{name}.yaml path. -// Nothing infers a destination from where the repository keeps other resources of -// the same type: a layout this operator cannot derive from one root is declared -// here or it takes the built-in path. Because the built-in path omits the API version, -// objects that differ only by version share a file; a target that watches several -// versions of the same group/resource and wants them separated must use a -// ByType/Default template that includes {version}. +// GitTargetPlacementSpec declares where NEW resources are written when no +// document for their identity exists yet in Git — one exact-type map plus a +// fallback default template (Option B2 of +// docs/layout/new-file-placement-rules.md). There is deliberately no separate +// "sensitive" placement block: sensitivity is a write-safety classification +// the controller owns (encrypt the content, keep the path identity-complete, +// never append or co-mingle), not a second placement namespace the user has to +// configure. A user routes Secrets the same way they route anything else — by +// naming their type in ByType. When a resource's type has no ByType entry and +// no Default, the new document goes beside the folder's kustomization when the +// whole folder is governed by exactly one supported kustomization (so the file +// is reachable from a render root instead of being written where kustomize +// would never build it), and otherwise at the built-in versionless +// {namespace}/{group}/{resource}/{name}.yaml path. Nothing infers a +// destination from where the repository keeps other resources of the same +// type: a layout this operator cannot derive from one root is declared here or +// it takes the built-in path. Because the built-in path omits the API version, +// objects that differ only by version share a file; a target that watches +// several versions of the same group/resource and wants them separated must +// use a ByType/Default template that includes {version}. type GitTargetPlacementSpec struct { - // ByType maps a resource type, written "{group}/{version}/{resource}" ("apps/v1/deployments", - // or "v1/configmaps" for the core group), to the path template for new resources of that - // type. The template of a sensitive type, such as Secrets, must contain the resource's whole - // identity, so two of them never share a file. + // ByType maps a resource type, written "{group}/{version}/{resource}" + // ("apps/v1/deployments", or "v1/configmaps" for the core group), to the path + // template for new resources of that type. The template of a sensitive type, + // such as Secrets, must contain the resource's whole identity, so two of them + // never share a file. // +optional ByType map[string]string `json:"byType,omitempty"` - // Default is the path template for a new resource whose type has no byType entry. Omitted, a - // new document goes next to the folder's kustomization when exactly one governs the folder, - // and otherwise to {namespace}/{group}/{resource}/{name}.yaml. That path has no version, so - // add {version} to a template to keep two versions of one resource apart. A bundling template - // such as "all.yaml" is valid only when every sensitive type has its own byType entry. + // Default is the path template for a new resource whose type has no byType + // entry. Omitted, a new document goes next to the folder's kustomization when + // exactly one governs the folder, and otherwise to + // {namespace}/{group}/{resource}/{name}.yaml. That path has no version, so add + // {version} to a template to keep two versions of one resource apart. A + // bundling template such as "all.yaml" is valid only when every sensitive type + // has its own byType entry. // +optional Default string `json:"default,omitempty"` @@ -279,11 +301,12 @@ type GitTargetPlacementSpec struct { // either way; this flag only decides what to do when there is no root at all. To leave a // folder's root untouched, root the target lower — the ancestor walk stops at the write jail. - // UseKustomize makes GitOps Reverser create a kustomization.yaml at spec.path when no - // kustomization governs the folder yet. The first new document creates it, listing every - // managed document already in the folder and the new one, and nothing else: no namespace:. - // It has no effect on a folder that already has a kustomization. A new file is always added - // to the kustomization governing it, and a template that places a document outside that + // UseKustomize makes GitOps Reverser create a kustomization.yaml at spec.path + // when no kustomization governs the folder yet. The first new document creates + // it, listing every managed document already in the folder and the new one, + // and nothing else: no namespace:. It has no effect on a folder that already + // has a kustomization. A new file is always added to the kustomization + // governing it, and a template that places a document outside that // kustomization is refused. Defaults to false. // +optional UseKustomize bool `json:"useKustomize,omitempty"` @@ -295,8 +318,9 @@ type GitTargetStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this GitTarget's state. Ready, Reconciling and - // Stalled follow kstatus; the others say which check holds the GitTarget back. + // Conditions are the latest observations of this GitTarget's state. Ready, + // Reconciling and Stalled follow kstatus; the others say which check holds the + // GitTarget back. // +optional // +listType=map // +listMapKey=type @@ -311,15 +335,16 @@ type GitTargetStatus struct { // An observation, not a condition: a sweep suppressed by spec.prune.mode is the configured // outcome, and a condition going False for it would train operators to ignore the real ones. - // Retention counts the documents a resync kept because spec.prune.mode did not allow removing - // them. A delete observed on the cluster and not mirrored under "Never" is not counted, so a - // "Never" folder can report zero. It is information only and changes no condition. + // Retention counts the documents a resync kept because spec.prune.mode did not + // allow removing them. A delete observed on the cluster and not mirrored under + // "Never" is not counted, so a "Never" folder can report zero. It is + // information only and changes no condition. // +optional Retention *GitTargetRetentionStatus `json:"retention,omitempty"` - // Placement is this folder's layout as the last scan resolved it: whether a kustomization - // governs new documents, which one, and which bases the folder renders but may not write. The - // LayoutResolved condition gives the verdict. + // Placement is this folder's layout as the last scan resolved it: whether a + // kustomization governs new documents, which one, and which bases the folder + // renders but may not write. The LayoutResolved condition gives the verdict. // +optional Placement *GitTargetPlacementStatus `json:"placement,omitempty"` } @@ -332,54 +357,60 @@ type GitTargetStatus struct { // every field is a fact from the last scan, available before the target has written a byte. // No counters — those are metrics, and a counter in status is a status write per event. -// GitTargetPlacementStatus is what the last scan resolved about a GitTarget folder's layout. +// GitTargetPlacementStatus is what the last scan resolved about a GitTarget +// folder's layout. type GitTargetPlacementStatus struct { // Mode is how this folder is written. // - // "Plain": no kustomization governs the folder. Documents are written and removed, and - // nothing else is touched. + // "Plain": no kustomization governs the folder. Documents are written and + // removed, and nothing else is touched. // - // "KustomizeRoot": one kustomization governs the folder and renders nothing outside it. A new - // document is also added to its resources:, a removed one is dropped from it, and every write - // is checked by rendering, so kustomize can refuse one. + // "KustomizeRoot": one kustomization governs the folder and renders nothing + // outside it. A new document is also added to its resources:, a removed one is + // dropped from it, and every write is checked by rendering, so kustomize can + // refuse one. // - // "KustomizeOverlay": as KustomizeRoot, and the folder also renders a base outside it - // (readOnlyBases). An edit to a field the base sets is written into the overlay where it can - // be (images:, replicas:) and refused with WriteBoundaryRefused otherwise. Deleting an object - // the base supplies writes a $patch: delete into the overlay. + // "KustomizeOverlay": as KustomizeRoot, and the folder also renders a base + // outside it (readOnlyBases). An edit to a field the base sets is written into + // the overlay where it can be (images:, replicas:) and refused with + // WriteBoundaryRefused otherwise. Deleting an object the base supplies writes + // a $patch: delete into the overlay. // - // Empty when the folder holds more than one kustomize root (LayoutResolved=Ambiguous): no new - // document is placed until spec.path points at one of them. + // Empty when the folder holds more than one kustomize root + // (LayoutResolved=Ambiguous): no new document is placed until spec.path points + // at one of them. // +optional // +kubebuilder:validation:Enum=Plain;KustomizeRoot;KustomizeOverlay Mode PlacementMode `json:"mode,omitempty"` - // RenderRoot is the kustomization directory that governs new documents, relative to - // spec.path ("." is the folder itself). Empty for Plain, and when the layout is Ambiguous. + // RenderRoot is the kustomization directory that governs new documents, + // relative to spec.path ("." is the folder itself). Empty for Plain, and when + // the layout is Ambiguous. // +optional RenderRoot string `json:"renderRoot,omitempty"` - // ReadOnlyBases are the kustomization directories this folder renders but may not write, - // relative to spec.path, so they start with "../". Set only for KustomizeOverlay. An edit to a - // document under one of them is refused with WriteBoundaryRefused unless the overlay can - // express it. + // ReadOnlyBases are the kustomization directories this folder renders but may + // not write, relative to spec.path, so they start with "../". Set only for + // KustomizeOverlay. An edit to a document under one of them is refused with + // WriteBoundaryRefused unless the overlay can express it. // +optional ReadOnlyBases []string `json:"readOnlyBases,omitempty"` - // ResolvedAtCommit is the commit at which this layout was first seen. It does not move while - // the layout stays the same, so a commit older than the branch head means the layout has not - // changed since. Empty until the branch has a commit. + // ResolvedAtCommit is the commit at which this layout was first seen. It does + // not move while the layout stays the same, so a commit older than the branch + // head means the layout has not changed since. Empty until the branch has a + // commit. // +optional ResolvedAtCommit string `json:"resolvedAtCommit,omitempty"` - // ResolvedAt is when this layout was first seen. Like resolvedAtCommit, it does not move while - // the layout stays the same. + // ResolvedAt is when this layout was first seen. Like resolvedAtCommit, it + // does not move while the layout stays the same. // +optional ResolvedAt *metav1.Time `json:"resolvedAt,omitempty"` } -// PlacementMode is how a GitTarget folder is written: as plain files, as a kustomize root, or as -// an overlay over a base it may not write to. +// PlacementMode is how a GitTarget folder is written: as plain files, as a +// kustomize root, or as an overlay over a base it may not write to. type PlacementMode string const ( @@ -396,20 +427,23 @@ const ( // retained. Pull-based: only as fresh as the last reconcile, which is fine for an observation and // is another reason this must not become a condition. -// GitTargetRetentionStatus is a bounded roll-up of what this GitTarget's prune policy kept. +// GitTargetRetentionStatus is a bounded roll-up of what this GitTarget's prune +// policy kept. type GitTargetRetentionStatus struct { - // Mode is the spec.prune.mode these counts were taken under, with the default applied. + // Mode is the spec.prune.mode these counts were taken under, with the default + // applied. // +optional // +kubebuilder:validation:Enum=Never;OnEvent;Always Mode PruneMode `json:"mode,omitempty"` - // RetainedDocuments is how many documents the prune policy kept that have no resource on the - // cluster. Zero means a resync found nothing to keep. An absent retention block means no - // resync has reported yet. + // RetainedDocuments is how many documents the prune policy kept that have no + // resource on the cluster. Zero means a resync found nothing to keep. An + // absent retention block means no resync has reported yet. RetainedDocuments int32 `json:"retainedDocuments"` - // LastChangedAt is when the count or the mode last changed. A resync that reports the same - // count leaves it alone, so it does not say when retention was last measured. + // LastChangedAt is when the count or the mode last changed. A resync that + // reports the same count leaves it alone, so it does not say when retention + // was last measured. // +optional LastChangedAt *metav1.Time `json:"lastChangedAt,omitempty"` } @@ -435,9 +469,9 @@ type GitTargetRetentionStatus struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// GitTarget mirrors objects from a source cluster into one folder of a Git branch. Its WatchRules -// and ClusterWatchRules select the objects, its KubeProvider reads them, and its GitBranch writes -// them. +// GitTarget mirrors objects from a source cluster into one folder of a Git +// branch. Its WatchRules and ClusterWatchRules select the objects, its +// KubeProvider reads them, and its GitBranch writes them. type GitTarget struct { metav1.TypeMeta `json:",inline"` @@ -497,7 +531,8 @@ func init() { SchemeBuilder.Register(&GitTarget{}, &GitTargetList{}) } -// RefusalAction is the action taken when a live write is refused. See GitTargetSpec.OnRefusal. +// RefusalAction is the action taken when a live write is refused. See +// GitTargetSpec.OnRefusal. type RefusalAction string const ( diff --git a/api/v1alpha3/kubeprovider_types.go b/api/v1alpha3/kubeprovider_types.go index bc50a258..d3b18903 100644 --- a/api/v1alpha3/kubeprovider_types.go +++ b/api/v1alpha3/kubeprovider_types.go @@ -13,62 +13,67 @@ import ( // KubeProviderSpec defines the desired state of KubeProvider. // -// Exactly one credential arm is set, and there is no third: an omitted, malformed or refused -// credential must never fall through to the operator's own identity, so that identity is an arm -// of its own (inCluster) rather than what an absent secretRef means. +// Exactly one credential arm is set, and there is no third: an omitted, +// malformed or refused credential must never fall through to the operator's +// own identity, so that identity is an arm of its own (inCluster) rather than +// what an absent secretRef means. // +kubebuilder:validation:XValidation:rule="has(self.secretRef) != has(self.inCluster)",message="set exactly one of spec.secretRef and spec.inCluster" // -// The in-cluster client is the operator's own and is shared by everything that uses it, so a -// per-provider throttle cannot apply to it. Refused rather than ignored. +// The in-cluster client is the operator's own and is shared by everything that +// uses it, so a per-provider throttle cannot apply to it. Refused rather than +// ignored. // +kubebuilder:validation:XValidation:rule="!has(self.inCluster) || !has(self.client)",message="spec.client is refused beside spec.inCluster: the operator's own client is not throttled per provider" // -// Which cluster a provider means must not silently change under the GitTargets mirroring through -// it, so the credential is immutable, including switching arms. A rotated kubeconfig is a change -// to the Secret, which stays allowed. +// Which cluster a provider means must not silently change under the GitTargets +// mirroring through it, so the credential is immutable, including switching +// arms. A rotated kubeconfig is a change to the Secret, which stays allowed. // +kubebuilder:validation:XValidation:rule="has(self.inCluster) == has(oldSelf.inCluster) && (!has(self.secretRef) || self.secretRef == oldSelf.secretRef)",message="spec.secretRef and spec.inCluster are immutable; delete and recreate the KubeProvider to point it at a different cluster" type KubeProviderSpec struct { - // SecretRef names the Secret, in this namespace, that holds a kubeconfig for the cluster. With - // no key, the kubeconfig is read from "value", then "value.yaml". A kubeconfig that uses exec - // auth, insecure-skip-tls-verify or credentials named by file path is refused with - // Validated=False unless the installation allows it. Set exactly one of secretRef and - // inCluster. Immutable: delete and recreate the KubeProvider to change it. + // SecretRef names the Secret, in this namespace, that holds a kubeconfig for + // the cluster. With no key, the kubeconfig is read from "value", then + // "value.yaml". A kubeconfig that uses exec auth, insecure-skip-tls-verify or + // credentials named by file path is refused with Validated=False unless the + // installation allows it. Set exactly one of secretRef and inCluster. + // Immutable: delete and recreate the KubeProvider to change it. // +optional // +kubebuilder:validation:XValidation:rule="size(self.name) > 0",message="spec.secretRef.name must not be empty" SecretRef *meta.SecretKeyReference `json:"secretRef,omitempty"` - // InCluster makes this KubeProvider use the operator's own identity in the cluster it runs - // in. It is honored only for a KubeProvider the installation lists in - // --in-cluster-kube-providers; any other reports Stalled=True, reason InClusterNotAuthorized, - // and is never used. Set exactly one of secretRef and inCluster. Immutable: delete and - // recreate the KubeProvider to change it. + // InCluster makes this KubeProvider use the operator's own identity in the + // cluster it runs in. It is honored only for a KubeProvider the installation + // lists in --in-cluster-kube-providers; any other reports Stalled=True, reason + // InClusterNotAuthorized, and is never used. Set exactly one of secretRef and + // inCluster. Immutable: delete and recreate the KubeProvider to change it. // +optional InCluster *KubeProviderInCluster `json:"inCluster,omitempty"` - // AllowedGitTargets says which GitTargets may mirror through this KubeProvider, and what. Each - // grant admits GitTargets from some namespaces, bounds which source namespaces their - // WatchRules may read (sourceNamespaces), and lists the cluster-scoped types their - // ClusterWatchRules may mirror (clusterResources). Omitted, it is one grant for this namespace, - // reading the source namespace of the same name and no cluster-scoped type. A written list is - // the whole policy, and [] admits nobody. Grants combine by union. + // AllowedGitTargets says which GitTargets may mirror through this + // KubeProvider, and what. Each grant admits GitTargets from some namespaces, + // bounds which source namespaces their WatchRules may read (sourceNamespaces), + // and lists the cluster-scoped types their ClusterWatchRules may mirror + // (clusterResources). Omitted, it is one grant for this namespace, reading the + // source namespace of the same name and no cluster-scoped type. A written list + // is the whole policy, and [] admits nobody. Grants combine by union. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 // +kubebuilder:default={{consumerNamespaces: {from: Same}, sourceNamespaces: {"{consumer.namespace}"}}} AllowedGitTargets []KubeProviderGitTargetGrant `json:"allowedGitTargets"` - // AllowedApplierClaims says which FluxClaims and ArgoCDClaims may hold an applier through this - // KubeProvider, and in which namespaces that applier may be (applierNamespaces). Being - // admitted to mirror does not admit to apply. Omitted, it is one grant for this namespace, - // holding appliers in the namespace of the same name. A written list is the whole policy, and - // [] admits nobody. Grants combine by union. + // AllowedApplierClaims says which FluxClaims and ArgoCDClaims may hold an + // applier through this KubeProvider, and in which namespaces that applier may + // be (applierNamespaces). Being admitted to mirror does not admit to apply. + // Omitted, it is one grant for this namespace, holding appliers in the + // namespace of the same name. A written list is the whole policy, and [] + // admits nobody. Grants combine by union. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 // +kubebuilder:default={{consumerNamespaces: {from: Same}, applierNamespaces: {"{consumer.namespace}"}}} AllowedApplierClaims []KubeProviderApplierClaimGrant `json:"allowedApplierClaims"` - // Client sets the request rate limits for this cluster. Omitted, they are 20 QPS with a burst - // of 30. Refused beside inCluster. + // Client sets the request rate limits for this cluster. Omitted, they are 20 + // QPS with a burst of 30. Refused beside inCluster. // +optional Client *KubeProviderClient `json:"client,omitempty"` @@ -76,42 +81,48 @@ type KubeProviderSpec struct { // flags are --author-attribution-*: the prefix groups a flat flag namespace, and a block on a // source-cluster object already supplies that scope. - // Attribution configures how commit authors are found from this cluster's audit events. + // Attribution configures how commit authors are found from this cluster's + // audit events. // +optional Attribution *KubeProviderAttribution `json:"attribution,omitempty"` } -// KubeProviderInCluster selects the operator's own identity. It has no fields: the arm being set -// is the whole statement. +// KubeProviderInCluster selects the operator's own identity. It has no fields: +// the arm being set is the whole statement. type KubeProviderInCluster struct{} // KubeProviderClient holds the per-cluster kube-client throttles. type KubeProviderClient struct { - // QPS is the sustained request rate to this cluster, in queries per second. Defaults to 20. + // QPS is the sustained request rate to this cluster, in queries per second. + // Defaults to 20. // +optional // +kubebuilder:validation:Minimum=1 QPS *int32 `json:"qps,omitempty"` - // Burst is how many requests to this cluster may exceed qps at once. Defaults to 30. + // Burst is how many requests to this cluster may exceed qps at once. Defaults + // to 30. // +optional // +kubebuilder:validation:Minimum=1 Burst *int32 `json:"burst,omitempty"` } -// KubeProviderAttribution holds the per-cluster author-attribution settings. It exists as a block -// so later per-cluster knobs (grace, mode) have a home beside auditRoute. +// KubeProviderAttribution holds the per-cluster author-attribution settings. +// It exists as a block so later per-cluster knobs (grace, mode) have a home +// beside auditRoute. type KubeProviderAttribution struct { - // AuditRoute is the route this cluster's audit events arrive on: the in the API - // server's audit webhook URL, /audit-webhook/. When several logical clusters share one - // webhook backend, it is instead the value of the audit-event annotation that - // --author-attribution-audit-route-annotation-key names. KubeProviders with the same route - // read the same cluster's attribution facts. + // AuditRoute is the route this cluster's audit events arrive on: the + // in the API server's audit webhook URL, /audit-webhook/. When several + // logical clusters share one webhook backend, it is instead the value of the + // audit-event annotation that --author-attribution-audit-route-annotation-key + // names. KubeProviders with the same route read the same cluster's attribution + // facts. // - // Empty means .. Set it when several KubeProviders in this namespace connect - // to one cluster, since an API server posts under one route. A route belongs to one namespace: - // the one it starts with (.) when a KubeProvider there carries it, and otherwise - // the namespace of the oldest KubeProvider carrying it. KubeProviders in another namespace that - // carry it report Stalled=True, reason AuditRouteConflict, and are not used. + // Empty means .. Set it when several KubeProviders in this + // namespace connect to one cluster, since an API server posts under one route. + // A route belongs to one namespace: the one it starts with (.) when + // a KubeProvider there carries it, and otherwise the namespace of the oldest + // KubeProvider carrying it. KubeProviders in another namespace that carry it + // report Stalled=True, reason AuditRouteConflict, and are not used. // +optional // +kubebuilder:validation:MaxLength=253 // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` @@ -132,23 +143,25 @@ type KubeProviderStatus struct { // is mutable, and without recording which route earned the latch, a provider repointed at a new // route would carry True forward and report facts it has never received there. - // AuditRoute is the route in use: spec.attribution.auditRoute, or . when that - // is empty. The AuditFactsReceived condition describes this route, and starts over when it - // changes. + // AuditRoute is the route in use: spec.attribution.auditRoute, or + // . when that is empty. The AuditFactsReceived condition + // describes this route, and starts over when it changes. // +optional AuditRoute string `json:"auditRoute,omitempty"` - // Conditions are the latest observations of this KubeProvider's state. Ready, Reconciling and - // Stalled follow kstatus; Stalled=True with reason InClusterNotAuthorized or - // AuditRouteConflict means the KubeProvider is not used at all. Validated says whether the - // credential is safe and readable, checked without connecting. + // Conditions are the latest observations of this KubeProvider's state. Ready, + // Reconciling and Stalled follow kstatus; Stalled=True with reason + // InClusterNotAuthorized or AuditRouteConflict means the KubeProvider is not + // used at all. Validated says whether the credential is safe and readable, + // checked without connecting. // - // AuditFactsReceived, which is not part of Ready, says whether an attribution fact has ever - // arrived on status.auditRoute. It is True (Received) from the first one on and never goes - // back. Until then it is Unknown, and the reason says where to look: TransportUnavailable (the - // fact store refuses writes), NoAuditDelivery (no audit events arrive at all), or RouteUnused - // (audit events arrive, but never on this route). It is absent when author attribution is - // off. + // AuditFactsReceived, which is not part of Ready, says whether an attribution + // fact has ever arrived on status.auditRoute. It is True (Received) from the + // first one on and never goes back. Until then it is Unknown, and the reason + // says where to look: TransportUnavailable (the fact store refuses writes), + // NoAuditDelivery (no audit events arrive at all), or RouteUnused (audit + // events arrive, but never on this route). It is absent when author + // attribution is off. // +optional // +listType=map // +listMapKey=type @@ -167,10 +180,11 @@ type KubeProviderStatus struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// KubeProvider is a connection to a Kubernetes cluster and the identity used there: a kubeconfig -// Secret in its namespace, or the operator's own identity where the installation allows it. -// GitTargets mirror from it and applier claims reach their applier through it, as its -// spec.allowedGitTargets and spec.allowedApplierClaims allow. +// KubeProvider is a connection to a Kubernetes cluster and the identity used +// there: a kubeconfig Secret in its namespace, or the operator's own identity +// where the installation allows it. GitTargets mirror from it and applier +// claims reach their applier through it, as its spec.allowedGitTargets and +// spec.allowedApplierClaims allow. type KubeProvider struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/prune_policy.go b/api/v1alpha3/prune_policy.go index 05dbf06c..21dabbc9 100644 --- a/api/v1alpha3/prune_policy.go +++ b/api/v1alpha3/prune_policy.go @@ -2,8 +2,8 @@ package v1alpha3 -// PruneMode enumerates which of the two deletion paths may remove a managed document from a -// GitTarget's folder. +// PruneMode enumerates which of the two deletion paths may remove a managed +// document from a GitTarget's folder. // // | source DELETE event | resync mark-and-sweep // Never | suppressed | suppressed @@ -26,15 +26,17 @@ const ( // An object rather than a bare enum so a later volume guard (maxDeletesPerCommit) can be a sibling // field; scalar-to-object would be breaking later, the nesting level costs nothing now. -// PrunePolicy declares which deletion paths may remove documents from a GitTarget's folder. +// PrunePolicy declares which deletion paths may remove documents from a +// GitTarget's folder. type PrunePolicy struct { // The kubebuilder default only applies to NEWLY created objects; Kubernetes does not // retro-default stored ones. Every reader must go through EffectivePruneMode, which maps an // absent policy and an empty mode to OnEvent. - // Mode decides which deletions are mirrored. "Never" removes nothing. "OnEvent" mirrors a - // delete observed on the source cluster, but a resync removes nothing. "Always" also lets a - // resync remove a document that has no resource on the cluster. Defaults to "OnEvent". + // Mode decides which deletions are mirrored. "Never" removes nothing. + // "OnEvent" mirrors a delete observed on the source cluster, but a resync + // removes nothing. "Always" also lets a resync remove a document that has no + // resource on the cluster. Defaults to "OnEvent". // +optional // +kubebuilder:validation:Enum=Never;OnEvent;Always // +kubebuilder:default=OnEvent diff --git a/api/v1alpha3/shared_types.go b/api/v1alpha3/shared_types.go index b6f81c26..2a359c17 100644 --- a/api/v1alpha3/shared_types.go +++ b/api/v1alpha3/shared_types.go @@ -2,14 +2,14 @@ package v1alpha3 -// StreamsStatus is a bounded roll-up of the stream-readiness state for the types a GitTarget -// tracks, or a WatchRule or ClusterWatchRule resolves. +// StreamsStatus is a bounded roll-up of the stream-readiness state for the +// types a GitTarget tracks, or a WatchRule or ClusterWatchRule resolves. type StreamsStatus struct { // It restates Ready and Total, which the API conventions would normally rule out. It exists // solely to feed the Streams printer column: a column can read one JSONPath, not format two. - // Summary is ready/total for display, such as "3/4". Read ready and total instead of parsing - // it. + // Summary is ready/total for display, such as "3/4". Read ready and total + // instead of parsing it. // +optional Summary string `json:"summary,omitempty"` diff --git a/api/v1alpha3/watchrule_types.go b/api/v1alpha3/watchrule_types.go index 744729df..49129114 100644 --- a/api/v1alpha3/watchrule_types.go +++ b/api/v1alpha3/watchrule_types.go @@ -11,39 +11,41 @@ import ( // WatchRuleSpec defines the desired state of WatchRule. type WatchRuleSpec struct { - // GitTargetRef names the GitTarget, in this namespace, that mirrors what this WatchRule - // selects. + // GitTargetRef names the GitTarget, in this namespace, that mirrors what this + // WatchRule selects. // +required // +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.gitTargetRef.name must not be empty" GitTargetRef meta.LocalObjectReference `json:"gitTargetRef"` - // Rules select the resources to mirror. A resource matching any rule is mirrored. Each rule - // names resource types, optionally narrowed by API group, version, source namespace and - // labels. The GitTarget's spec.prune.mode decides whether a removal on the cluster deletes the - // Git document. + // Rules select the resources to mirror. A resource matching any rule is + // mirrored. Each rule names resource types, optionally narrowed by API group, + // version, source namespace and labels. The GitTarget's spec.prune.mode + // decides whether a removal on the cluster deletes the Git document. // +required // +kubebuilder:validation:MinItems=1 Rules []ResourceRule `json:"rules"` } -// ResourceRule selects namespaced resources by type, and optionally by API group, version, source -// namespace and label. +// ResourceRule selects namespaced resources by type, and optionally by API +// group, version, source namespace and label. type ResourceRule struct { - // APIGroups are the API groups to match: "" is the core group, and "*" is every group, as in - // ["", "apps"] or ["*"]. Omitted, each resource name is looked up across the served groups and - // must be served by exactly one. + // APIGroups are the API groups to match: "" is the core group, and "*" is + // every group, as in ["", "apps"] or ["*"]. Omitted, each resource name is + // looked up across the served groups and must be served by exactly one. // +optional APIGroups []string `json:"apiGroups,omitempty"` - // APIVersions are the versions to match, or "*" for every served version. Omitted, each - // resource's preferred version is used. The built-in Git path has no version, so two versions - // of one object share a file unless the GitTarget's placement template includes {version}. + // APIVersions are the versions to match, or "*" for every served version. + // Omitted, each resource's preferred version is used. The built-in Git path + // has no version, so two versions of one object share a file unless the + // GitTarget's placement template includes {version}. // +optional APIVersions []string `json:"apiVersions,omitempty"` - // Resources are the resource types to match, by plural name ("configmaps", "deployments"), or - // "*" for every type. Matching is exact and case-insensitive; partial wildcards such as "pod*" - // match nothing, and subresources ("pods/log") are rejected. For a custom resource whose name + // Resources are the resource types to match, by plural name ("configmaps", + // "deployments"), or "*" for every type. Matching is exact and + // case-insensitive; partial wildcards such as "pod*" match nothing, and + // subresources ("pods/log") are rejected. For a custom resource whose name // more than one group serves, set apiGroups. // +required // +kubebuilder:validation:MinItems=1 @@ -78,15 +80,15 @@ type ResourceRule struct { // credential could read (see CollectionKey in internal/types/collection.go). A target carrying // both therefore runs two streams over overlapping objects, and that is correct. - // SourceNamespace is the source-cluster namespace this rule reads: omitted for this - // WatchRule's own namespace, a name for another one, or "*" for every namespace the - // KubeProvider's credential can read. Git placement follows each object's own namespace, not - // this field. + // SourceNamespace is the source-cluster namespace this rule reads: omitted for + // this WatchRule's own namespace, a name for another one, or "*" for every + // namespace the KubeProvider's credential can read. Git placement follows each + // object's own namespace, not this field. // - // The KubeProvider's spec.allowedGitTargets must allow the namespace, through the - // sourceNamespaces of a grant admitting the GitTarget; "*" needs "*" there. One rule outside - // the grants refuses the whole WatchRule (SourceNamespaceNotAllowed), checked when it - // reconciles. + // The KubeProvider's spec.allowedGitTargets must allow the namespace, through + // the sourceNamespaces of a grant admitting the GitTarget; "*" needs "*" + // there. One rule outside the grants refuses the whole WatchRule + // (SourceNamespaceNotAllowed), checked when it reconciles. // +optional // +kubebuilder:validation:MaxLength=63 // +kubebuilder:validation:Pattern=`^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)$` @@ -138,9 +140,10 @@ type WatchRuleStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are the latest observations of this WatchRule's state. Ready, Reconciling and - // Stalled follow kstatus; ResourcesResolved, SourceNamespaceAuthorized and StreamsRunning say - // which check holds the rule back. + // Conditions are the latest observations of this WatchRule's state. Ready, + // Reconciling and Stalled follow kstatus; ResourcesResolved, + // SourceNamespaceAuthorized and StreamsRunning say which check holds the rule + // back. // +optional // +listType=map // +listMapKey=type @@ -169,9 +172,10 @@ type WatchRuleStatus struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// WatchRule selects namespaced resources for a GitTarget in its namespace to mirror, by type, -// API group, version, source namespace and label. Each rule reads one source namespace: this -// WatchRule's own by default. Use a ClusterWatchRule for cluster-scoped types. +// WatchRule selects namespaced resources for a GitTarget in its namespace to +// mirror, by type, API group, version, source namespace and label. Each rule +// reads one source namespace: this WatchRule's own by default. Use a +// ClusterWatchRule for cluster-scoped types. type WatchRule struct { metav1.TypeMeta `json:",inline"` diff --git a/config/crd/bases/configbutler.ai_argocdclaims.yaml b/config/crd/bases/configbutler.ai_argocdclaims.yaml index e96ba3f2..9b4a3372 100644 --- a/config/crd/bases/configbutler.ai_argocdclaims.yaml +++ b/config/crd/bases/configbutler.ai_argocdclaims.yaml @@ -57,11 +57,12 @@ spec: schema: openAPIV3Schema: description: |- - ArgoCDClaim hands control of an Argo CD Application to GitOps Reverser. The Application's - automated sync stays off, and GitOps Reverser starts each sync, always after the edits pending - on the branch are published: after a commit it did not push, a refused or reverted edit, or an - object the API created, and once at startup or when a GitTarget first names it. GitTargets name - it with spec.applierClaimRef. + ArgoCDClaim hands control of an Argo CD Application to GitOps Reverser. The + Application's automated sync stays off, and GitOps Reverser starts each + sync, always after the edits pending on the branch are published: after a + commit it did not push, a refused or reverted edit, or an object the API + created, and once at startup or when a GitTarget first names it. GitTargets + name it with spec.applierClaimRef. properties: apiVersion: description: |- @@ -85,18 +86,21 @@ spec: properties: applicationRef: description: |- - ApplicationRef names the Argo CD Application that syncs the folders of the GitTargets naming - this ArgoCDClaim. It must have a single spec.source whose targetRevision names a branch. + ApplicationRef names the Argo CD Application that syncs the folders of the + GitTargets naming this ArgoCDClaim. It must have a single spec.source whose + targetRevision names a branch. - The ArgoCDClaim keeps its automated sync off by applying spec.syncPolicy.automated.enabled: - false, switches it off again when somebody switches it on, and gives the switch up when it - lets go; automated.prune still applies to the syncs GitOps Reverser starts. An Application - that an ApplicationSet generates, or whose switch another field manager applies, must have - automated sync off already. Each sync is pinned to the commit GitOps Reverser checked, and a - running operation is never replaced. + The ArgoCDClaim keeps its automated sync off by applying + spec.syncPolicy.automated.enabled: false, switches it off again when + somebody switches it on, and gives the switch up when it lets go; + automated.prune still applies to the syncs GitOps Reverser starts. An + Application that an ApplicationSet generates, or whose switch another field + manager applies, must have automated sync off already. Each sync is pinned + to the commit GitOps Reverser checked, and a running operation is never + replaced. - An omitted namespace means this ArgoCDClaim's namespace. Immutable: delete and recreate the - ArgoCDClaim to change it. + An omitted namespace means this ArgoCDClaim's namespace. Immutable: delete + and recreate the ArgoCDClaim to change it. properties: name: description: Name of the referent. @@ -116,11 +120,13 @@ spec: rule: self.name != '' kubeProviderRef: description: |- - KubeProviderRef names the KubeProvider of the cluster that runs the Application, such as a - management cluster running Argo CD. An omitted namespace means this ArgoCDClaim's namespace. - The KubeProvider's spec.allowedApplierClaims must admit this namespace, with the - Application's namespace in applierNamespaces, and its identity needs get, update and patch on - the Application. Immutable: delete and recreate the ArgoCDClaim to change it. + KubeProviderRef names the KubeProvider of the cluster that runs the + Application, such as a management cluster running Argo CD. An omitted + namespace means this ArgoCDClaim's namespace. The KubeProvider's + spec.allowedApplierClaims must admit this namespace, with the Application's + namespace in applierNamespaces, and its identity needs get, update and patch + on the Application. Immutable: delete and recreate the ArgoCDClaim to change + it. properties: name: description: Name of the referent. @@ -147,15 +153,16 @@ spec: properties: conditions: description: |- - Conditions are the latest observations of this ArgoCDClaim's state. Ready, Reconciling and - Stalled follow kstatus. + Conditions are the latest observations of this ArgoCDClaim's state. Ready, + Reconciling and Stalled follow kstatus. - Configured is True when an eligible GitTarget names this ArgoCDClaim, and the Application - exists, syncs a single source, has automated sync off or a switch this ArgoCDClaim may hold, - may be written, and no other ArgoCDClaim holds it. An eligible GitTarget is in this - namespace, is admitted by its KubeProviders, and writes the branch the Application syncs. Of - several ArgoCDClaims for one Application, the one holding it already holds it, then the - oldest; the others are Configured=False, reason HeldByAnotherClaim. + Configured is True when an eligible GitTarget names this ArgoCDClaim, and + the Application exists, syncs a single source, has automated sync off or a + switch this ArgoCDClaim may hold, may be written, and no other ArgoCDClaim + holds it. An eligible GitTarget is in this namespace, is admitted by its + KubeProviders, and writes the branch the Application syncs. Of several + ArgoCDClaims for one Application, the one holding it already holds it, then + the oldest; the others are Configured=False, reason HeldByAnotherClaim. InControl says whether GitOps Reverser decides when the Application syncs: @@ -241,33 +248,36 @@ spec: type: integer operation: description: |- - Operation is the sync GitOps Reverser started on the Application: the one in flight, or the - last one. Absent until the first. + Operation is the sync GitOps Reverser started on the Application: the one in + flight, or the last one. Absent until the first. properties: appliedRevision: description: |- - AppliedRevision is the revision the applier reports applying when the operation completes. - It can differ from commit when the branch moved between the check and the apply. + AppliedRevision is the revision the applier reports applying when the + operation completes. It can differ from commit when the branch moved between + the check and the apply. type: string commit: description: |- - Commit is the commit the applier was asked to apply. Empty when the remote did not carry the - branch. + Commit is the commit the applier was asked to apply. Empty when the remote + did not carry the branch. type: string finishedAt: description: FinishedAt is when the operation was seen to complete. format: date-time type: string message: - description: Message says why a Failed operation failed, or why - an Unresolved one is overdue. + description: |- + Message says why a Failed operation failed, or why an Unresolved one is + overdue. type: string phase: description: |- - Phase is where the operation stands. Requested, Running, Settling (Flux only: applied, and - the Kustomization not yet suspended again) and Unresolved (overdue, still followed) are in - flight: the applier can still revert an edit to an object it applies, and is asked for - nothing else. Succeeded and Failed are complete; Failed may have applied part of the commit. + Phase is where the operation stands. Requested, Running, Settling (Flux + only: applied, and the Kustomization not yet suspended again) and Unresolved + (overdue, still followed) are in flight: the applier can still revert an + edit to an object it applies, and is asked for nothing else. Succeeded and + Failed are complete; Failed may have applied part of the commit. enum: - Requested - Running @@ -278,11 +288,12 @@ spec: type: string reason: description: |- - Reason is why the apply was asked for: ForeignCommit (a commit GitOps Reverser did not push), - Refusal (a refused edit under spec.onRefusal: "Reapply"), Repair (an earlier apply reverted - a newer API edit), or CatchUp (adopting objects the API created, and once at startup or when - a GitTarget first names the claim). With several GitTargets on the claim, it is the strongest - of their reasons. + Reason is why the apply was asked for: ForeignCommit (a commit GitOps + Reverser did not push), Refusal (a refused edit under spec.onRefusal: + "Reapply"), Repair (an earlier apply reverted a newer API edit), or CatchUp + (adopting objects the API created, and once at startup or when a GitTarget + first names the claim). With several GitTargets on the claim, it is the + strongest of their reasons. enum: - ForeignCommit - Refusal @@ -295,14 +306,15 @@ spec: type: string suspendToken: description: |- - SuspendToken is the reconcile.fluxcd.io/requestedAt value of the suspend that follows the - apply. Once the Kustomization reports it handled, nothing started before the suspend is still - running. Flux only. + SuspendToken is the reconcile.fluxcd.io/requestedAt value of the suspend + that follows the apply. Once the Kustomization reports it handled, nothing + started before the suspend is still running. Flux only. type: string token: description: |- - Token identifies the request in the applier's own status: the reconcile.fluxcd.io/requestedAt - value on a Flux object, or the "gitops-reverser request" entry in an Argo CD operation's info. + Token identifies the request in the applier's own status: the + reconcile.fluxcd.io/requestedAt value on a Flux object, or the + "gitops-reverser request" entry in an Argo CD operation's info. type: string required: - phase @@ -310,10 +322,11 @@ spec: type: object previousSuspendState: description: |- - PreviousSuspendState is how the Application stood before this ArgoCDClaim first switched its - automated sync off, and what handing it back restores: NotSuspended (automated sync on) or - Suspended (off). It is cleared once the Application is handed back. Absent, handing it back - leaves what the Application's own spec says. A switch that an ApplicationSet or another + PreviousSuspendState is how the Application stood before this ArgoCDClaim + first switched its automated sync off, and what handing it back restores: + NotSuspended (automated sync on) or Suspended (off). It is cleared once the + Application is handed back. Absent, handing it back leaves what the + Application's own spec says. A switch that an ApplicationSet or another field manager owns keeps that owner's value either way. enum: - Suspended @@ -321,23 +334,23 @@ spec: type: string repairPending: description: |- - RepairPending is true while a Repair sync is pending: a sync ran a commit older than an edit - made through the API and reverted it on the cluster. Git holds the edit, and the cluster runs - the older value until the next sync. + RepairPending is true while a Repair sync is pending: a sync ran a commit + older than an edit made through the API and reverted it on the cluster. Git + holds the edit, and the cluster runs the older value until the next sync. type: boolean source: description: |- - Source is the Application's spec.source, as last read. It is published while an eligible - GitTarget names this ArgoCDClaim, and that GitTarget must write the repository it syncs, on - the branch its targetRevision names. + Source is the Application's spec.source, as last read. It is published while + an eligible GitTarget names this ArgoCDClaim, and that GitTarget must write + the repository it syncs, on the branch its targetRevision names. properties: repoURL: description: RepoURL is the Application's spec.source.repoURL. type: string targetRevision: description: |- - TargetRevision is the Application's spec.source.targetRevision, as written: a branch name, - or refs/heads/ and the name. Empty means Argo CD's HEAD. + TargetRevision is the Application's spec.source.targetRevision, as written: + a branch name, or refs/heads/ and the name. Empty means Argo CD's HEAD. type: string type: object type: object diff --git a/config/crd/bases/configbutler.ai_clusterwatchrules.yaml b/config/crd/bases/configbutler.ai_clusterwatchrules.yaml index 051c8f68..5b0882e9 100644 --- a/config/crd/bases/configbutler.ai_clusterwatchrules.yaml +++ b/config/crd/bases/configbutler.ai_clusterwatchrules.yaml @@ -46,9 +46,10 @@ spec: schema: openAPIV3Schema: description: |- - ClusterWatchRule selects cluster-scoped resources, such as Nodes, ClusterRoles and CRDs, for a - GitTarget to mirror. The KubeProvider's grant admitting that GitTarget must list each type in - clusterResources. Use a WatchRule for namespaced resources. + ClusterWatchRule selects cluster-scoped resources, such as Nodes, + ClusterRoles and CRDs, for a GitTarget to mirror. The KubeProvider's grant + admitting that GitTarget must list each type in clusterResources. Use a + WatchRule for namespaced resources. properties: apiVersion: description: |- @@ -72,8 +73,9 @@ spec: properties: gitTargetRef: description: |- - GitTargetRef names the GitTarget that mirrors what this ClusterWatchRule selects. The - namespace is required, because a ClusterWatchRule has none to default to. + GitTargetRef names the GitTarget that mirrors what this ClusterWatchRule + selects. The namespace is required, because a ClusterWatchRule has none to + default to. properties: name: description: Name of the referent. @@ -93,26 +95,27 @@ spec: rule: has(self.namespace) && self.namespace != '' rules: description: |- - Rules select the cluster-scoped resources to mirror. A resource matching any rule is - mirrored. A rule that resolves to no cluster-scoped type mirrors nothing; use a WatchRule for - namespaced resources. + Rules select the cluster-scoped resources to mirror. A resource matching any + rule is mirrored. A rule that resolves to no cluster-scoped type mirrors + nothing; use a WatchRule for namespaced resources. items: description: |- - ClusterResourceRule selects cluster-scoped resources by type, and optionally by API group, - version and label. + ClusterResourceRule selects cluster-scoped resources by type, and optionally + by API group, version and label. properties: apiGroups: description: |- - APIGroups are the API groups to match: "" is the core group, and "*" is every group, as in - ["rbac.authorization.k8s.io"] or ["*"]. Omitted, each resource name is looked up across the - served groups and must be served by exactly one. + APIGroups are the API groups to match: "" is the core group, and "*" is + every group, as in ["rbac.authorization.k8s.io"] or ["*"]. Omitted, each + resource name is looked up across the served groups and must be served by + exactly one. items: type: string type: array apiVersions: description: |- - APIVersions are the versions to match, or "*" for every served version. Omitted, each - resource's preferred version is used. + APIVersions are the versions to match, or "*" for every served version. + Omitted, each resource's preferred version is used. items: type: string type: array @@ -177,9 +180,10 @@ spec: x-kubernetes-map-type: atomic resources: description: |- - Resources are the resource types to match, by plural name ("nodes", "clusterroles"), or "*" - for every type. Matching is exact and case-insensitive; partial wildcards such as "cluster*" - match nothing, and subresources ("nodes/status") are rejected. + Resources are the resource types to match, by plural name ("nodes", + "clusterroles"), or "*" for every type. Matching is exact and + case-insensitive; partial wildcards such as "cluster*" match nothing, and + subresources ("nodes/status") are rejected. items: minLength: 1 pattern: ^[^/]*$ @@ -200,9 +204,9 @@ spec: properties: conditions: description: |- - Conditions are the latest observations of this ClusterWatchRule's state. Ready, Reconciling - and Stalled follow kstatus; ResourcesResolved and StreamsRunning say which check holds the - rule back. + Conditions are the latest observations of this ClusterWatchRule's state. + Ready, Reconciling and Stalled follow kstatus; ResourcesResolved and + StreamsRunning say which check holds the rule back. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -293,8 +297,8 @@ spec: type: integer summary: description: |- - Summary is ready/total for display, such as "3/4". Read ready and total instead of parsing - it. + Summary is ready/total for display, such as "3/4". Read ready and total + instead of parsing it. type: string total: description: Total is how many types are watched. diff --git a/config/crd/bases/configbutler.ai_commitrequests.yaml b/config/crd/bases/configbutler.ai_commitrequests.yaml index f0dbe161..90b56c58 100644 --- a/config/crd/bases/configbutler.ai_commitrequests.yaml +++ b/config/crd/bases/configbutler.ai_commitrequests.yaml @@ -50,9 +50,10 @@ spec: schema: openAPIV3Schema: description: |- - CommitRequest asks for a save: it attaches to its author's current or next commit window on a - GitTarget, closes that window on its own timers, and gives the commit its message. Its spec is - immutable, and status.commit reports the commit once it is on the remote. + CommitRequest asks for a save: it attaches to its author's current or next + commit window on a GitTarget, closes that window on its own timers, and + gives the commit its message. Its spec is immutable, and status.commit + reports the commit once it is on the remote. properties: apiVersion: description: |- @@ -72,13 +73,14 @@ spec: metadata: type: object spec: - description: spec defines the desired state of CommitRequest. It is immutable - after creation. + description: |- + spec defines the desired state of CommitRequest. It is immutable after + creation. properties: gitTargetRef: description: |- - GitTargetRef names the GitTarget, in this namespace, whose commit window this request - attaches to. + GitTargetRef names the GitTarget, in this namespace, whose commit window + this request attaches to. properties: name: description: Name of the referent. @@ -111,12 +113,13 @@ spec: whenNothingToCommit: default: Resolve description: |- - WhenNothingToCommit decides what happens when the request ends with nothing to commit: - attachTimeout ran out with no window, or the window closed without changing Git. "Resolve" - finishes without a commit; "CommitEmpty" records spec.message in an empty commit. The Ready - reason says which case happened. A window that belonged to another author never gets an - empty commit, and a request on a GitTarget that may not be written (suspended, or render - fidelity not established) fails either way. Defaults to "Resolve". + WhenNothingToCommit decides what happens when the request ends with nothing + to commit: attachTimeout ran out with no window, or the window closed + without changing Git. "Resolve" finishes without a commit; "CommitEmpty" + records spec.message in an empty commit. The Ready reason says which case + happened. A window that belonged to another author never gets an empty + commit, and a request on a GitTarget that may not be written (suspended, or + render fidelity not established) fails either way. Defaults to "Resolve". enum: - Resolve - CommitEmpty @@ -127,18 +130,20 @@ spec: attachTimeout: 2s maxDuration: 2s description: |- - Window says which commit window this request attaches to, how long it waits for one, and - the timers that close the window once it has. They replace the GitTarget's timers for that - window. A request never opens a window: only a write does. Omitted, the request attaches to - the author's current or next window within 2s and closes it 2s later. + Window says which commit window this request attaches to, how long it waits + for one, and the timers that close the window once it has. They replace the + GitTarget's timers for that window. A request never opens a window: only a + write does. Omitted, the request attaches to the author's current or next + window within 2s and closes it 2s later. properties: attach: default: CurrentOrNext description: |- - Attach selects the window. "CurrentOrNext" attaches to the author's open window, or to the - next one a write opens. "Next" closes the author's open window under its own message first, - and attaches to the next one; it separates earlier writes from later ones, but cannot prove - a write was made after the request. Defaults to "CurrentOrNext". + Attach selects the window. "CurrentOrNext" attaches to the author's open + window, or to the next one a write opens. "Next" closes the author's open + window under its own message first, and attaches to the next one; it + separates earlier writes from later ones, but cannot prove a write was made + after the request. Defaults to "CurrentOrNext". enum: - CurrentOrNext - Next @@ -146,10 +151,11 @@ spec: attachTimeout: default: 2s description: |- - AttachTimeout is how long the request waits for a window, as a Go duration, from when GitOps - Reverser picks the request up. "0s" attaches only to a window already open. A request whose - wait ran out never takes a later window. At most "5m". Defaults to "2s", which covers a - write's wait for its audit event. + AttachTimeout is how long the request waits for a window, as a Go duration, + from when GitOps Reverser picks the request up. "0s" attaches only to a + window already open. A request whose wait ran out never takes a later + window. At most "5m". Defaults to "2s", which covers a write's wait for its + audit event. pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$ type: string x-kubernetes-validations: @@ -157,8 +163,9 @@ spec: rule: duration(self) <= duration('5m') idleTimeout: description: |- - IdleTimeout closes the attached window this long after its last write, as a Go duration. - Omitted, only maxDuration closes it. At most "5m", and not more than maxDuration. + IdleTimeout closes the attached window this long after its last write, as a + Go duration. Omitted, only maxDuration closes it. At most "5m", and not more + than maxDuration. pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$ type: string x-kubernetes-validations: @@ -167,9 +174,9 @@ spec: maxDuration: default: 2s description: |- - MaxDuration closes the attached window this long after the request attached, however many - writes keep arriving, as a Go duration. "0s" commits right after attaching. At most "5m". - Defaults to "2s". + MaxDuration closes the attached window this long after the request attached, + however many writes keep arriving, as a Go duration. "0s" commits right + after attaching. At most "5m". Defaults to "2s". pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$ type: string x-kubernetes-validations: @@ -214,11 +221,12 @@ spec: type: string conditions: description: |- - Conditions are the latest observations of this CommitRequest's state. Ready, Reconciling - and Stalled follow kstatus: Ready is True once the request ended without error, with a - pushed commit or with nothing to save. Pushed is True once the commit is on the remote. - AuthorAttributed says whether the request's creator, recorded at admission, names the - commit author; False is not a failure and does not affect Ready. + Conditions are the latest observations of this CommitRequest's state. Ready, + Reconciling and Stalled follow kstatus: Ready is True once the request ended + without error, with a pushed commit or with nothing to save. Pushed is True + once the commit is on the remote. AuthorAttributed says whether the + request's creator, recorded at admission, names the commit author; False is + not a failure and does not affect Ready. items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/bases/configbutler.ai_fluxclaims.yaml b/config/crd/bases/configbutler.ai_fluxclaims.yaml index ce266f85..b7cfc649 100644 --- a/config/crd/bases/configbutler.ai_fluxclaims.yaml +++ b/config/crd/bases/configbutler.ai_fluxclaims.yaml @@ -57,11 +57,12 @@ spec: schema: openAPIV3Schema: description: |- - FluxClaim hands control of a Flux Kustomization to GitOps Reverser. The Kustomization stays - suspended, and GitOps Reverser resumes it for each apply it starts, always after the edits - pending on the branch are published: after a commit it did not push, a refused or reverted - edit, or an object the API created, and once at startup or when a GitTarget first names it. - GitTargets name it with spec.applierClaimRef. + FluxClaim hands control of a Flux Kustomization to GitOps Reverser. The + Kustomization stays suspended, and GitOps Reverser resumes it for each apply + it starts, always after the edits pending on the branch are published: after + a commit it did not push, a refused or reverted edit, or an object the API + created, and once at startup or when a GitTarget first names it. GitTargets + name it with spec.applierClaimRef. properties: apiVersion: description: |- @@ -85,11 +86,13 @@ spec: properties: kubeProviderRef: description: |- - KubeProviderRef names the KubeProvider of the cluster that runs the Kustomization, such as a - management cluster running Flux. An omitted namespace means this FluxClaim's namespace. The - KubeProvider's spec.allowedApplierClaims must admit this namespace, with the Kustomization's - namespace in applierNamespaces, and its identity needs get and patch on the Kustomization and - its GitRepository. Immutable: delete and recreate the FluxClaim to change it. + KubeProviderRef names the KubeProvider of the cluster that runs the + Kustomization, such as a management cluster running Flux. An omitted + namespace means this FluxClaim's namespace. The KubeProvider's + spec.allowedApplierClaims must admit this namespace, with the + Kustomization's namespace in applierNamespaces, and its identity needs get + and patch on the Kustomization and its GitRepository. Immutable: delete and + recreate the FluxClaim to change it. properties: name: description: Name of the referent. @@ -109,10 +112,11 @@ spec: rule: self == oldSelf kustomizationRef: description: |- - KustomizationRef names the Flux Kustomization that applies the folders of the GitTargets - naming this FluxClaim. Its spec.sourceRef must be a GitRepository that follows a branch. An - omitted namespace means this FluxClaim's namespace. Immutable: delete and recreate the - FluxClaim to change it. + KustomizationRef names the Flux Kustomization that applies the folders of + the GitTargets naming this FluxClaim. Its spec.sourceRef must be a + GitRepository that follows a branch. An omitted namespace means this + FluxClaim's namespace. Immutable: delete and recreate the FluxClaim to + change it. properties: name: description: Name of the referent. @@ -139,15 +143,16 @@ spec: properties: conditions: description: |- - Conditions are the latest observations of this FluxClaim's state. Ready, Reconciling and - Stalled follow kstatus. + Conditions are the latest observations of this FluxClaim's state. Ready, + Reconciling and Stalled follow kstatus. - Configured is True when an eligible GitTarget names this FluxClaim, and the Kustomization - exists, applies a GitRepository that follows a branch, may be patched, and no other FluxClaim - holds it. An eligible GitTarget is in this namespace, is admitted by its KubeProviders, and - writes the branch the GitRepository fetches. Of several FluxClaims for one Kustomization, the - one with an apply in flight holds it, then the one holding it already, then the oldest; the - others are Configured=False, reason HeldByAnotherClaim. + Configured is True when an eligible GitTarget names this FluxClaim, and the + Kustomization exists, applies a GitRepository that follows a branch, may be + patched, and no other FluxClaim holds it. An eligible GitTarget is in this + namespace, is admitted by its KubeProviders, and writes the branch the + GitRepository fetches. Of several FluxClaims for one Kustomization, the one + with an apply in flight holds it, then the one holding it already, then the + oldest; the others are Configured=False, reason HeldByAnotherClaim. InControl says whether GitOps Reverser decides when the Kustomization applies: @@ -230,33 +235,36 @@ spec: type: integer operation: description: |- - Operation is the apply GitOps Reverser started on the Kustomization: the one in flight, or - the last one. Absent until the first. + Operation is the apply GitOps Reverser started on the Kustomization: the one + in flight, or the last one. Absent until the first. properties: appliedRevision: description: |- - AppliedRevision is the revision the applier reports applying when the operation completes. - It can differ from commit when the branch moved between the check and the apply. + AppliedRevision is the revision the applier reports applying when the + operation completes. It can differ from commit when the branch moved between + the check and the apply. type: string commit: description: |- - Commit is the commit the applier was asked to apply. Empty when the remote did not carry the - branch. + Commit is the commit the applier was asked to apply. Empty when the remote + did not carry the branch. type: string finishedAt: description: FinishedAt is when the operation was seen to complete. format: date-time type: string message: - description: Message says why a Failed operation failed, or why - an Unresolved one is overdue. + description: |- + Message says why a Failed operation failed, or why an Unresolved one is + overdue. type: string phase: description: |- - Phase is where the operation stands. Requested, Running, Settling (Flux only: applied, and - the Kustomization not yet suspended again) and Unresolved (overdue, still followed) are in - flight: the applier can still revert an edit to an object it applies, and is asked for - nothing else. Succeeded and Failed are complete; Failed may have applied part of the commit. + Phase is where the operation stands. Requested, Running, Settling (Flux + only: applied, and the Kustomization not yet suspended again) and Unresolved + (overdue, still followed) are in flight: the applier can still revert an + edit to an object it applies, and is asked for nothing else. Succeeded and + Failed are complete; Failed may have applied part of the commit. enum: - Requested - Running @@ -267,11 +275,12 @@ spec: type: string reason: description: |- - Reason is why the apply was asked for: ForeignCommit (a commit GitOps Reverser did not push), - Refusal (a refused edit under spec.onRefusal: "Reapply"), Repair (an earlier apply reverted - a newer API edit), or CatchUp (adopting objects the API created, and once at startup or when - a GitTarget first names the claim). With several GitTargets on the claim, it is the strongest - of their reasons. + Reason is why the apply was asked for: ForeignCommit (a commit GitOps + Reverser did not push), Refusal (a refused edit under spec.onRefusal: + "Reapply"), Repair (an earlier apply reverted a newer API edit), or CatchUp + (adopting objects the API created, and once at startup or when a GitTarget + first names the claim). With several GitTargets on the claim, it is the + strongest of their reasons. enum: - ForeignCommit - Refusal @@ -284,14 +293,15 @@ spec: type: string suspendToken: description: |- - SuspendToken is the reconcile.fluxcd.io/requestedAt value of the suspend that follows the - apply. Once the Kustomization reports it handled, nothing started before the suspend is still - running. Flux only. + SuspendToken is the reconcile.fluxcd.io/requestedAt value of the suspend + that follows the apply. Once the Kustomization reports it handled, nothing + started before the suspend is still running. Flux only. type: string token: description: |- - Token identifies the request in the applier's own status: the reconcile.fluxcd.io/requestedAt - value on a Flux object, or the "gitops-reverser request" entry in an Argo CD operation's info. + Token identifies the request in the applier's own status: the + reconcile.fluxcd.io/requestedAt value on a Flux object, or the + "gitops-reverser request" entry in an Argo CD operation's info. type: string required: - phase @@ -299,31 +309,33 @@ spec: type: object previousSuspendState: description: |- - PreviousSuspendState is how the Kustomization stood before this FluxClaim first suspended - it, and what handing it back restores: one that was Suspended stays suspended, one that was - NotSuspended is resumed. It is cleared once the Kustomization is handed back. Absent, - handing it back resumes the Kustomization. + PreviousSuspendState is how the Kustomization stood before this FluxClaim + first suspended it, and what handing it back restores: one that was + Suspended stays suspended, one that was NotSuspended is resumed. It is + cleared once the Kustomization is handed back. Absent, handing it back + resumes the Kustomization. enum: - Suspended - NotSuspended type: string repairPending: description: |- - RepairPending is true while a Repair apply is pending: an apply ran a commit older than an - edit made through the API and reverted it on the cluster. Git holds the edit, and the cluster - runs the older value until the next apply. + RepairPending is true while a Repair apply is pending: an apply ran a commit + older than an edit made through the API and reverted it on the cluster. Git + holds the edit, and the cluster runs the older value until the next apply. type: boolean source: description: |- - Source is the GitRepository the Kustomization applies, as last read. It is published while an - eligible GitTarget names this FluxClaim, and that GitTarget must write the repository and - branch it fetches. + Source is the GitRepository the Kustomization applies, as last read. It is + published while an eligible GitTarget names this FluxClaim, and that + GitTarget must write the repository and branch it fetches. properties: alsoAppliedBy: description: |- - AlsoAppliedBy lists the other objects that apply this GitRepository and that no FluxClaim - holds: Kustomizations, and HelmReleases whose chart comes from it. Each applies a new commit - as soon as Flux fetches it. Information only; it never affects Ready. + AlsoAppliedBy lists the other objects that apply this GitRepository and that + no FluxClaim holds: Kustomizations, and HelmReleases whose chart comes from + it. Each applies a new commit as soon as Flux fetches it. Information only; + it never affects Ready. items: description: |- NamespacedObjectKindReference contains enough information to locate the typed referenced Kubernetes resource object @@ -353,8 +365,9 @@ spec: description: Branch is the GitRepository's spec.ref.branch. type: string gitRepository: - description: GitRepository is the Kustomization's spec.sourceRef, - with its namespace filled in. + description: |- + GitRepository is the Kustomization's spec.sourceRef, with its namespace + filled in. properties: name: description: Name of the referent. diff --git a/config/crd/bases/configbutler.ai_gitbranches.yaml b/config/crd/bases/configbutler.ai_gitbranches.yaml index 982d8fb0..3a5a860b 100644 --- a/config/crd/bases/configbutler.ai_gitbranches.yaml +++ b/config/crd/bases/configbutler.ai_gitbranches.yaml @@ -57,9 +57,9 @@ spec: schema: openAPIV3Schema: description: |- - GitBranch is one branch of a Git repository, written with a GitProvider's credential. Every - GitTarget that references it writes through it, and only one GitBranch may hold a repository - branch at a time. + GitBranch is one branch of a Git repository, written with a GitProvider's + credential. Every GitTarget that references it writes through it, and only + one GitBranch may hold a repository branch at a time. properties: apiVersion: description: |- @@ -86,24 +86,25 @@ spec: - consumerNamespaces: from: Same description: |- - AllowedGitTargets says which GitTargets may write through this GitBranch. Omitted, it is one - grant for this namespace (consumerNamespaces: {from: Same}). A written list is the whole - policy, so it admits this namespace only if a grant says so, and [] admits nobody. Grants - combine by union. + AllowedGitTargets says which GitTargets may write through this GitBranch. + Omitted, it is one grant for this namespace (consumerNamespaces: {from: + Same}). A written list is the whole policy, so it admits this namespace only + if a grant says so, and [] admits nobody. Grants combine by union. items: - description: GitBranchGitTargetGrant admits the GitTargets of some - namespaces to write through a GitBranch. + description: |- + GitBranchGitTargetGrant admits the GitTargets of some namespaces to write + through a GitBranch. properties: consumerNamespaces: description: |- - ConsumerNamespaces are the namespaces whose GitTargets this grant admits. from: "Same" is - this GitBranch's namespace. + ConsumerNamespaces are the namespaces whose GitTargets this grant admits. + from: "Same" is this GitBranch's namespace. properties: from: description: |- - From is where this grant admits consumers from: "Same" (the namespace of the object that - holds the grant), "Names" (the namespaces in names), "Selector" (the namespaces whose labels - match selector) or "All" (every namespace). + From is where this grant admits consumers from: "Same" (the namespace of the + object that holds the grant), "Names" (the namespaces in names), "Selector" + (the namespaces whose labels match selector) or "All" (every namespace). enum: - Same - Names @@ -114,8 +115,9 @@ spec: description: 'Names are the namespaces admitted with from: "Names", by exact name.' items: - description: GrantNamespace is a namespace a grant admits, - by exact name. A variable and "*" are refused. + description: |- + GrantNamespace is a namespace a grant admits, by exact name. A variable and + "*" are refused. maxLength: 63 type: string x-kubernetes-validations: @@ -137,8 +139,8 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches the labels of the namespaces admitted with from: "Selector". It must not be - empty: to admit every namespace, use from: "All". + Selector matches the labels of the namespaces admitted with from: + "Selector". It must not be empty: to admit every namespace, use from: "All". properties: matchExpressions: description: matchExpressions is a list of label selector @@ -209,19 +211,20 @@ spec: x-kubernetes-list-type: atomic branch: description: |- - Branch is the branch on the remote that this GitBranch writes. It must match a branch pattern - that the GitProvider's spec.allowedGitBranches give this namespace. Only one GitBranch holds - a repository branch: the oldest admitted one whose credential can open a push session. Any - other reports Stalled, reason BranchConflict. Immutable: delete and recreate the GitBranch to + Branch is the branch on the remote that this GitBranch writes. It must match + a branch pattern that the GitProvider's spec.allowedGitBranches give this + namespace. Only one GitBranch holds a repository branch: the oldest admitted + one whose credential can open a push session. Any other reports Stalled, + reason BranchConflict. Immutable: delete and recreate the GitBranch to change it. minLength: 1 type: string gitProviderRef: description: |- - GitProviderRef names the GitProvider whose repository and credential write this branch. An - omitted namespace means this GitBranch's namespace. The GitProvider's spec.allowedGitBranches - must admit this namespace and spec.branch. Immutable: delete and recreate the GitBranch to - change it. + GitProviderRef names the GitProvider whose repository and credential write + this branch. An omitted namespace means this GitBranch's namespace. The + GitProvider's spec.allowedGitBranches must admit this namespace and + spec.branch. Immutable: delete and recreate the GitBranch to change it. properties: name: description: Name of the referent. @@ -238,13 +241,14 @@ spec: rule: self.name != '' parentBranch: description: |- - ParentBranch is the branch spec.branch is created from, and compared against while - spec.branch does not exist on the remote. Omitted, it is the remote's default branch, or no - history at all in an empty repository. When set, it must exist on the remote for spec.branch - to be created; otherwise this GitBranch reports ParentBranchNotFound and writes nothing. Once - spec.branch exists, the parent branch is no longer followed. It is only read, so - spec.allowedGitBranches need not allow it. Immutable, including adding or removing it: - delete and recreate the GitBranch to change it. + ParentBranch is the branch spec.branch is created from, and compared against + while spec.branch does not exist on the remote. Omitted, it is the remote's + default branch, or no history at all in an empty repository. When set, it + must exist on the remote for spec.branch to be created; otherwise this + GitBranch reports ParentBranchNotFound and writes nothing. Once spec.branch + exists, the parent branch is no longer followed. It is only read, so + spec.allowedGitBranches need not allow it. Immutable, including adding or + removing it: delete and recreate the GitBranch to change it. minLength: 1 type: string required: @@ -267,8 +271,9 @@ spec: properties: conditions: description: |- - Conditions are the latest observations of this GitBranch's state: whether it holds its - repository branch and can write it. Ready, Reconciling and Stalled follow kstatus. + Conditions are the latest observations of this GitBranch's state: whether it + holds its repository branch and can write it. Ready, Reconciling and Stalled + follow kstatus. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -329,17 +334,18 @@ spec: x-kubernetes-list-type: map gitTargetCount: description: |- - GitTargetCount is how many GitTargets, in any namespace, reference this GitBranch. Each one - reports its own folder's health in its own conditions. + GitTargetCount is how many GitTargets, in any namespace, reference this + GitBranch. Each one reports its own folder's health in its own conditions. format: int32 type: integer lastPushNoticeAt: description: |- - LastPushNoticeAt is when a GitReceiver last delivered a push for this branch. It records that - the delivery arrived; status.remote says what the remote holds. Absent means no delivery has - reached this branch: no GitReceiver is set up or reachable, the delivery names the repository - by a URL other than the GitProvider's spec.url, or no GitTarget references this GitBranch. It - advances at most once a minute. + LastPushNoticeAt is when a GitReceiver last delivered a push for this + branch. It records that the delivery arrived; status.remote says what the + remote holds. Absent means no delivery has reached this branch: no + GitReceiver is set up or reachable, the delivery names the repository by a + URL other than the GitProvider's spec.url, or no GitTarget references this + GitBranch. It advances at most once a minute. format: date-time type: string observedGeneration: @@ -349,14 +355,16 @@ spec: type: integer remote: description: |- - Remote is where this branch is on the Git remote, and when that was last checked by a push or - a fetch. It is updated at most once a minute. Absent until the first check, and again after - the GitProvider is recreated for another repository. + Remote is where this branch is on the Git remote, and when that was last + checked by a push or a fetch. It is updated at most once a minute. Absent + until the first check, and again after the GitProvider is recreated for + another repository. properties: commit: description: |- - Commit is the commit hash the branch is at on the remote. Empty means the branch is not on - the remote yet, which is not an error: a branch nothing has written to does not exist. + Commit is the commit hash the branch is at on the remote. Empty means the + branch is not on the remote yet, which is not an error: a branch nothing has + written to does not exist. type: string lastVerifiedAt: description: LastVerifiedAt is when the remote was last observed. @@ -364,24 +372,26 @@ spec: type: string parent: description: |- - Parent is what the branch would be created from. Set only while the branch is not on the - remote. + Parent is what the branch would be created from. Set only while the branch + is not on the remote. properties: branch: description: |- - Branch is the parent branch: spec.parentBranch, or the remote's default branch when that is - omitted. Empty when state is "Unborn", and when the remote names no default branch. + Branch is the parent branch: spec.parentBranch, or the remote's default + branch when that is omitted. Empty when state is "Unborn", and when the + remote names no default branch. type: string commit: description: |- - Commit is the commit hash the parent branch is at on the remote, which the branch's first - commit would build on. Set only when state is "Found". + Commit is the commit hash the parent branch is at on the remote, which the + branch's first commit would build on. Set only when state is "Found". type: string state: description: |- - State is what the branch would be created from: "Found" (the parent branch's tip), "Missing" - (the parent branch is not on the remote, so nothing is written) or "Unborn" (an empty - repository and no spec.parentBranch, so the first commit has no history). + State is what the branch would be created from: "Found" (the parent branch's + tip), "Missing" (the parent branch is not on the remote, so nothing is + written) or "Unborn" (an empty repository and no spec.parentBranch, so the + first commit has no history). enum: - Found - Missing @@ -392,9 +402,10 @@ spec: type: object verifiedBy: description: |- - VerifiedBy is how commit was learned: "Push" means the remote accepted a push from GitOps - Reverser, and "Fetch" means a fetch found it. "Fetch" next to a commit GitOps Reverser did not - make means somebody else pushed to the branch. + VerifiedBy is how commit was learned: "Push" means the remote accepted a + push from GitOps Reverser, and "Fetch" means a fetch found it. "Fetch" next + to a commit GitOps Reverser did not make means somebody else pushed to the + branch. enum: - Push - Fetch @@ -402,9 +413,10 @@ spec: type: object repository: description: |- - Repository is the GitProvider's repository URL, normalized to host and path with no scheme, - user or ".git" suffix. With spec.branch it is what only one GitBranch may hold, so two - GitProviders that spell one repository differently still conflict. + Repository is the GitProvider's repository URL, normalized to host and path + with no scheme, user or ".git" suffix. With spec.branch it is what only one + GitBranch may hold, so two GitProviders that spell one repository + differently still conflict. type: string type: object required: diff --git a/config/crd/bases/configbutler.ai_gitproviders.yaml b/config/crd/bases/configbutler.ai_gitproviders.yaml index 594689fc..1e3048be 100644 --- a/config/crd/bases/configbutler.ai_gitproviders.yaml +++ b/config/crd/bases/configbutler.ai_gitproviders.yaml @@ -39,9 +39,10 @@ spec: schema: openAPIV3Schema: description: |- - GitProvider is a connection to one Git repository: its URL, the credential that reaches it, - the committer identity and commit signing. GitBranches claim branches of the repository - through it, as its spec.allowedGitBranches allow. + GitProvider is a connection to one Git repository: its URL, the credential + that reaches it, the committer identity and commit signing. GitBranches + claim branches of the repository through it, as its spec.allowedGitBranches + allow. properties: apiVersion: description: |- @@ -65,29 +66,33 @@ spec: properties: allowedGitBranches: description: |- - AllowedGitBranches says which GitBranches may claim a branch of this repository through this - GitProvider: a grant must admit the GitBranch's namespace, and one of its branchPatterns must - match the GitBranch's spec.branch. Grants combine by union. Unlike the grant lists of - GitBranch and KubeProvider it has no default: omitted or empty, no GitBranch may claim + AllowedGitBranches says which GitBranches may claim a branch of this + repository through this GitProvider: a grant must admit the GitBranch's + namespace, and one of its branchPatterns must match the GitBranch's + spec.branch. Grants combine by union. Unlike the grant lists of GitBranch + and KubeProvider it has no default: omitted or empty, no GitBranch may claim through it, including in this namespace. items: description: |- - GitProviderGitBranchGrant admits the GitBranches of some namespaces to claim, through a - GitProvider, the branches its patterns match. + GitProviderGitBranchGrant admits the GitBranches of some namespaces to + claim, through a GitProvider, the branches its patterns match. properties: branchPatterns: description: |- - BranchPatterns are the branches an admitted GitBranch may claim, each matched against the - whole branch name. "*" matches any characters, "/" included, so "*" alone is every branch and - "main" is only main. "{consumer.namespace}" and "{consumer.name}" are the GitBranch's - namespace and name, each a whole path segment: "teams/{consumer.namespace}/*". - "{consumer.name}" needs "{consumer.namespace}" in the same pattern, because a GitBranch picks - its own name. There is no escaping, "?" or character class. + BranchPatterns are the branches an admitted GitBranch may claim, each + matched against the whole branch name. "*" matches any characters, "/" + included, so "*" alone is every branch and "main" is only main. + "{consumer.namespace}" and "{consumer.name}" are the GitBranch's namespace + and name, each a whole path segment: "teams/{consumer.namespace}/*". + "{consumer.name}" needs "{consumer.namespace}" in the same pattern, because + a GitBranch picks its own name. There is no escaping, "?" or character + class. items: description: |- - BranchPattern matches a whole branch name: literal text, "*" for any characters including "/", - and "{consumer.namespace}" and "{consumer.name}" as whole path segments. "{consumer.name}" - needs "{consumer.namespace}" in the same pattern. + BranchPattern matches a whole branch name: literal text, "*" for any + characters including "/", and "{consumer.namespace}" and "{consumer.name}" + as whole path segments. "{consumer.name}" needs "{consumer.namespace}" in + the same pattern. maxLength: 255 minLength: 1 pattern: ^[^\s~^:?\[\\]*$ @@ -128,14 +133,14 @@ spec: x-kubernetes-list-type: set consumerNamespaces: description: |- - ConsumerNamespaces are the namespaces whose GitBranches this grant admits. from: "Same" is - this GitProvider's namespace. + ConsumerNamespaces are the namespaces whose GitBranches this grant admits. + from: "Same" is this GitProvider's namespace. properties: from: description: |- - From is where this grant admits consumers from: "Same" (the namespace of the object that - holds the grant), "Names" (the namespaces in names), "Selector" (the namespaces whose labels - match selector) or "All" (every namespace). + From is where this grant admits consumers from: "Same" (the namespace of the + object that holds the grant), "Names" (the namespaces in names), "Selector" + (the namespaces whose labels match selector) or "All" (every namespace). enum: - Same - Names @@ -146,8 +151,9 @@ spec: description: 'Names are the namespaces admitted with from: "Names", by exact name.' items: - description: GrantNamespace is a namespace a grant admits, - by exact name. A variable and "*" are refused. + description: |- + GrantNamespace is a namespace a grant admits, by exact name. A variable and + "*" are refused. maxLength: 63 type: string x-kubernetes-validations: @@ -169,8 +175,8 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches the labels of the namespaces admitted with from: "Selector". It must not be - empty: to admit every namespace, use from: "All". + Selector matches the labels of the namespaces admitted with from: + "Selector". It must not be empty: to admit every namespace, use from: "All". properties: matchExpressions: description: matchExpressions is a list of label selector @@ -242,13 +248,14 @@ spec: x-kubernetes-list-type: atomic commit: description: |- - Commit sets the committer identity and commit signing. Commit messages are set on the - GitTarget, in spec.commit.message. + Commit sets the committer identity and commit signing. Commit messages are + set on the GitTarget, in spec.commit.message. properties: committer: description: |- - Committer is the identity written as each commit's committer. With signing enabled, its - email must be a verified address of the account that owns the signing key. + Committer is the identity written as each commit's committer. With signing + enabled, its email must be a verified address of the account that owns the + signing key. properties: email: default: noreply@configbutler.ai @@ -266,12 +273,13 @@ spec: generateWhenMissing: default: false description: |- - GenerateWhenMissing creates a signing key in the Secret when it holds none. Defaults to - false. + GenerateWhenMissing creates a signing key in the Secret when it holds none. + Defaults to false. type: boolean secretRef: - description: SecretRef names the Secret, in this namespace, - that holds the SSH signing key. + description: |- + SecretRef names the Secret, in this namespace, that holds the SSH signing + key. properties: name: description: Name of the referent. @@ -288,10 +296,11 @@ spec: type: object knownHostsRef: description: |- - KnownHostsRef names a ConfigMap or Secret, in this namespace, holding SSH known_hosts, so - several GitProviders on one host can share them. SSH only. Host keys come from the - credentials Secret's own known_hosts first, then this reference, then the installation's - default; without valid keys, SSH fails. + KnownHostsRef names a ConfigMap or Secret, in this namespace, holding SSH + known_hosts, so several GitProviders on one host can share them. SSH only. + Host keys come from the credentials Secret's own known_hosts first, then + this reference, then the installation's default; without valid keys, SSH + fails. properties: kind: default: ConfigMap @@ -302,8 +311,9 @@ spec: type: string name: description: |- - Name is the name of the ConfigMap or Secret. Its "known_hosts" key is read, or - "ssh_known_hosts" (the key Argo CD's argocd-ssh-known-hosts-cm uses) when that is absent. + Name is the name of the ConfigMap or Secret. Its "known_hosts" key is read, + or "ssh_known_hosts" (the key Argo CD's argocd-ssh-known-hosts-cm uses) when + that is absent. minLength: 1 type: string required: @@ -311,8 +321,8 @@ spec: type: object secretRef: description: |- - SecretRef names the Secret, in this namespace, that holds the credentials for the repository. - Omitted means anonymous access. + SecretRef names the Secret, in this namespace, that holds the credentials + for the repository. Omitted means anonymous access. properties: name: description: Name of the referent. @@ -326,8 +336,8 @@ spec: rule: self.name != '' url: description: |- - URL is the repository's URL, over HTTPS or SSH. Immutable: delete and recreate the - GitProvider to change it. + URL is the repository's URL, over HTTPS or SSH. Immutable: delete and + recreate the GitProvider to change it. minLength: 1 type: string required: @@ -342,9 +352,9 @@ spec: properties: conditions: description: |- - Conditions are the latest observations of this GitProvider's state: whether the repository - answers with its credential, and whether commit signing is ready. Ready, Reconciling and - Stalled follow kstatus. + Conditions are the latest observations of this GitProvider's state: whether + the repository answers with its credential, and whether commit signing is + ready. Ready, Reconciling and Stalled follow kstatus. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -419,9 +429,9 @@ spec: type: integer signingPublicKey: description: |- - SigningPublicKey is the SSH public key commits are signed with, in authorized_keys format. - Register it as a signing key with your Git host. Set only when spec.commit.signing is - configured and its key is available. + SigningPublicKey is the SSH public key commits are signed with, in + authorized_keys format. Register it as a signing key with your Git host. Set + only when spec.commit.signing is configured and its key is available. type: string type: object required: diff --git a/config/crd/bases/configbutler.ai_gitreceivers.yaml b/config/crd/bases/configbutler.ai_gitreceivers.yaml index 1b1e9a2d..268a50ce 100644 --- a/config/crd/bases/configbutler.ai_gitreceivers.yaml +++ b/config/crd/bases/configbutler.ai_gitreceivers.yaml @@ -39,12 +39,14 @@ spec: schema: openAPIV3Schema: description: |- - GitReceiver is the endpoint a Git host notifies when it receives a push. A delivery wakes every - GitBranch of every GitProvider, in any namespace, whose spec.url names the delivery's - repository, so pushes are picked up at once instead of at the next refresh. A GitProvider that - reaches the host through another host name or port is not matched. A delivery is only a hint: - the branch is then read with its own GitProvider's credential, so a delivery cannot choose a - URL, a credential or a commit. GitBranch.status.lastPushNoticeAt shows deliveries arriving. + GitReceiver is the endpoint a Git host notifies when it receives a push. A + delivery wakes every GitBranch of every GitProvider, in any namespace, whose + spec.url names the delivery's repository, so pushes are picked up at once + instead of at the next refresh. A GitProvider that reaches the host through + another host name or port is not matched. A delivery is only a hint: the + branch is then read with its own GitProvider's credential, so a delivery + cannot choose a URL, a credential or a commit. + GitBranch.status.lastPushNoticeAt shows deliveries arriving. properties: apiVersion: description: |- @@ -68,9 +70,9 @@ spec: properties: secretRef: description: |- - SecretRef names the Secret, in this namespace, whose "token" key is the webhook secret. The - token also determines status.webhookPath, so rotating it moves the path: register the new - path with the new secret. + SecretRef names the Secret, in this namespace, whose "token" key is the + webhook secret. The token also determines status.webhookPath, so rotating it + moves the path: register the new path with the new secret. properties: name: description: Name of the referent. @@ -83,10 +85,11 @@ spec: rule: self.name != '' type: description: |- - Type is the payload and signature the Git host sends: "github" (X-Hub-Signature-256), - "gitlab" (X-Gitlab-Token), "gitea" (X-Gitea-Signature), or "generic-hmac" for a relay of - your own (X-Signature: sha256=, over a {"repository", "branch", - "after"} body). + Type is the payload and signature the Git host sends: "github" + (X-Hub-Signature-256), "gitlab" (X-Gitlab-Token), "gitea" + (X-Gitea-Signature), or "generic-hmac" for a relay of your own (X-Signature: + sha256=, over a {"repository", "branch", "after"} + body). enum: - github - gitlab @@ -102,9 +105,9 @@ spec: properties: conditions: description: |- - Conditions are the latest observations of this GitReceiver's state. Ready, Reconciling and - Stalled follow kstatus; Ready is True once the token is readable and the receiver listener - is on. + Conditions are the latest observations of this GitReceiver's state. Ready, + Reconciling and Stalled follow kstatus; Ready is True once the token is + readable and the receiver listener is on. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -170,9 +173,10 @@ spec: type: integer webhookPath: description: |- - WebhookPath is the path this GitReceiver is served on, under the receiver listener: /git-push/ - and an HMAC of its namespace and name, keyed by the token. Point the Git host's push webhook - at the listener's address plus this path. Empty while the token is missing. + WebhookPath is the path this GitReceiver is served on, under the receiver + listener: /git-push/ and an HMAC of its namespace and name, keyed by the + token. Point the Git host's push webhook at the listener's address plus this + path. Empty while the token is missing. type: string type: object required: diff --git a/config/crd/bases/configbutler.ai_gittargets.yaml b/config/crd/bases/configbutler.ai_gittargets.yaml index 81eaefdb..c55b3bd8 100644 --- a/config/crd/bases/configbutler.ai_gittargets.yaml +++ b/config/crd/bases/configbutler.ai_gittargets.yaml @@ -83,9 +83,9 @@ spec: schema: openAPIV3Schema: description: |- - GitTarget mirrors objects from a source cluster into one folder of a Git branch. Its WatchRules - and ClusterWatchRules select the objects, its KubeProvider reads them, and its GitBranch writes - them. + GitTarget mirrors objects from a source cluster into one folder of a Git + branch. Its WatchRules and ClusterWatchRules select the objects, its + KubeProvider reads them, and its GitBranch writes them. properties: apiVersion: description: |- @@ -109,12 +109,14 @@ spec: properties: applierClaimRef: description: |- - ApplierClaimRef names the FluxClaim or ArgoCDClaim, in this namespace, that holds the applier - of this folder: the Flux Kustomization or Argo CD Application that applies it. With it set, - GitOps Reverser decides when that applier runs, and runs it only after the edits it holds are - published. Objects another applier owns are not mirrored into the folder. The applier must - apply this GitTarget's repository and branch; the ApplierClaimReady condition reports whether - it does. Several GitTargets may name one applier claim. + ApplierClaimRef names the FluxClaim or ArgoCDClaim, in this namespace, that + holds the applier of this folder: the Flux Kustomization or Argo CD + Application that applies it. With it set, GitOps Reverser decides when that + applier runs, and runs it only after the edits it holds are published. + Objects another applier owns are not mirrored into the folder. The applier + must apply this GitTarget's repository and branch; the ApplierClaimReady + condition reports whether it does. Several GitTargets may name one applier + claim. properties: kind: description: 'Kind is the applier claim''s kind: "FluxClaim" or @@ -133,13 +135,15 @@ spec: type: object commit: description: |- - Commit configures how this GitTarget's writes are batched into commits and how the commits - are worded. Omitted, a commit window closes 5s after its last write or 1m after it opened, - and the built-in message templates are used. + Commit configures how this GitTarget's writes are batched into commits and + how the commits are worded. Omitted, a commit window closes 5s after its + last write or 1m after it opened, and the built-in message templates are + used. properties: message: - description: Message sets the commit message templates. A template - left empty uses the built-in one. + description: |- + Message sets the commit message templates. A template left empty uses the + built-in one. properties: liveTemplate: description: |- @@ -170,33 +174,37 @@ spec: type: string reconcileTemplate: description: |- - ReconcileTemplate is the Go template for the message of a snapshot or resync commit. Fields: - Count, GitTarget, Group, Version, Resource, APIVersion, Namespace and ResourceVersion (the - list's, not one object's). The type and namespace fields are empty for a snapshot of the - whole folder, and ResourceVersion can be empty, so guard them. A template naming Revision, + ReconcileTemplate is the Go template for the message of a snapshot or resync + commit. Fields: Count, GitTarget, Group, Version, Resource, APIVersion, + Namespace and ResourceVersion (the list's, not one object's). The type and + namespace fields are empty for a snapshot of the whole folder, and + ResourceVersion can be empty, so guard them. A template naming Revision, renamed to ResourceVersion, is rejected. type: string requestTemplate: description: |- - RequestTemplate is the Go template that frames a CommitRequest's message. It is used only for - a commit whose CommitRequest supplied a message, and gets liveTemplate's fields plus - RequestMessage, the message as written; template syntax inside the message stays literal. - Omitted, the message is committed as written. A template that never renders RequestMessage - is rejected, and one that fails at commit time commits the message as written. + RequestTemplate is the Go template that frames a CommitRequest's message. It + is used only for a commit whose CommitRequest supplied a message, and gets + liveTemplate's fields plus RequestMessage, the message as written; template + syntax inside the message stays literal. Omitted, the message is committed + as written. A template that never renders RequestMessage is rejected, and + one that fails at commit time commits the message as written. type: string type: object window: description: |- - Window sets when this GitTarget's commit windows close. A window opens on an author's first - write and collects that author's writes until idleTimeout passes without one, or maxDuration - after it opened, whichever comes first. A change applies to windows opened afterward. A - CommitRequest attached to a window replaces both timers for that window. + Window sets when this GitTarget's commit windows close. A window opens on an + author's first write and collects that author's writes until idleTimeout + passes without one, or maxDuration after it opened, whichever comes first. A + change applies to windows opened afterward. A CommitRequest attached to a + window replaces both timers for that window. properties: idleTimeout: default: 5s description: |- - IdleTimeout closes the window this long after its last write, as a Go duration ("5s", - "750ms"). "0s" makes one commit per write. At most "24h". Defaults to "5s". + IdleTimeout closes the window this long after its last write, as a Go + duration ("5s", "750ms"). "0s" makes one commit per write. At most "24h". + Defaults to "5s". pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$ type: string x-kubernetes-validations: @@ -206,9 +214,9 @@ spec: maxDuration: default: 1m description: |- - MaxDuration closes the window this long after it opened, however many writes keep arriving, - as a Go duration ("1m", "90s"). "0s" closes it right after the write that opened it. At most - "24h". Defaults to "1m". + MaxDuration closes the window this long after it opened, however many writes + keep arriving, as a Go duration ("1m", "90s"). "0s" closes it right after + the write that opened it. At most "24h". Defaults to "1m". pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$ type: string x-kubernetes-validations: @@ -222,8 +230,9 @@ spec: <= duration(self.maxDuration)' type: object encryption: - description: Encryption configures how Secrets are encrypted before - they are written to Git. + description: |- + Encryption configures how Secrets are encrypted before they are written to + Git. properties: age: description: Age configures age recipients for SOPS. @@ -231,8 +240,8 @@ spec: enabled: default: false description: |- - Enabled turns on age encryption: resolving recipients, and creating a key when asked. - Defaults to false. + Enabled turns on age encryption: resolving recipients, and creating a key + when asked. Defaults to false. type: boolean recipients: description: Recipients says where the age recipients come @@ -241,14 +250,14 @@ spec: extractFromSecret: default: false description: |- - ExtractFromSecret adds the recipient of every *.agekey entry in encryption.secretRef. - Defaults to false. + ExtractFromSecret adds the recipient of every *.agekey entry in + encryption.secretRef. Defaults to false. type: boolean generateWhenMissing: default: false description: |- - GenerateWhenMissing creates a date-named *.agekey entry in encryption.secretRef when it has - none. Defaults to false. + GenerateWhenMissing creates a date-named *.agekey entry in + encryption.secretRef when it has none. Defaults to false. type: boolean publicKeys: description: PublicKeys is a fixed list of age recipients @@ -261,14 +270,16 @@ spec: type: object provider: default: sops - description: Provider is the encryption tool. Only "sops" is supported. - Defaults to "sops". + description: |- + Provider is the encryption tool. Only "sops" is supported. Defaults to + "sops". enum: - sops type: string secretRef: - description: SecretRef names the Secret, in this namespace, that - holds the provider's keys. + description: |- + SecretRef names the Secret, in this namespace, that holds the provider's + keys. properties: name: description: Name of the referent. @@ -284,10 +295,11 @@ spec: type: object gitBranchRef: description: |- - GitBranchRef names the GitBranch this GitTarget writes to: the repository branch and the - credential that writes it. An omitted namespace means this GitTarget's namespace. The - GitBranch's spec.allowedGitTargets must admit this namespace. Several GitTargets may share - one GitBranch. Immutable: delete and recreate the GitTarget to change it. + GitBranchRef names the GitBranch this GitTarget writes to: the repository + branch and the credential that writes it. An omitted namespace means this + GitTarget's namespace. The GitBranch's spec.allowedGitTargets must admit + this namespace. Several GitTargets may share one GitBranch. Immutable: + delete and recreate the GitTarget to change it. properties: name: description: Name of the referent. @@ -304,10 +316,11 @@ spec: rule: self.name != '' kubeProviderRef: description: |- - KubeProviderRef names the KubeProvider this GitTarget mirrors from: the source cluster and the - credential that reads it. An omitted namespace means this GitTarget's namespace. The - KubeProvider's spec.allowedGitTargets must admit this namespace. Immutable: delete and - recreate the GitTarget to change it. + KubeProviderRef names the KubeProvider this GitTarget mirrors from: the + source cluster and the credential that reads it. An omitted namespace means + this GitTarget's namespace. The KubeProvider's spec.allowedGitTargets must + admit this namespace. Immutable: delete and recreate the GitTarget to change + it. properties: name: description: Name of the referent. @@ -328,83 +341,94 @@ spec: onRefusal: default: Ignore description: |- - OnRefusal decides what happens when a live edit is refused because it has no valid place in - the folder. A refusal makes no commit, so the edit stays on the cluster until something - applies Git over it. + OnRefusal decides what happens when a live edit is refused because it has no + valid place in the folder. A refusal makes no commit, so the edit stays on + the cluster until something applies Git over it. - "Ignore" (the default) reports the refusal in conditions and events and does nothing else. + "Ignore" (the default) reports the refusal in conditions and events and does + nothing else. - "Reapply" asks the applier in spec.applierClaimRef, which it requires, to apply the branch - once the pending edits are published, which reverts the refused edit. It fires only for an - edit to a document the folder already holds, in a write that removes no document, and never - for a refusal of the whole folder, such as unparseable YAML. A refusal that stands is - reapplied once, not on every recheck. + "Reapply" asks the applier in spec.applierClaimRef, which it requires, to + apply the branch once the pending edits are published, which reverts the + refused edit. It fires only for an edit to a document the folder already + holds, in a write that removes no document, and never for a refusal of the + whole folder, such as unparseable YAML. A refusal that stands is reapplied + once, not on every recheck. enum: - Ignore - Reapply type: string path: description: |- - Path is the folder this GitTarget writes, relative to the repository root. Use "." for the - root itself. A leading "/" and ".." are rejected, and a trailing "/" is dropped. Immutable: - delete and recreate the GitTarget to change it. + Path is the folder this GitTarget writes, relative to the repository root. + Use "." for the root itself. A leading "/" and ".." are rejected, and a + trailing "/" is dropped. Immutable: delete and recreate the GitTarget to + change it. minLength: 1 type: string placement: description: |- - Placement decides where a new resource is written: one with no document in the folder yet. - A resource that already has a document is updated where that document is, so a change here - affects only resources created afterward, and labeling a resource later never moves its file. + Placement decides where a new resource is written: one with no document in + the folder yet. A resource that already has a document is updated where that + document is, so a change here affects only resources created afterward, and + labeling a resource later never moves its file. - byType and default are path templates over the resource's identity ({namespace}, {name}, - {group}, {version}, {resource}, {kind} and more), and may read a label: - "{label:app.kubernetes.io/instance}/configmaps.yaml". "{label:key}" renders "_unlabeled" for a - resource without the label, and "{namespace}" renders "_cluster" for a cluster-scoped one. - Append "|fallback" to either to choose that value: "{label:team|unassigned}", - "{namespace|_global}". A fallback is up to 63 characters of [A-Za-z0-9._-], is not "." or - "..", and may be empty to drop the path segment. A {namespace} fallback that names a real - namespace files cluster-scoped resources in that namespace's folder. A fallback on any other - variable is rejected. + byType and default are path templates over the resource's identity + ({namespace}, {name}, {group}, {version}, {resource}, {kind} and more), and + may read a label: "{label:app.kubernetes.io/instance}/configmaps.yaml". + "{label:key}" renders "_unlabeled" for a resource without the label, and + "{namespace}" renders "_cluster" for a cluster-scoped one. Append + "|fallback" to either to choose that value: "{label:team|unassigned}", + "{namespace|_global}". A fallback is up to 63 characters of [A-Za-z0-9._-], + is not "." or "..", and may be empty to drop the path segment. A {namespace} + fallback that names a real namespace files cluster-scoped resources in that + namespace's folder. A fallback on any other variable is rejected. properties: byType: additionalProperties: type: string description: |- - ByType maps a resource type, written "{group}/{version}/{resource}" ("apps/v1/deployments", - or "v1/configmaps" for the core group), to the path template for new resources of that - type. The template of a sensitive type, such as Secrets, must contain the resource's whole - identity, so two of them never share a file. + ByType maps a resource type, written "{group}/{version}/{resource}" + ("apps/v1/deployments", or "v1/configmaps" for the core group), to the path + template for new resources of that type. The template of a sensitive type, + such as Secrets, must contain the resource's whole identity, so two of them + never share a file. type: object default: description: |- - Default is the path template for a new resource whose type has no byType entry. Omitted, a - new document goes next to the folder's kustomization when exactly one governs the folder, - and otherwise to {namespace}/{group}/{resource}/{name}.yaml. That path has no version, so - add {version} to a template to keep two versions of one resource apart. A bundling template - such as "all.yaml" is valid only when every sensitive type has its own byType entry. + Default is the path template for a new resource whose type has no byType + entry. Omitted, a new document goes next to the folder's kustomization when + exactly one governs the folder, and otherwise to + {namespace}/{group}/{resource}/{name}.yaml. That path has no version, so add + {version} to a template to keep two versions of one resource apart. A + bundling template such as "all.yaml" is valid only when every sensitive type + has its own byType entry. type: string useKustomize: description: |- - UseKustomize makes GitOps Reverser create a kustomization.yaml at spec.path when no - kustomization governs the folder yet. The first new document creates it, listing every - managed document already in the folder and the new one, and nothing else: no namespace:. - It has no effect on a folder that already has a kustomization. A new file is always added - to the kustomization governing it, and a template that places a document outside that + UseKustomize makes GitOps Reverser create a kustomization.yaml at spec.path + when no kustomization governs the folder yet. The first new document creates + it, listing every managed document already in the folder and the new one, + and nothing else: no namespace:. It has no effect on a folder that already + has a kustomization. A new file is always added to the kustomization + governing it, and a template that places a document outside that kustomization is refused. Defaults to false. type: boolean type: object prune: description: |- - Prune decides which deletions remove documents from this folder: a delete observed on the - source cluster, and a document a resync finds no resource for. Omitted, mode is "OnEvent": - observed deletes are mirrored, and a resync removes nothing. + Prune decides which deletions remove documents from this folder: a delete + observed on the source cluster, and a document a resync finds no resource + for. Omitted, mode is "OnEvent": observed deletes are mirrored, and a resync + removes nothing. properties: mode: default: OnEvent description: |- - Mode decides which deletions are mirrored. "Never" removes nothing. "OnEvent" mirrors a - delete observed on the source cluster, but a resync removes nothing. "Always" also lets a - resync remove a document that has no resource on the cluster. Defaults to "OnEvent". + Mode decides which deletions are mirrored. "Never" removes nothing. + "OnEvent" mirrors a delete observed on the source cluster, but a resync + removes nothing. "Always" also lets a resync remove a document that has no + resource on the cluster. Defaults to "OnEvent". enum: - Never - OnEvent @@ -414,28 +438,31 @@ spec: serializeNamespace: description: |- SerializeNamespace decides whether a namespaced resource's document carries - metadata.namespace. It applies to every write, not just the first, and is ignored for - cluster-scoped resources. + metadata.namespace. It applies to every write, not just the first, and is + ignored for cluster-scoped resources. - Omitted, it is decided per document: the namespace is left out only when the kustomization - governing the document's path sets namespace: to that same namespace. This suits a folder of - nested kustomize roots that each set their own namespace. + Omitted, it is decided per document: the namespace is left out only when the + kustomization governing the document's path sets namespace: to that same + namespace. This suits a folder of nested kustomize roots that each set their + own namespace. true always writes it. Use it for a flat folder applied as is. - false never writes it, for a folder whose namespace something else supplies: a Flux - Kustomization's targetNamespace, an Argo CD Application's destination.namespace, or a - kustomization. Such a folder holds one source namespace; a WatchRule bringing a second one is - refused with GitPathAccepted=False, reason MultipleSourceNamespaces. + false never writes it, for a folder whose namespace something else supplies: + a Flux Kustomization's targetNamespace, an Argo CD Application's + destination.namespace, or a kustomization. Such a folder holds one source + namespace; a WatchRule bringing a second one is refused with + GitPathAccepted=False, reason MultipleSourceNamespaces. type: boolean suspend: description: |- - Suspend stops this GitTarget from making new commits, without deleting it. Watches keep - running and status.placement stays current; status.retention is not published while it is - set. A commit already made locally is still pushed, so one more commit can appear seconds - after it is set: suspend stops the next write and does not undo the last one. Clearing it - resumes from the current cluster state at the next resync; writes skipped while suspended - are not replayed. Defaults to false. + Suspend stops this GitTarget from making new commits, without deleting it. + Watches keep running and status.placement stays current; status.retention is + not published while it is set. A commit already made locally is still + pushed, so one more commit can appear seconds after it is set: suspend stops + the next write and does not undo the last one. Clearing it resumes from the + current cluster state at the next resync; writes skipped while suspended are + not replayed. Defaults to false. type: boolean required: - gitBranchRef @@ -457,8 +484,9 @@ spec: properties: conditions: description: |- - Conditions are the latest observations of this GitTarget's state. Ready, Reconciling and - Stalled follow kstatus; the others say which check holds the GitTarget back. + Conditions are the latest observations of this GitTarget's state. Ready, + Reconciling and Stalled follow kstatus; the others say which check holds the + GitTarget back. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -524,28 +552,31 @@ spec: type: integer placement: description: |- - Placement is this folder's layout as the last scan resolved it: whether a kustomization - governs new documents, which one, and which bases the folder renders but may not write. The - LayoutResolved condition gives the verdict. + Placement is this folder's layout as the last scan resolved it: whether a + kustomization governs new documents, which one, and which bases the folder + renders but may not write. The LayoutResolved condition gives the verdict. properties: mode: description: |- Mode is how this folder is written. - "Plain": no kustomization governs the folder. Documents are written and removed, and - nothing else is touched. + "Plain": no kustomization governs the folder. Documents are written and + removed, and nothing else is touched. - "KustomizeRoot": one kustomization governs the folder and renders nothing outside it. A new - document is also added to its resources:, a removed one is dropped from it, and every write - is checked by rendering, so kustomize can refuse one. + "KustomizeRoot": one kustomization governs the folder and renders nothing + outside it. A new document is also added to its resources:, a removed one is + dropped from it, and every write is checked by rendering, so kustomize can + refuse one. - "KustomizeOverlay": as KustomizeRoot, and the folder also renders a base outside it - (readOnlyBases). An edit to a field the base sets is written into the overlay where it can - be (images:, replicas:) and refused with WriteBoundaryRefused otherwise. Deleting an object - the base supplies writes a $patch: delete into the overlay. + "KustomizeOverlay": as KustomizeRoot, and the folder also renders a base + outside it (readOnlyBases). An edit to a field the base sets is written into + the overlay where it can be (images:, replicas:) and refused with + WriteBoundaryRefused otherwise. Deleting an object the base supplies writes + a $patch: delete into the overlay. - Empty when the folder holds more than one kustomize root (LayoutResolved=Ambiguous): no new - document is placed until spec.path points at one of them. + Empty when the folder holds more than one kustomize root + (LayoutResolved=Ambiguous): no new document is placed until spec.path points + at one of them. enum: - Plain - KustomizeRoot @@ -553,46 +584,51 @@ spec: type: string readOnlyBases: description: |- - ReadOnlyBases are the kustomization directories this folder renders but may not write, - relative to spec.path, so they start with "../". Set only for KustomizeOverlay. An edit to a - document under one of them is refused with WriteBoundaryRefused unless the overlay can - express it. + ReadOnlyBases are the kustomization directories this folder renders but may + not write, relative to spec.path, so they start with "../". Set only for + KustomizeOverlay. An edit to a document under one of them is refused with + WriteBoundaryRefused unless the overlay can express it. items: type: string type: array renderRoot: description: |- - RenderRoot is the kustomization directory that governs new documents, relative to - spec.path ("." is the folder itself). Empty for Plain, and when the layout is Ambiguous. + RenderRoot is the kustomization directory that governs new documents, + relative to spec.path ("." is the folder itself). Empty for Plain, and when + the layout is Ambiguous. type: string resolvedAt: description: |- - ResolvedAt is when this layout was first seen. Like resolvedAtCommit, it does not move while - the layout stays the same. + ResolvedAt is when this layout was first seen. Like resolvedAtCommit, it + does not move while the layout stays the same. format: date-time type: string resolvedAtCommit: description: |- - ResolvedAtCommit is the commit at which this layout was first seen. It does not move while - the layout stays the same, so a commit older than the branch head means the layout has not - changed since. Empty until the branch has a commit. + ResolvedAtCommit is the commit at which this layout was first seen. It does + not move while the layout stays the same, so a commit older than the branch + head means the layout has not changed since. Empty until the branch has a + commit. type: string type: object retention: description: |- - Retention counts the documents a resync kept because spec.prune.mode did not allow removing - them. A delete observed on the cluster and not mirrored under "Never" is not counted, so a - "Never" folder can report zero. It is information only and changes no condition. + Retention counts the documents a resync kept because spec.prune.mode did not + allow removing them. A delete observed on the cluster and not mirrored under + "Never" is not counted, so a "Never" folder can report zero. It is + information only and changes no condition. properties: lastChangedAt: description: |- - LastChangedAt is when the count or the mode last changed. A resync that reports the same - count leaves it alone, so it does not say when retention was last measured. + LastChangedAt is when the count or the mode last changed. A resync that + reports the same count leaves it alone, so it does not say when retention + was last measured. format: date-time type: string mode: - description: Mode is the spec.prune.mode these counts were taken - under, with the default applied. + description: |- + Mode is the spec.prune.mode these counts were taken under, with the default + applied. enum: - Never - OnEvent @@ -600,9 +636,9 @@ spec: type: string retainedDocuments: description: |- - RetainedDocuments is how many documents the prune policy kept that have no resource on the - cluster. Zero means a resync found nothing to keep. An absent retention block means no - resync has reported yet. + RetainedDocuments is how many documents the prune policy kept that have no + resource on the cluster. Zero means a resync found nothing to keep. An + absent retention block means no resync has reported yet. format: int32 type: integer required: @@ -635,8 +671,8 @@ spec: type: integer summary: description: |- - Summary is ready/total for display, such as "3/4". Read ready and total instead of parsing - it. + Summary is ready/total for display, such as "3/4". Read ready and total + instead of parsing it. type: string total: description: Total is how many types are watched. diff --git a/config/crd/bases/configbutler.ai_kubeproviders.yaml b/config/crd/bases/configbutler.ai_kubeproviders.yaml index 1dadb5ca..20b4ce68 100644 --- a/config/crd/bases/configbutler.ai_kubeproviders.yaml +++ b/config/crd/bases/configbutler.ai_kubeproviders.yaml @@ -43,10 +43,11 @@ spec: schema: openAPIV3Schema: description: |- - KubeProvider is a connection to a Kubernetes cluster and the identity used there: a kubeconfig - Secret in its namespace, or the operator's own identity where the installation allows it. - GitTargets mirror from it and applier claims reach their applier through it, as its - spec.allowedGitTargets and spec.allowedApplierClaims allow. + KubeProvider is a connection to a Kubernetes cluster and the identity used + there: a kubeconfig Secret in its namespace, or the operator's own identity + where the installation allows it. GitTargets mirror from it and applier + claims reach their applier through it, as its spec.allowedGitTargets and + spec.allowedApplierClaims allow. properties: apiVersion: description: |- @@ -75,28 +76,32 @@ spec: consumerNamespaces: from: Same description: |- - AllowedApplierClaims says which FluxClaims and ArgoCDClaims may hold an applier through this - KubeProvider, and in which namespaces that applier may be (applierNamespaces). Being - admitted to mirror does not admit to apply. Omitted, it is one grant for this namespace, - holding appliers in the namespace of the same name. A written list is the whole policy, and - [] admits nobody. Grants combine by union. + AllowedApplierClaims says which FluxClaims and ArgoCDClaims may hold an + applier through this KubeProvider, and in which namespaces that applier may + be (applierNamespaces). Being admitted to mirror does not admit to apply. + Omitted, it is one grant for this namespace, holding appliers in the + namespace of the same name. A written list is the whole policy, and [] + admits nobody. Grants combine by union. items: description: |- - KubeProviderApplierClaimGrant admits the FluxClaims and ArgoCDClaims of some namespaces to hold - an applier through a KubeProvider, and bounds the namespaces that applier may be in. + KubeProviderApplierClaimGrant admits the FluxClaims and ArgoCDClaims of some + namespaces to hold an applier through a KubeProvider, and bounds the + namespaces that applier may be in. properties: applierNamespaces: description: |- - ApplierNamespaces are the namespaces, in this KubeProvider's cluster, where the Kustomization - or Application of an admitted applier claim may be: exact names, "{consumer.namespace}" for - the namespace named like the claim's, or "*" for every namespace. The scope is the union over - every grant admitting the claim, checked when the claim reconciles and when a grant changes. - An applier outside it is refused (ApplierNamespaceNotAllowed), and a claim holding it hands - it back. + ApplierNamespaces are the namespaces, in this KubeProvider's cluster, where + the Kustomization or Application of an admitted applier claim may be: exact + names, "{consumer.namespace}" for the namespace named like the claim's, or + "*" for every namespace. The scope is the union over every grant admitting + the claim, checked when the claim reconciles and when a grant changes. An + applier outside it is refused (ApplierNamespaceNotAllowed), and a claim + holding it hands it back. items: description: |- - ScopeNamespace is a namespace in a grant's scope: a name, "{consumer.namespace}" for the - namespace named like the consumer's, or "*" for every namespace. + ScopeNamespace is a namespace in a grant's scope: a name, + "{consumer.namespace}" for the namespace named like the consumer's, or "*" + for every namespace. maxLength: 63 type: string x-kubernetes-validations: @@ -115,14 +120,14 @@ spec: x-kubernetes-list-type: set consumerNamespaces: description: |- - ConsumerNamespaces are the namespaces whose FluxClaims and ArgoCDClaims this grant admits. - from: "Same" is this KubeProvider's namespace. + ConsumerNamespaces are the namespaces whose FluxClaims and ArgoCDClaims this + grant admits. from: "Same" is this KubeProvider's namespace. properties: from: description: |- - From is where this grant admits consumers from: "Same" (the namespace of the object that - holds the grant), "Names" (the namespaces in names), "Selector" (the namespaces whose labels - match selector) or "All" (every namespace). + From is where this grant admits consumers from: "Same" (the namespace of the + object that holds the grant), "Names" (the namespaces in names), "Selector" + (the namespaces whose labels match selector) or "All" (every namespace). enum: - Same - Names @@ -133,8 +138,9 @@ spec: description: 'Names are the namespaces admitted with from: "Names", by exact name.' items: - description: GrantNamespace is a namespace a grant admits, - by exact name. A variable and "*" are refused. + description: |- + GrantNamespace is a namespace a grant admits, by exact name. A variable and + "*" are refused. maxLength: 63 type: string x-kubernetes-validations: @@ -156,8 +162,8 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches the labels of the namespaces admitted with from: "Selector". It must not be - empty: to admit every namespace, use from: "All". + Selector matches the labels of the namespaces admitted with from: + "Selector". It must not be empty: to admit every namespace, use from: "All". properties: matchExpressions: description: matchExpressions is a list of label selector @@ -234,32 +240,36 @@ spec: sourceNamespaces: - '{consumer.namespace}' description: |- - AllowedGitTargets says which GitTargets may mirror through this KubeProvider, and what. Each - grant admits GitTargets from some namespaces, bounds which source namespaces their - WatchRules may read (sourceNamespaces), and lists the cluster-scoped types their - ClusterWatchRules may mirror (clusterResources). Omitted, it is one grant for this namespace, - reading the source namespace of the same name and no cluster-scoped type. A written list is - the whole policy, and [] admits nobody. Grants combine by union. + AllowedGitTargets says which GitTargets may mirror through this + KubeProvider, and what. Each grant admits GitTargets from some namespaces, + bounds which source namespaces their WatchRules may read (sourceNamespaces), + and lists the cluster-scoped types their ClusterWatchRules may mirror + (clusterResources). Omitted, it is one grant for this namespace, reading the + source namespace of the same name and no cluster-scoped type. A written list + is the whole policy, and [] admits nobody. Grants combine by union. items: description: |- - KubeProviderGitTargetGrant admits the GitTargets of some namespaces to mirror through a - KubeProvider, and bounds what they may read. It sets sourceNamespaces, clusterResources, or both. + KubeProviderGitTargetGrant admits the GitTargets of some namespaces to + mirror through a KubeProvider, and bounds what they may read. It sets + sourceNamespaces, clusterResources, or both. properties: clusterResources: description: |- - ClusterResources are the cluster-scoped types that the ClusterWatchRules of an admitted - GitTarget may mirror: resource.group ("clusterroles.rbac.authorization.k8s.io"), a bare - resource for the core group ("nodes"), or "*" for every type, including CRDs installed - later. Each covers every served version. Omitted, this grant gives none. + ClusterResources are the cluster-scoped types that the ClusterWatchRules of + an admitted GitTarget may mirror: resource.group + ("clusterroles.rbac.authorization.k8s.io"), a bare resource for the core + group ("nodes"), or "*" for every type, including CRDs installed later. Each + covers every served version. Omitted, this grant gives none. - The scope is the union over every grant admitting the GitTarget, and mirrors nothing until a - ClusterWatchRule asks for a type. A rule with "*" in apiGroups or resources needs "*" in a - grant, and is refused, not narrowed, without it. A ClusterWatchRule asking for any type - outside the scope is refused as a whole (ClusterResourceNotAllowed) when it reconciles. + The scope is the union over every grant admitting the GitTarget, and mirrors + nothing until a ClusterWatchRule asks for a type. A rule with "*" in + apiGroups or resources needs "*" in a grant, and is refused, not narrowed, + without it. A ClusterWatchRule asking for any type outside the scope is + refused as a whole (ClusterResourceNotAllowed) when it reconciles. items: description: |- - ClusterResourceIdentity is a cluster-scoped resource type: resource.group, a bare resource for - the core group, or "*" for every type. + ClusterResourceIdentity is a cluster-scoped resource type: resource.group, a + bare resource for the core group, or "*" for every type. maxLength: 317 pattern: ^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$ type: string @@ -269,14 +279,14 @@ spec: x-kubernetes-list-type: set consumerNamespaces: description: |- - ConsumerNamespaces are the namespaces whose GitTargets this grant admits. from: "Same" is - this KubeProvider's namespace. + ConsumerNamespaces are the namespaces whose GitTargets this grant admits. + from: "Same" is this KubeProvider's namespace. properties: from: description: |- - From is where this grant admits consumers from: "Same" (the namespace of the object that - holds the grant), "Names" (the namespaces in names), "Selector" (the namespaces whose labels - match selector) or "All" (every namespace). + From is where this grant admits consumers from: "Same" (the namespace of the + object that holds the grant), "Names" (the namespaces in names), "Selector" + (the namespaces whose labels match selector) or "All" (every namespace). enum: - Same - Names @@ -287,8 +297,9 @@ spec: description: 'Names are the namespaces admitted with from: "Names", by exact name.' items: - description: GrantNamespace is a namespace a grant admits, - by exact name. A variable and "*" are refused. + description: |- + GrantNamespace is a namespace a grant admits, by exact name. A variable and + "*" are refused. maxLength: 63 type: string x-kubernetes-validations: @@ -310,8 +321,8 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches the labels of the namespaces admitted with from: "Selector". It must not be - empty: to admit every namespace, use from: "All". + Selector matches the labels of the namespaces admitted with from: + "Selector". It must not be empty: to admit every namespace, use from: "All". properties: matchExpressions: description: matchExpressions is a list of label selector @@ -376,19 +387,22 @@ spec: && size(self.selector.matchExpressions) > 0)' sourceNamespaces: description: |- - SourceNamespaces are the source-cluster namespaces that the WatchRules of an admitted - GitTarget may read, in rules[].sourceNamespace: exact names, "{consumer.namespace}" for the - namespace named like the GitTarget's, or "*". Omitted, this grant gives none. + SourceNamespaces are the source-cluster namespaces that the WatchRules of an + admitted GitTarget may read, in rules[].sourceNamespace: exact names, + "{consumer.namespace}" for the namespace named like the GitTarget's, or "*". + Omitted, this grant gives none. - The scope is the union over every grant admitting the GitTarget. It permits and selects - nothing: a namespace is read only when a WatchRule names it. A rule with sourceNamespace "*" - needs "*" in a grant, and is refused, not narrowed, without it. A WatchRule with any rule - outside the scope is refused as a whole (SourceNamespaceNotAllowed) when it reconciles or a - grant changes. The credential's RBAC still applies inside the scope. + The scope is the union over every grant admitting the GitTarget. It permits + and selects nothing: a namespace is read only when a WatchRule names it. A + rule with sourceNamespace "*" needs "*" in a grant, and is refused, not + narrowed, without it. A WatchRule with any rule outside the scope is refused + as a whole (SourceNamespaceNotAllowed) when it reconciles or a grant + changes. The credential's RBAC still applies inside the scope. items: description: |- - ScopeNamespace is a namespace in a grant's scope: a name, "{consumer.namespace}" for the - namespace named like the consumer's, or "*" for every namespace. + ScopeNamespace is a namespace in a grant's scope: a name, + "{consumer.namespace}" for the namespace named like the consumer's, or "*" + for every namespace. maxLength: 63 type: string x-kubernetes-validations: @@ -416,59 +430,65 @@ spec: type: array x-kubernetes-list-type: atomic attribution: - description: Attribution configures how commit authors are found from - this cluster's audit events. + description: |- + Attribution configures how commit authors are found from this cluster's + audit events. properties: auditRoute: description: |- - AuditRoute is the route this cluster's audit events arrive on: the in the API - server's audit webhook URL, /audit-webhook/. When several logical clusters share one - webhook backend, it is instead the value of the audit-event annotation that - --author-attribution-audit-route-annotation-key names. KubeProviders with the same route - read the same cluster's attribution facts. + AuditRoute is the route this cluster's audit events arrive on: the + in the API server's audit webhook URL, /audit-webhook/. When several + logical clusters share one webhook backend, it is instead the value of the + audit-event annotation that --author-attribution-audit-route-annotation-key + names. KubeProviders with the same route read the same cluster's attribution + facts. - Empty means .. Set it when several KubeProviders in this namespace connect - to one cluster, since an API server posts under one route. A route belongs to one namespace: - the one it starts with (.) when a KubeProvider there carries it, and otherwise - the namespace of the oldest KubeProvider carrying it. KubeProviders in another namespace that - carry it report Stalled=True, reason AuditRouteConflict, and are not used. + Empty means .. Set it when several KubeProviders in this + namespace connect to one cluster, since an API server posts under one route. + A route belongs to one namespace: the one it starts with (.) when + a KubeProvider there carries it, and otherwise the namespace of the oldest + KubeProvider carrying it. KubeProviders in another namespace that carry it + report Stalled=True, reason AuditRouteConflict, and are not used. maxLength: 253 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string type: object client: description: |- - Client sets the request rate limits for this cluster. Omitted, they are 20 QPS with a burst - of 30. Refused beside inCluster. + Client sets the request rate limits for this cluster. Omitted, they are 20 + QPS with a burst of 30. Refused beside inCluster. properties: burst: - description: Burst is how many requests to this cluster may exceed - qps at once. Defaults to 30. + description: |- + Burst is how many requests to this cluster may exceed qps at once. Defaults + to 30. format: int32 minimum: 1 type: integer qps: - description: QPS is the sustained request rate to this cluster, - in queries per second. Defaults to 20. + description: |- + QPS is the sustained request rate to this cluster, in queries per second. + Defaults to 20. format: int32 minimum: 1 type: integer type: object inCluster: description: |- - InCluster makes this KubeProvider use the operator's own identity in the cluster it runs - in. It is honored only for a KubeProvider the installation lists in - --in-cluster-kube-providers; any other reports Stalled=True, reason InClusterNotAuthorized, - and is never used. Set exactly one of secretRef and inCluster. Immutable: delete and - recreate the KubeProvider to change it. + InCluster makes this KubeProvider use the operator's own identity in the + cluster it runs in. It is honored only for a KubeProvider the installation + lists in --in-cluster-kube-providers; any other reports Stalled=True, reason + InClusterNotAuthorized, and is never used. Set exactly one of secretRef and + inCluster. Immutable: delete and recreate the KubeProvider to change it. type: object secretRef: description: |- - SecretRef names the Secret, in this namespace, that holds a kubeconfig for the cluster. With - no key, the kubeconfig is read from "value", then "value.yaml". A kubeconfig that uses exec - auth, insecure-skip-tls-verify or credentials named by file path is refused with - Validated=False unless the installation allows it. Set exactly one of secretRef and - inCluster. Immutable: delete and recreate the KubeProvider to change it. + SecretRef names the Secret, in this namespace, that holds a kubeconfig for + the cluster. With no key, the kubeconfig is read from "value", then + "value.yaml". A kubeconfig that uses exec auth, insecure-skip-tls-verify or + credentials named by file path is refused with Validated=False unless the + installation allows it. Set exactly one of secretRef and inCluster. + Immutable: delete and recreate the KubeProvider to change it. properties: key: description: Key in the Secret, when not specified an implementation-specific @@ -499,23 +519,25 @@ spec: properties: auditRoute: description: |- - AuditRoute is the route in use: spec.attribution.auditRoute, or . when that - is empty. The AuditFactsReceived condition describes this route, and starts over when it - changes. + AuditRoute is the route in use: spec.attribution.auditRoute, or + . when that is empty. The AuditFactsReceived condition + describes this route, and starts over when it changes. type: string conditions: description: |- - Conditions are the latest observations of this KubeProvider's state. Ready, Reconciling and - Stalled follow kstatus; Stalled=True with reason InClusterNotAuthorized or - AuditRouteConflict means the KubeProvider is not used at all. Validated says whether the - credential is safe and readable, checked without connecting. + Conditions are the latest observations of this KubeProvider's state. Ready, + Reconciling and Stalled follow kstatus; Stalled=True with reason + InClusterNotAuthorized or AuditRouteConflict means the KubeProvider is not + used at all. Validated says whether the credential is safe and readable, + checked without connecting. - AuditFactsReceived, which is not part of Ready, says whether an attribution fact has ever - arrived on status.auditRoute. It is True (Received) from the first one on and never goes - back. Until then it is Unknown, and the reason says where to look: TransportUnavailable (the - fact store refuses writes), NoAuditDelivery (no audit events arrive at all), or RouteUnused - (audit events arrive, but never on this route). It is absent when author attribution is - off. + AuditFactsReceived, which is not part of Ready, says whether an attribution + fact has ever arrived on status.auditRoute. It is True (Received) from the + first one on and never goes back. Until then it is Unknown, and the reason + says where to look: TransportUnavailable (the fact store refuses writes), + NoAuditDelivery (no audit events arrive at all), or RouteUnused (audit + events arrive, but never on this route). It is absent when author + attribution is off. items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/bases/configbutler.ai_watchrules.yaml b/config/crd/bases/configbutler.ai_watchrules.yaml index e1adbfcc..095d6388 100644 --- a/config/crd/bases/configbutler.ai_watchrules.yaml +++ b/config/crd/bases/configbutler.ai_watchrules.yaml @@ -50,9 +50,10 @@ spec: schema: openAPIV3Schema: description: |- - WatchRule selects namespaced resources for a GitTarget in its namespace to mirror, by type, - API group, version, source namespace and label. Each rule reads one source namespace: this - WatchRule's own by default. Use a ClusterWatchRule for cluster-scoped types. + WatchRule selects namespaced resources for a GitTarget in its namespace to + mirror, by type, API group, version, source namespace and label. Each rule + reads one source namespace: this WatchRule's own by default. Use a + ClusterWatchRule for cluster-scoped types. properties: apiVersion: description: |- @@ -76,8 +77,8 @@ spec: properties: gitTargetRef: description: |- - GitTargetRef names the GitTarget, in this namespace, that mirrors what this WatchRule - selects. + GitTargetRef names the GitTarget, in this namespace, that mirrors what this + WatchRule selects. properties: name: description: Name of the referent. @@ -90,28 +91,29 @@ spec: rule: self.name != '' rules: description: |- - Rules select the resources to mirror. A resource matching any rule is mirrored. Each rule - names resource types, optionally narrowed by API group, version, source namespace and - labels. The GitTarget's spec.prune.mode decides whether a removal on the cluster deletes the - Git document. + Rules select the resources to mirror. A resource matching any rule is + mirrored. Each rule names resource types, optionally narrowed by API group, + version, source namespace and labels. The GitTarget's spec.prune.mode + decides whether a removal on the cluster deletes the Git document. items: description: |- - ResourceRule selects namespaced resources by type, and optionally by API group, version, source - namespace and label. + ResourceRule selects namespaced resources by type, and optionally by API + group, version, source namespace and label. properties: apiGroups: description: |- - APIGroups are the API groups to match: "" is the core group, and "*" is every group, as in - ["", "apps"] or ["*"]. Omitted, each resource name is looked up across the served groups and - must be served by exactly one. + APIGroups are the API groups to match: "" is the core group, and "*" is + every group, as in ["", "apps"] or ["*"]. Omitted, each resource name is + looked up across the served groups and must be served by exactly one. items: type: string type: array apiVersions: description: |- - APIVersions are the versions to match, or "*" for every served version. Omitted, each - resource's preferred version is used. The built-in Git path has no version, so two versions - of one object share a file unless the GitTarget's placement template includes {version}. + APIVersions are the versions to match, or "*" for every served version. + Omitted, each resource's preferred version is used. The built-in Git path + has no version, so two versions of one object share a file unless the + GitTarget's placement template includes {version}. items: type: string type: array @@ -176,9 +178,10 @@ spec: x-kubernetes-map-type: atomic resources: description: |- - Resources are the resource types to match, by plural name ("configmaps", "deployments"), or - "*" for every type. Matching is exact and case-insensitive; partial wildcards such as "pod*" - match nothing, and subresources ("pods/log") are rejected. For a custom resource whose name + Resources are the resource types to match, by plural name ("configmaps", + "deployments"), or "*" for every type. Matching is exact and + case-insensitive; partial wildcards such as "pod*" match nothing, and + subresources ("pods/log") are rejected. For a custom resource whose name more than one group serves, set apiGroups. items: minLength: 1 @@ -188,15 +191,15 @@ spec: type: array sourceNamespace: description: |- - SourceNamespace is the source-cluster namespace this rule reads: omitted for this - WatchRule's own namespace, a name for another one, or "*" for every namespace the - KubeProvider's credential can read. Git placement follows each object's own namespace, not - this field. + SourceNamespace is the source-cluster namespace this rule reads: omitted for + this WatchRule's own namespace, a name for another one, or "*" for every + namespace the KubeProvider's credential can read. Git placement follows each + object's own namespace, not this field. - The KubeProvider's spec.allowedGitTargets must allow the namespace, through the - sourceNamespaces of a grant admitting the GitTarget; "*" needs "*" there. One rule outside - the grants refuses the whole WatchRule (SourceNamespaceNotAllowed), checked when it - reconciles. + The KubeProvider's spec.allowedGitTargets must allow the namespace, through + the sourceNamespaces of a grant admitting the GitTarget; "*" needs "*" + there. One rule outside the grants refuses the whole WatchRule + (SourceNamespaceNotAllowed), checked when it reconciles. maxLength: 63 pattern: ^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)$ type: string @@ -214,9 +217,10 @@ spec: properties: conditions: description: |- - Conditions are the latest observations of this WatchRule's state. Ready, Reconciling and - Stalled follow kstatus; ResourcesResolved, SourceNamespaceAuthorized and StreamsRunning say - which check holds the rule back. + Conditions are the latest observations of this WatchRule's state. Ready, + Reconciling and Stalled follow kstatus; ResourcesResolved, + SourceNamespaceAuthorized and StreamsRunning say which check holds the rule + back. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -307,8 +311,8 @@ spec: type: integer summary: description: |- - Summary is ready/total for display, such as "3/4". Read ready and total instead of parsing - it. + Summary is ready/total for display, such as "3/4". Read ready and total + instead of parsing it. type: string total: description: Total is how many types are watched. From 400ae7aa788ca95e078842dbbe056a2ee2dc1762 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 08:18:46 +0000 Subject: [PATCH 08/15] chore(lint): keep API doc comments within 76 characters lint-api-doc-width, part of task lint, fails on any type or field doc comment line in api/ wider than 76 characters after the comment prefix: kubectl explain keeps each line break of a description and wraps every line again, so a wider line leaves its last words on a line of their own. It reads the Go source rather than config/crd/bases. The CRDs also carry descriptions controller-gen copies from upstream types, such as metav1.Condition and LabelSelector, and about 150 of those lines are wider than 76 with no way to rewrap them here. The source holds exactly the descriptions this repository writes, and a finding names the file and line. The task has rooted sources, so a second task lint run finds it cached, as CI asserts. AGENTS.md gains the limit in its CRD house style. Co-Authored-By: Claude Opus 5.5 (1M context) --- .golangci.yml | 5 + AGENTS.md | 6 +- Taskfile-build.yml | 19 ++++ hack/apidocwidth/main.go | 183 ++++++++++++++++++++++++++++++++++ hack/apidocwidth/main_test.go | 135 +++++++++++++++++++++++++ 5 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 hack/apidocwidth/main.go create mode 100644 hack/apidocwidth/main_test.go diff --git a/.golangci.yml b/.golangci.yml index d826bb6b..ca80b8a7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -390,6 +390,11 @@ linters: # caller that supplies a different path is its own test, into t.TempDir(). - path: 'hack/crdfields/.*\.go' linters: [forbidigo, gosec] + # The API doc width check is a standalone dev CLI as well: its report is console + # output (forbidigo), and it reads the Go files it walks under the directories it is + # given, api/ by default (gosec G122/G304). It writes nothing. + - path: 'hack/apidocwidth/.*\.go' + linters: [forbidigo, gosec] # Allow fmt.Println in metrics exporter (initialization logs) - path: 'internal/telemetry/.*\.go' linters: [forbidigo] diff --git a/AGENTS.md b/AGENTS.md index f2ccfd89..08ee5e96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,7 +118,11 @@ See [Documentation checks](./CONTRIBUTING.md#documentation-checks). Field paths bare (`spec.prune.mode`), literal values in double quotes (`"Reapply"`), no backticks, no capitals for emphasis, no em dashes, no `docs/` paths and no implementation vocabulary the reader cannot see (branch worker, fact index). Aim for under 80 words; the few - fields that need more get short paragraphs. Conditions open with `Conditions are the latest + fields that need more get short paragraphs. Keep every line of doc comment text to 76 + characters, measured after the `//` prefix and its space: `kubectl explain` keeps each line break and wraps every + line again, so a longer line leaves its last words on a line of their own + ([measurements](./docs/facts/crd-descriptions-in-kubectl-explain.md)). `task lint` checks + it (`lint-api-doc-width`). Conditions open with `Conditions are the latest observations of this 's state.` and `spec`/`status` with `defines the desired/observed state of .` An array item's Go type doc is also user-visible: it becomes `items`. - **Where that block goes matters, and getting it wrong fails silently.** On a FIELD, put it above diff --git a/Taskfile-build.yml b/Taskfile-build.yml index 67aad210..676902cb 100644 --- a/Taskfile-build.yml +++ b/Taskfile-build.yml @@ -246,6 +246,25 @@ tasks: - lint-actions - lint-helm - lint-docs + - lint-api-doc-width + + lint-api-doc-width: + desc: Check that every API doc comment line fits in 76 characters, so kubectl explain prints it whole + # kubectl explain keeps a CRD description's line breaks and wraps each line again at + # 76 characters of text, so a wider line leaves its last words on a line of their own. + # See docs/facts/crd-descriptions-in-kubectl-explain.md. + # + # It reads api/ rather than config/crd/bases, so it needs no `manifests` dep: the CRDs + # also carry descriptions copied from upstream types (metav1.Condition, LabelSelector) + # that this repository cannot rewrap. hack/apidocwidth says more. + # + # Rooted globs only. CI asserts that a second `task lint` run finds this task up to + # date, and an unrooted `**` would walk external-sources/. + sources: + - api/**/*.go + - hack/apidocwidth/*.go + cmds: + - go run ./hack/apidocwidth lint-docs: desc: All documentation checks — references, markdown structure, prose, and metric names diff --git a/hack/apidocwidth/main.go b/hack/apidocwidth/main.go new file mode 100644 index 00000000..2e5457fc --- /dev/null +++ b/hack/apidocwidth/main.go @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Command apidocwidth checks that every line of an API doc comment fits in 76 +// characters after the "// " prefix. +// +// A type or field doc comment under api/ becomes a CRD description, which is the +// text an operator reads in `kubectl explain`. kubectl keeps the comment's line +// breaks and wraps each line again at 76 characters of text, so a longer line leaves +// its last words on a line of their own. The measurements are in +// docs/facts/crd-descriptions-in-kubectl-explain.md. +// +// It reads the Go source rather than config/crd/bases. The CRDs also carry the +// descriptions controller-gen copies from upstream types (metav1.Condition, +// LabelSelector, Flux's object references, apiVersion and kind), which this +// repository cannot rewrap: about 150 of their lines are wider than 76. The source +// holds exactly the descriptions written here, and a finding in it names the file and +// line to fix rather than a schema path. +// +// Only the doc comments of type declarations and struct fields are measured, because +// those are the only comments controller-gen turns into descriptions. Marker lines +// (`+kubebuilder:...`, `+optional`) never reach the CRD, so they are skipped. A +// rationale block needs no special case: it is separated from the doc comment by a +// blank line, so Go does not attach it as the doc comment at all. +// +// Usage: +// +// apidocwidth [-width N] [dir ...] +// +// The directories default to api. Exits non-zero and prints file:line for every line +// that is too wide. +package main + +import ( + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/fs" + "os" + "path/filepath" + "strings" + "unicode/utf8" +) + +const ( + defaultWidth = 76 + exitFindings = 1 + exitUsage = 2 +) + +// finding is one doc comment line wider than the limit. +type finding struct { + pos token.Position + width int + text string +} + +func main() { + width := flag.Int("width", defaultWidth, "maximum characters per doc comment line, after the \"// \" prefix") + flag.Parse() + dirs := flag.Args() + if len(dirs) == 0 { + dirs = []string{"api"} + } + + var findings []finding + files := 0 + for _, dir := range dirs { + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !isAPISource(path) { + return nil + } + src, err := os.ReadFile(path) + if err != nil { + return err + } + found, err := checkFile(path, src, *width) + if err != nil { + return err + } + files++ + findings = append(findings, found...) + return nil + }) + if err != nil { + fmt.Fprintf(os.Stderr, "apidocwidth: %v\n", err) + os.Exit(exitUsage) + } + } + + for _, f := range findings { + fmt.Printf("%s:%d: %d characters, over %d: %s\n", f.pos.Filename, f.pos.Line, f.width, *width, f.text) + } + if len(findings) > 0 { + fmt.Fprintf(os.Stderr, "apidocwidth: %d doc comment lines are wider than %d characters; "+ + "kubectl explain prints their last words on a line of their own\n", len(findings), *width) + os.Exit(exitFindings) + } + fmt.Printf("apidocwidth: %d files, every API doc comment line fits in %d characters\n", files, *width) +} + +// isAPISource reports whether path is hand-written Go that controller-gen reads: not a +// test, and not generated deepcopy code. +func isAPISource(path string) bool { + base := filepath.Base(path) + return strings.HasSuffix(base, ".go") && + !strings.HasSuffix(base, "_test.go") && + !strings.HasPrefix(base, "zz_generated") +} + +// checkFile returns every type and struct-field doc comment line in src wider than width. +func checkFile(filename string, src []byte, width int) ([]finding, error) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, filename, src, parser.ParseComments) + if err != nil { + return nil, err + } + var findings []finding + for _, doc := range descriptionDocs(file) { + if doc == nil { + continue + } + for _, c := range doc.List { + text, ok := docText(c.Text) + if !ok { + continue + } + if n := utf8.RuneCountInString(text); n > width { + findings = append(findings, finding{pos: fset.Position(c.Pos()), width: n, text: text}) + } + } + } + return findings, nil +} + +// descriptionDocs returns the doc comments controller-gen can turn into descriptions: those +// of type declarations and of struct fields. +func descriptionDocs(file *ast.File) []*ast.CommentGroup { + var docs []*ast.CommentGroup + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.TYPE { + continue + } + for _, spec := range gen.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + // A lone `type X struct` carries its doc on the declaration, a grouped one on the + // spec; controller-gen reads whichever is set, and so does this. + doc := typeSpec.Doc + if doc == nil { + doc = gen.Doc + } + docs = append(docs, doc) + if structType, ok := typeSpec.Type.(*ast.StructType); ok { + for _, field := range structType.Fields.List { + docs = append(docs, field.Doc) + } + } + } + } + return docs +} + +// docText is the description text of one comment line, and false for a line that never +// reaches a description: a marker, or a /* */ comment. +func docText(comment string) (string, bool) { + text, ok := strings.CutPrefix(comment, "//") + if !ok { + return "", false + } + text = strings.TrimPrefix(text, " ") + if strings.HasPrefix(text, "+") { + return "", false + } + return text, true +} diff --git a/hack/apidocwidth/main_test.go b/hack/apidocwidth/main_test.go new file mode 100644 index 00000000..da46873b --- /dev/null +++ b/hack/apidocwidth/main_test.go @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "strings" + "testing" +) + +// line returns a doc comment text of exactly n characters. +func line(n int) string { + return strings.Repeat("x", n) +} + +func TestCheckFile_MeasuresTypeAndFieldDocComments(t *testing.T) { + src := `package v1 + +// Spec is a type doc comment that is too wide: ` + line(60) + ` +type Spec struct { + // Field is a field doc comment that is too wide: ` + line(60) + ` + Field string +} +` + findings, err := checkFile("types.go", []byte(src), defaultWidth) + if err != nil { + t.Fatal(err) + } + if len(findings) != 2 { + t.Fatalf("want 2 findings, got %d: %+v", len(findings), findings) + } + if findings[0].pos.Line != 3 || findings[1].pos.Line != 5 { + t.Errorf("want findings on lines 3 and 5, got %d and %d", findings[0].pos.Line, findings[1].pos.Line) + } +} + +func TestCheckFile_LimitIsInclusive(t *testing.T) { + for _, tc := range []struct { + name string + width int + want int + }{ + {name: "exactly the limit", width: defaultWidth, want: 0}, + {name: "one over", width: defaultWidth + 1, want: 1}, + } { + t.Run(tc.name, func(t *testing.T) { + src := "package v1\n\ntype Spec struct {\n\t// " + line(tc.width) + "\n\tField string\n}\n" + findings, err := checkFile("types.go", []byte(src), defaultWidth) + if err != nil { + t.Fatal(err) + } + if len(findings) != tc.want { + t.Errorf("want %d findings, got %d", tc.want, len(findings)) + } + }) + } +} + +func TestCheckFile_CountsCharactersNotBytes(t *testing.T) { + // Each en dash is three bytes and one character, as kubectl explain counts it. + src := "package v1\n\ntype Spec struct {\n\t// " + strings.Repeat("–", defaultWidth) + "\n\tField string\n}\n" + findings, err := checkFile("types.go", []byte(src), defaultWidth) + if err != nil { + t.Fatal(err) + } + if len(findings) != 0 { + t.Errorf("want no findings for %d characters, got %+v", defaultWidth, findings) + } +} + +func TestCheckFile_SkipsTextThatNeverReachesTheCRD(t *testing.T) { + src := `package v1 + +// A rationale block, separated from the doc comment by a blank line: ` + line(60) + ` + +// Spec is short. +// +kubebuilder:validation:XValidation:rule="` + line(80) + `" +type Spec struct { + // A field rationale block: ` + line(80) + ` + + // Field is short. + // +kubebuilder:validation:Pattern=` + "`" + line(80) + "`" + ` + Field string +} + +// Helper is a function, whose doc comment is not a description: ` + line(60) + ` +func Helper() {} + +// Constant is not a description either: ` + line(60) + ` +const Constant = 1 +` + findings, err := checkFile("types.go", []byte(src), defaultWidth) + if err != nil { + t.Fatal(err) + } + if len(findings) != 0 { + t.Errorf("want no findings, got %+v", findings) + } +} + +func TestCheckFile_ReadsAGroupedTypeSpecsOwnDoc(t *testing.T) { + src := `package v1 + +// The group's doc is not any one type's: ` + line(60) + ` +type ( + // Mode is too wide: ` + line(70) + ` + Mode string +) +` + findings, err := checkFile("types.go", []byte(src), defaultWidth) + if err != nil { + t.Fatal(err) + } + if len(findings) != 1 || findings[0].pos.Line != 5 { + t.Errorf("want one finding on line 5, got %+v", findings) + } +} + +func TestIsAPISource(t *testing.T) { + for path, want := range map[string]bool{ + "api/v1alpha3/gittarget_types.go": true, + "api/v1alpha3/gittarget_types_test.go": false, + "api/v1alpha3/zz_generated.deepcopy.go": false, + "api/v1alpha3/README.md": false, + } { + if got := isAPISource(path); got != want { + t.Errorf("isAPISource(%q) = %v, want %v", path, got, want) + } + } +} + +func TestCheckFile_ReportsAParseError(t *testing.T) { + if _, err := checkFile("broken.go", []byte("package v1\n\ntype {"), defaultWidth); err == nil { + t.Error("want a parse error, got nil") + } +} From 4089fa7e3930176410ce0ec25477fac2bdb50eb1 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 08:20:04 +0000 Subject: [PATCH 09/15] docs(api): list the choices in grant and receiver descriptions ConsumerNamespaces.from and GitReceiver.spec.type give each value its own bullet. GitBranch.spec.allowedGitTargets and both KubeProvider grant lists separate the omitted, empty, and explicit cases, and keep their defaults for source namespaces and applier namespaces. GitProvider.spec.allowedGitBranches is unchanged: omitted and empty both deny every GitBranch there. AGENTS.md says when to use bullets in a CRD description. Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 3 +- api/v1alpha3/access_grants.go | 16 +++-- api/v1alpha3/gitbranch_types.go | 13 ++-- api/v1alpha3/gitreceiver_types.go | 13 ++-- api/v1alpha3/kubeprovider_types.go | 36 +++++++--- .../bases/configbutler.ai_gitbranches.yaml | 29 +++++--- .../bases/configbutler.ai_gitproviders.yaml | 16 +++-- .../bases/configbutler.ai_gitreceivers.yaml | 13 ++-- .../bases/configbutler.ai_kubeproviders.yaml | 68 ++++++++++++------- 9 files changed, 136 insertions(+), 71 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 08ee5e96..d3f6af75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,7 +122,8 @@ See [Documentation checks](./CONTRIBUTING.md#documentation-checks). characters, measured after the `//` prefix and its space: `kubectl explain` keeps each line break and wraps every line again, so a longer line leaves its last words on a line of their own ([measurements](./docs/facts/crd-descriptions-in-kubectl-explain.md)). `task lint` checks - it (`lint-api-doc-width`). Conditions open with `Conditions are the latest + it (`lint-api-doc-width`). Use `-` bullets, continued with a two-space indent, when the + reader compares values or cases; keep prose where there is no choice. Conditions open with `Conditions are the latest observations of this 's state.` and `spec`/`status` with `defines the desired/observed state of .` An array item's Go type doc is also user-visible: it becomes `items`. - **Where that block goes matters, and getting it wrong fails silently.** On a FIELD, put it above diff --git a/api/v1alpha3/access_grants.go b/api/v1alpha3/access_grants.go index 529728fa..8518a336 100644 --- a/api/v1alpha3/access_grants.go +++ b/api/v1alpha3/access_grants.go @@ -96,21 +96,25 @@ type BranchPattern string // +kubebuilder:validation:XValidation:rule="!has(self.selector) || self.from == 'Selector'",message="selector goes only with from: Selector" // +kubebuilder:validation:XValidation:rule="!has(self.selector) || (has(self.selector.matchLabels) && size(self.selector.matchLabels) > 0) || (has(self.selector.matchExpressions) && size(self.selector.matchExpressions) > 0)",message="selector: {} is refused; to admit every namespace, use from: All" type ConsumerNamespaces struct { - // From is where this grant admits consumers from: "Same" (the namespace of the - // object that holds the grant), "Names" (the namespaces in names), "Selector" - // (the namespaces whose labels match selector) or "All" (every namespace). + // From selects which namespaces this grant allows consumers from: + // + // - "Same": the namespace of the object that holds the grant. + // - "Names": the namespaces listed in names. + // - "Selector": the namespaces whose labels match selector. + // - "All": every namespace. // +required From GrantFrom `json:"from"` - // Names are the namespaces admitted with from: "Names", by exact name. + // Names are the namespaces allowed with from: "Names", by exact name. // +optional // +listType=set // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=64 Names []GrantNamespace `json:"names,omitempty"` - // Selector matches the labels of the namespaces admitted with from: - // "Selector". It must not be empty: to admit every namespace, use from: "All". + // Selector matches the labels of the namespaces allowed with from: + // "Selector". It must not be empty: to allow every namespace, use from: + // "All". // +optional Selector *metav1.LabelSelector `json:"selector,omitempty"` } diff --git a/api/v1alpha3/gitbranch_types.go b/api/v1alpha3/gitbranch_types.go index 937c7a66..3250bf53 100644 --- a/api/v1alpha3/gitbranch_types.go +++ b/api/v1alpha3/gitbranch_types.go @@ -60,10 +60,15 @@ type GitBranchSpec struct { // Mutable, unlike the fields above: claiming a branch includes the right to share it. - // AllowedGitTargets says which GitTargets may write through this GitBranch. - // Omitted, it is one grant for this namespace (consumerNamespaces: {from: - // Same}). A written list is the whole policy, so it admits this namespace only - // if a grant says so, and [] admits nobody. Grants combine by union. + // AllowedGitTargets says which GitTargets may write through this GitBranch: + // + // - Omitted: the GitTargets in this GitBranch's namespace, as one grant + // with from: "Same". + // - Empty list ([]): no GitTarget. + // - A list of grants: the GitTargets that at least one grant allows. + // + // A list replaces the default, so it needs a grant with from: "Same" for + // the GitTargets in this namespace to keep access. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 diff --git a/api/v1alpha3/gitreceiver_types.go b/api/v1alpha3/gitreceiver_types.go index 56b6f623..0cff1ede 100644 --- a/api/v1alpha3/gitreceiver_types.go +++ b/api/v1alpha3/gitreceiver_types.go @@ -17,11 +17,14 @@ import ( // GitReceiverSpec defines the desired state of GitReceiver. type GitReceiverSpec struct { - // Type is the payload and signature the Git host sends: "github" - // (X-Hub-Signature-256), "gitlab" (X-Gitlab-Token), "gitea" - // (X-Gitea-Signature), or "generic-hmac" for a relay of your own (X-Signature: - // sha256=, over a {"repository", "branch", "after"} - // body). + // Type is the payload and signature format the Git host sends: + // + // - "github": GitHub's payload, signed in X-Hub-Signature-256. + // - "gitlab": GitLab's payload, with the secret in X-Gitlab-Token. + // - "gitea": Gitea's payload, signed in X-Gitea-Signature. + // - "generic-hmac": a relay of your own. It sends a JSON object with + // repository, branch and after fields, signed in X-Signature as + // sha256=. // +required // +kubebuilder:validation:Enum=github;gitlab;gitea;generic-hmac Type ReceiverType `json:"type"` diff --git a/api/v1alpha3/kubeprovider_types.go b/api/v1alpha3/kubeprovider_types.go index d3b18903..e19044b6 100644 --- a/api/v1alpha3/kubeprovider_types.go +++ b/api/v1alpha3/kubeprovider_types.go @@ -48,12 +48,19 @@ type KubeProviderSpec struct { InCluster *KubeProviderInCluster `json:"inCluster,omitempty"` // AllowedGitTargets says which GitTargets may mirror through this - // KubeProvider, and what. Each grant admits GitTargets from some namespaces, - // bounds which source namespaces their WatchRules may read (sourceNamespaces), - // and lists the cluster-scoped types their ClusterWatchRules may mirror - // (clusterResources). Omitted, it is one grant for this namespace, reading the - // source namespace of the same name and no cluster-scoped type. A written list - // is the whole policy, and [] admits nobody. Grants combine by union. + // KubeProvider, and what. Each grant allows the GitTargets of some + // namespaces, bounds which source namespaces their WatchRules may read + // (sourceNamespaces), and lists the cluster-scoped types their + // ClusterWatchRules may mirror (clusterResources). + // + // - Omitted: the GitTargets in this KubeProvider's namespace, reading only + // the source namespace of the same name and no cluster-scoped type. + // - Empty list ([]): no GitTarget. + // - A list of grants: the GitTargets that at least one grant allows, with + // what every grant that allows them permits. + // + // A list replaces the default, so it needs a grant with from: "Same" for + // the GitTargets in this namespace to keep access. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 @@ -61,11 +68,18 @@ type KubeProviderSpec struct { AllowedGitTargets []KubeProviderGitTargetGrant `json:"allowedGitTargets"` // AllowedApplierClaims says which FluxClaims and ArgoCDClaims may hold an - // applier through this KubeProvider, and in which namespaces that applier may - // be (applierNamespaces). Being admitted to mirror does not admit to apply. - // Omitted, it is one grant for this namespace, holding appliers in the - // namespace of the same name. A written list is the whole policy, and [] - // admits nobody. Grants combine by union. + // applier through this KubeProvider, and in which namespaces that applier + // may be (applierNamespaces). A namespace in allowedGitTargets gets no + // applier access from that. + // + // - Omitted: the claims in this KubeProvider's namespace, holding appliers + // in the namespace of the same name. + // - Empty list ([]): no claim. + // - A list of grants: the claims that at least one grant allows, holding + // appliers in the applierNamespaces of every grant that allows them. + // + // A list replaces the default, so it needs a grant with from: "Same" for + // the claims in this namespace to keep access. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 diff --git a/config/crd/bases/configbutler.ai_gitbranches.yaml b/config/crd/bases/configbutler.ai_gitbranches.yaml index 3a5a860b..e2007671 100644 --- a/config/crd/bases/configbutler.ai_gitbranches.yaml +++ b/config/crd/bases/configbutler.ai_gitbranches.yaml @@ -86,10 +86,15 @@ spec: - consumerNamespaces: from: Same description: |- - AllowedGitTargets says which GitTargets may write through this GitBranch. - Omitted, it is one grant for this namespace (consumerNamespaces: {from: - Same}). A written list is the whole policy, so it admits this namespace only - if a grant says so, and [] admits nobody. Grants combine by union. + AllowedGitTargets says which GitTargets may write through this GitBranch: + + - Omitted: the GitTargets in this GitBranch's namespace, as one grant + with from: "Same". + - Empty list ([]): no GitTarget. + - A list of grants: the GitTargets that at least one grant allows. + + A list replaces the default, so it needs a grant with from: "Same" for + the GitTargets in this namespace to keep access. items: description: |- GitBranchGitTargetGrant admits the GitTargets of some namespaces to write @@ -102,9 +107,12 @@ spec: properties: from: description: |- - From is where this grant admits consumers from: "Same" (the namespace of the - object that holds the grant), "Names" (the namespaces in names), "Selector" - (the namespaces whose labels match selector) or "All" (every namespace). + From selects which namespaces this grant allows consumers from: + + - "Same": the namespace of the object that holds the grant. + - "Names": the namespaces listed in names. + - "Selector": the namespaces whose labels match selector. + - "All": every namespace. enum: - Same - Names @@ -112,7 +120,7 @@ spec: - All type: string names: - description: 'Names are the namespaces admitted with from: + description: 'Names are the namespaces allowed with from: "Names", by exact name.' items: description: |- @@ -139,8 +147,9 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches the labels of the namespaces admitted with from: - "Selector". It must not be empty: to admit every namespace, use from: "All". + Selector matches the labels of the namespaces allowed with from: + "Selector". It must not be empty: to allow every namespace, use from: + "All". properties: matchExpressions: description: matchExpressions is a list of label selector diff --git a/config/crd/bases/configbutler.ai_gitproviders.yaml b/config/crd/bases/configbutler.ai_gitproviders.yaml index 1e3048be..f9c59936 100644 --- a/config/crd/bases/configbutler.ai_gitproviders.yaml +++ b/config/crd/bases/configbutler.ai_gitproviders.yaml @@ -138,9 +138,12 @@ spec: properties: from: description: |- - From is where this grant admits consumers from: "Same" (the namespace of the - object that holds the grant), "Names" (the namespaces in names), "Selector" - (the namespaces whose labels match selector) or "All" (every namespace). + From selects which namespaces this grant allows consumers from: + + - "Same": the namespace of the object that holds the grant. + - "Names": the namespaces listed in names. + - "Selector": the namespaces whose labels match selector. + - "All": every namespace. enum: - Same - Names @@ -148,7 +151,7 @@ spec: - All type: string names: - description: 'Names are the namespaces admitted with from: + description: 'Names are the namespaces allowed with from: "Names", by exact name.' items: description: |- @@ -175,8 +178,9 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches the labels of the namespaces admitted with from: - "Selector". It must not be empty: to admit every namespace, use from: "All". + Selector matches the labels of the namespaces allowed with from: + "Selector". It must not be empty: to allow every namespace, use from: + "All". properties: matchExpressions: description: matchExpressions is a list of label selector diff --git a/config/crd/bases/configbutler.ai_gitreceivers.yaml b/config/crd/bases/configbutler.ai_gitreceivers.yaml index 268a50ce..3b6d966e 100644 --- a/config/crd/bases/configbutler.ai_gitreceivers.yaml +++ b/config/crd/bases/configbutler.ai_gitreceivers.yaml @@ -85,11 +85,14 @@ spec: rule: self.name != '' type: description: |- - Type is the payload and signature the Git host sends: "github" - (X-Hub-Signature-256), "gitlab" (X-Gitlab-Token), "gitea" - (X-Gitea-Signature), or "generic-hmac" for a relay of your own (X-Signature: - sha256=, over a {"repository", "branch", "after"} - body). + Type is the payload and signature format the Git host sends: + + - "github": GitHub's payload, signed in X-Hub-Signature-256. + - "gitlab": GitLab's payload, with the secret in X-Gitlab-Token. + - "gitea": Gitea's payload, signed in X-Gitea-Signature. + - "generic-hmac": a relay of your own. It sends a JSON object with + repository, branch and after fields, signed in X-Signature as + sha256=. enum: - github - gitlab diff --git a/config/crd/bases/configbutler.ai_kubeproviders.yaml b/config/crd/bases/configbutler.ai_kubeproviders.yaml index 20b4ce68..12a480bf 100644 --- a/config/crd/bases/configbutler.ai_kubeproviders.yaml +++ b/config/crd/bases/configbutler.ai_kubeproviders.yaml @@ -77,11 +77,18 @@ spec: from: Same description: |- AllowedApplierClaims says which FluxClaims and ArgoCDClaims may hold an - applier through this KubeProvider, and in which namespaces that applier may - be (applierNamespaces). Being admitted to mirror does not admit to apply. - Omitted, it is one grant for this namespace, holding appliers in the - namespace of the same name. A written list is the whole policy, and [] - admits nobody. Grants combine by union. + applier through this KubeProvider, and in which namespaces that applier + may be (applierNamespaces). A namespace in allowedGitTargets gets no + applier access from that. + + - Omitted: the claims in this KubeProvider's namespace, holding appliers + in the namespace of the same name. + - Empty list ([]): no claim. + - A list of grants: the claims that at least one grant allows, holding + appliers in the applierNamespaces of every grant that allows them. + + A list replaces the default, so it needs a grant with from: "Same" for + the claims in this namespace to keep access. items: description: |- KubeProviderApplierClaimGrant admits the FluxClaims and ArgoCDClaims of some @@ -125,9 +132,12 @@ spec: properties: from: description: |- - From is where this grant admits consumers from: "Same" (the namespace of the - object that holds the grant), "Names" (the namespaces in names), "Selector" - (the namespaces whose labels match selector) or "All" (every namespace). + From selects which namespaces this grant allows consumers from: + + - "Same": the namespace of the object that holds the grant. + - "Names": the namespaces listed in names. + - "Selector": the namespaces whose labels match selector. + - "All": every namespace. enum: - Same - Names @@ -135,7 +145,7 @@ spec: - All type: string names: - description: 'Names are the namespaces admitted with from: + description: 'Names are the namespaces allowed with from: "Names", by exact name.' items: description: |- @@ -162,8 +172,9 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches the labels of the namespaces admitted with from: - "Selector". It must not be empty: to admit every namespace, use from: "All". + Selector matches the labels of the namespaces allowed with from: + "Selector". It must not be empty: to allow every namespace, use from: + "All". properties: matchExpressions: description: matchExpressions is a list of label selector @@ -241,12 +252,19 @@ spec: - '{consumer.namespace}' description: |- AllowedGitTargets says which GitTargets may mirror through this - KubeProvider, and what. Each grant admits GitTargets from some namespaces, - bounds which source namespaces their WatchRules may read (sourceNamespaces), - and lists the cluster-scoped types their ClusterWatchRules may mirror - (clusterResources). Omitted, it is one grant for this namespace, reading the - source namespace of the same name and no cluster-scoped type. A written list - is the whole policy, and [] admits nobody. Grants combine by union. + KubeProvider, and what. Each grant allows the GitTargets of some + namespaces, bounds which source namespaces their WatchRules may read + (sourceNamespaces), and lists the cluster-scoped types their + ClusterWatchRules may mirror (clusterResources). + + - Omitted: the GitTargets in this KubeProvider's namespace, reading only + the source namespace of the same name and no cluster-scoped type. + - Empty list ([]): no GitTarget. + - A list of grants: the GitTargets that at least one grant allows, with + what every grant that allows them permits. + + A list replaces the default, so it needs a grant with from: "Same" for + the GitTargets in this namespace to keep access. items: description: |- KubeProviderGitTargetGrant admits the GitTargets of some namespaces to @@ -284,9 +302,12 @@ spec: properties: from: description: |- - From is where this grant admits consumers from: "Same" (the namespace of the - object that holds the grant), "Names" (the namespaces in names), "Selector" - (the namespaces whose labels match selector) or "All" (every namespace). + From selects which namespaces this grant allows consumers from: + + - "Same": the namespace of the object that holds the grant. + - "Names": the namespaces listed in names. + - "Selector": the namespaces whose labels match selector. + - "All": every namespace. enum: - Same - Names @@ -294,7 +315,7 @@ spec: - All type: string names: - description: 'Names are the namespaces admitted with from: + description: 'Names are the namespaces allowed with from: "Names", by exact name.' items: description: |- @@ -321,8 +342,9 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches the labels of the namespaces admitted with from: - "Selector". It must not be empty: to admit every namespace, use from: "All". + Selector matches the labels of the namespaces allowed with from: + "Selector". It must not be empty: to allow every namespace, use from: + "All". properties: matchExpressions: description: matchExpressions is a list of label selector From 3f75d968b135844b24796974393cc5d723b5b5fd Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 08:23:03 +0000 Subject: [PATCH 10/15] docs(api): split the long CRD descriptions by topic Both claim kinds lead with control of apply timing and list what starts an apply. GitTarget.spec.placement separates existing documents, path variables, and fallbacks; spec.suspend separates stopping, the last push, and resuming; spec.prune.mode gives each mode a bullet, the default first. GitBranch.spec.parentBranch separates the default from a missing parent and names the GitProvider whose allowedGitBranches it means. GitProvider's knownHostsRef numbers its three sources and names the flag that permits missing host keys. KubeProvider's auditRoute separates which namespace owns a route from the conflict it causes. Co-Authored-By: Claude Opus 5.5 (1M context) --- api/v1alpha3/argocdclaim_types.go | 18 ++++-- api/v1alpha3/fluxclaim_types.go | 18 ++++-- api/v1alpha3/gitbranch_types.go | 20 +++--- api/v1alpha3/gitprovider_types.go | 12 +++- api/v1alpha3/gittarget_types.go | 50 +++++++++------ api/v1alpha3/kubeprovider_types.go | 20 +++--- api/v1alpha3/prune_policy.go | 11 ++-- .../bases/configbutler.ai_argocdclaims.yaml | 18 ++++-- .../crd/bases/configbutler.ai_fluxclaims.yaml | 18 ++++-- .../bases/configbutler.ai_gitbranches.yaml | 20 +++--- .../bases/configbutler.ai_gitproviders.yaml | 12 +++- .../crd/bases/configbutler.ai_gittargets.yaml | 61 +++++++++++-------- .../bases/configbutler.ai_kubeproviders.yaml | 20 +++--- 13 files changed, 190 insertions(+), 108 deletions(-) diff --git a/api/v1alpha3/argocdclaim_types.go b/api/v1alpha3/argocdclaim_types.go index 23df7ff9..f0ad8ea1 100644 --- a/api/v1alpha3/argocdclaim_types.go +++ b/api/v1alpha3/argocdclaim_types.go @@ -153,12 +153,18 @@ type ArgoCDClaimSource struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// ArgoCDClaim hands control of an Argo CD Application to GitOps Reverser. The -// Application's automated sync stays off, and GitOps Reverser starts each -// sync, always after the edits pending on the branch are published: after a -// commit it did not push, a refused or reverted edit, or an object the API -// created, and once at startup or when a GitTarget first names it. GitTargets -// name it with spec.applierClaimRef. +// ArgoCDClaim lets GitOps Reverser decide when an Argo CD Application syncs. +// The Application's automated sync stays off, and GitOps Reverser starts each +// sync, always after the edits pending on the branch are published. It starts +// one: +// +// - after a commit that GitOps Reverser did not push; +// - after a refused edit, on a GitTarget with spec.onRefusal "Reapply"; +// - after a sync reverted an edit made through the API; +// - after objects were created through the API in its folders; +// - once at startup, and when a GitTarget first names this ArgoCDClaim. +// +// GitTargets name it with spec.applierClaimRef. type ArgoCDClaim struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/fluxclaim_types.go b/api/v1alpha3/fluxclaim_types.go index f6211dcb..b4cb8e1c 100644 --- a/api/v1alpha3/fluxclaim_types.go +++ b/api/v1alpha3/fluxclaim_types.go @@ -156,12 +156,18 @@ type FluxClaimSource struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// FluxClaim hands control of a Flux Kustomization to GitOps Reverser. The -// Kustomization stays suspended, and GitOps Reverser resumes it for each apply -// it starts, always after the edits pending on the branch are published: after -// a commit it did not push, a refused or reverted edit, or an object the API -// created, and once at startup or when a GitTarget first names it. GitTargets -// name it with spec.applierClaimRef. +// FluxClaim lets GitOps Reverser decide when a Flux Kustomization applies. +// The Kustomization stays suspended, and GitOps Reverser resumes it for each +// apply it starts, always after the edits pending on the branch are +// published. It starts one: +// +// - after a commit that GitOps Reverser did not push; +// - after a refused edit, on a GitTarget with spec.onRefusal "Reapply"; +// - after an apply reverted an edit made through the API; +// - after objects were created through the API in its folders; +// - once at startup, and when a GitTarget first names this FluxClaim. +// +// GitTargets name it with spec.applierClaimRef. type FluxClaim struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha3/gitbranch_types.go b/api/v1alpha3/gitbranch_types.go index 3250bf53..796e8c4e 100644 --- a/api/v1alpha3/gitbranch_types.go +++ b/api/v1alpha3/gitbranch_types.go @@ -46,14 +46,18 @@ type GitBranchSpec struct { // parent changed under a worker holding writes is the one case that needed its own replay, so it // is immutable. It can never hold a commit or tag: a pinned starting point would be a sibling. - // ParentBranch is the branch spec.branch is created from, and compared against - // while spec.branch does not exist on the remote. Omitted, it is the remote's - // default branch, or no history at all in an empty repository. When set, it - // must exist on the remote for spec.branch to be created; otherwise this - // GitBranch reports ParentBranchNotFound and writes nothing. Once spec.branch - // exists, the parent branch is no longer followed. It is only read, so - // spec.allowedGitBranches need not allow it. Immutable, including adding or - // removing it: delete and recreate the GitBranch to change it. + // ParentBranch is the branch spec.branch is created from, and compared + // against while spec.branch does not exist on the remote. Omitted, it is the + // remote's default branch, or no history at all in an empty repository. + // + // When set, it must exist on the remote for spec.branch to be created; + // otherwise this GitBranch reports Stalled=True, reason ParentBranchNotFound, + // and writes nothing. Once spec.branch exists, the parent branch is no + // longer followed. It is only read, so the GitProvider's + // spec.allowedGitBranches need not allow it. + // + // Immutable, including adding or removing it: delete and recreate the + // GitBranch to change it. // +optional // +kubebuilder:validation:MinLength=1 ParentBranch string `json:"parentBranch,omitempty"` diff --git a/api/v1alpha3/gitprovider_types.go b/api/v1alpha3/gitprovider_types.go index cda08cd3..2de1033a 100644 --- a/api/v1alpha3/gitprovider_types.go +++ b/api/v1alpha3/gitprovider_types.go @@ -38,9 +38,15 @@ type GitProviderSpec struct { // KnownHostsRef names a ConfigMap or Secret, in this namespace, holding SSH // known_hosts, so several GitProviders on one host can share them. SSH only. - // Host keys come from the credentials Secret's own known_hosts first, then - // this reference, then the installation's default; without valid keys, SSH - // fails. + // + // Host keys come from the first of these that has any: + // + // 1. the known_hosts key of the credentials Secret, spec.secretRef; + // 2. this reference; + // 3. the installation's default. + // + // Without host keys, SSH fails unless the installation sets + // --insecure-allow-missing-known-hosts. // +optional KnownHostsRef *KnownHostsReference `json:"knownHostsRef,omitempty"` diff --git a/api/v1alpha3/gittarget_types.go b/api/v1alpha3/gittarget_types.go index b08ed7bb..9029e5cd 100644 --- a/api/v1alpha3/gittarget_types.go +++ b/api/v1alpha3/gittarget_types.go @@ -107,21 +107,26 @@ type GitTargetSpec struct { // identity inside a file and the write-time guards still refuse a sensitive document in a // shared one. - // Placement decides where a new resource is written: one with no document in - // the folder yet. A resource that already has a document is updated where that - // document is, so a change here affects only resources created afterward, and - // labeling a resource later never moves its file. + // Placement decides where a new resource is written: one with no document + // in the folder yet. A resource that already has a document is updated + // where that document is. So a change here affects only resources created + // afterward, and labeling a resource later never moves its file. // - // byType and default are path templates over the resource's identity - // ({namespace}, {name}, {group}, {version}, {resource}, {kind} and more), and + // byType and default are path templates over the resource's identity: + // {namespace}, {name}, {group}, {version}, {resource}, {kind} and more. They // may read a label: "{label:app.kubernetes.io/instance}/configmaps.yaml". - // "{label:key}" renders "_unlabeled" for a resource without the label, and - // "{namespace}" renders "_cluster" for a cluster-scoped one. Append - // "|fallback" to either to choose that value: "{label:team|unassigned}", - // "{namespace|_global}". A fallback is up to 63 characters of [A-Za-z0-9._-], - // is not "." or "..", and may be empty to drop the path segment. A {namespace} - // fallback that names a real namespace files cluster-scoped resources in that - // namespace's folder. A fallback on any other variable is rejected. + // + // Two variables can be absent, and each then renders a built-in value: + // + // - "{label:key}" renders "_unlabeled" for a resource without the label. + // - "{namespace}" renders "_cluster" for a cluster-scoped resource. + // + // Append "|fallback" to either to choose that value instead: + // "{label:team|unassigned}", "{namespace|_global}". A fallback is up to 63 + // characters of [A-Za-z0-9._-], is not "." or "..", and may be empty to + // drop the path segment. A {namespace} fallback that names a real namespace + // files cluster-scoped resources in that namespace's folder. A fallback on + // any other variable is rejected. // +optional Placement *GitTargetPlacementSpec `json:"placement,omitempty"` @@ -186,13 +191,18 @@ type GitTargetSpec struct { // out of order, on resume. status.retention is withheld because nothing is swept, and zero // would read as converged. - // Suspend stops this GitTarget from making new commits, without deleting it. - // Watches keep running and status.placement stays current; status.retention is - // not published while it is set. A commit already made locally is still - // pushed, so one more commit can appear seconds after it is set: suspend stops - // the next write and does not undo the last one. Clearing it resumes from the - // current cluster state at the next resync; writes skipped while suspended are - // not replayed. Defaults to false. + // Suspend stops this GitTarget from making new commits, without deleting + // it. Defaults to false. + // + // While it is set, watches keep running and status.placement stays current, + // but status.retention is not published. + // + // A commit already made locally is still pushed, so one more commit can + // appear seconds after it is set: suspend stops the next write and does not + // undo the last one. + // + // Clearing it resumes from the current cluster state at the next resync. + // Writes skipped while suspended are not replayed. // +optional Suspend bool `json:"suspend,omitempty"` } diff --git a/api/v1alpha3/kubeprovider_types.go b/api/v1alpha3/kubeprovider_types.go index e19044b6..a8f4b4fb 100644 --- a/api/v1alpha3/kubeprovider_types.go +++ b/api/v1alpha3/kubeprovider_types.go @@ -128,15 +128,21 @@ type KubeProviderAttribution struct { // in the API server's audit webhook URL, /audit-webhook/. When several // logical clusters share one webhook backend, it is instead the value of the // audit-event annotation that --author-attribution-audit-route-annotation-key - // names. KubeProviders with the same route read the same cluster's attribution - // facts. + // names. KubeProviders with the same route read the same cluster's + // attribution facts. // // Empty means .. Set it when several KubeProviders in this - // namespace connect to one cluster, since an API server posts under one route. - // A route belongs to one namespace: the one it starts with (.) when - // a KubeProvider there carries it, and otherwise the namespace of the oldest - // KubeProvider carrying it. KubeProviders in another namespace that carry it - // report Stalled=True, reason AuditRouteConflict, and are not used. + // namespace connect to one cluster, since an API server posts under one + // route. + // + // A route belongs to one namespace: + // + // - the namespace the route starts with (.), when a KubeProvider + // there carries it; + // - otherwise, the namespace of the oldest KubeProvider carrying it. + // + // KubeProviders in another namespace that carry the route report + // Stalled=True, reason AuditRouteConflict, and are not used. // +optional // +kubebuilder:validation:MaxLength=253 // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` diff --git a/api/v1alpha3/prune_policy.go b/api/v1alpha3/prune_policy.go index 21dabbc9..f99434e6 100644 --- a/api/v1alpha3/prune_policy.go +++ b/api/v1alpha3/prune_policy.go @@ -33,10 +33,13 @@ type PrunePolicy struct { // retro-default stored ones. Every reader must go through EffectivePruneMode, which maps an // absent policy and an empty mode to OnEvent. - // Mode decides which deletions are mirrored. "Never" removes nothing. - // "OnEvent" mirrors a delete observed on the source cluster, but a resync - // removes nothing. "Always" also lets a resync remove a document that has no - // resource on the cluster. Defaults to "OnEvent". + // Mode decides which deletions are mirrored. Defaults to "OnEvent". + // + // - "OnEvent": mirrors a delete observed on the source cluster; a resync + // removes nothing. + // - "Never": removes nothing. + // - "Always": as "OnEvent", and a resync also removes a document that has + // no resource on the cluster. // +optional // +kubebuilder:validation:Enum=Never;OnEvent;Always // +kubebuilder:default=OnEvent diff --git a/config/crd/bases/configbutler.ai_argocdclaims.yaml b/config/crd/bases/configbutler.ai_argocdclaims.yaml index 9b4a3372..0a5162e8 100644 --- a/config/crd/bases/configbutler.ai_argocdclaims.yaml +++ b/config/crd/bases/configbutler.ai_argocdclaims.yaml @@ -57,12 +57,18 @@ spec: schema: openAPIV3Schema: description: |- - ArgoCDClaim hands control of an Argo CD Application to GitOps Reverser. The - Application's automated sync stays off, and GitOps Reverser starts each - sync, always after the edits pending on the branch are published: after a - commit it did not push, a refused or reverted edit, or an object the API - created, and once at startup or when a GitTarget first names it. GitTargets - name it with spec.applierClaimRef. + ArgoCDClaim lets GitOps Reverser decide when an Argo CD Application syncs. + The Application's automated sync stays off, and GitOps Reverser starts each + sync, always after the edits pending on the branch are published. It starts + one: + + - after a commit that GitOps Reverser did not push; + - after a refused edit, on a GitTarget with spec.onRefusal "Reapply"; + - after a sync reverted an edit made through the API; + - after objects were created through the API in its folders; + - once at startup, and when a GitTarget first names this ArgoCDClaim. + + GitTargets name it with spec.applierClaimRef. properties: apiVersion: description: |- diff --git a/config/crd/bases/configbutler.ai_fluxclaims.yaml b/config/crd/bases/configbutler.ai_fluxclaims.yaml index b7cfc649..d05eb531 100644 --- a/config/crd/bases/configbutler.ai_fluxclaims.yaml +++ b/config/crd/bases/configbutler.ai_fluxclaims.yaml @@ -57,12 +57,18 @@ spec: schema: openAPIV3Schema: description: |- - FluxClaim hands control of a Flux Kustomization to GitOps Reverser. The - Kustomization stays suspended, and GitOps Reverser resumes it for each apply - it starts, always after the edits pending on the branch are published: after - a commit it did not push, a refused or reverted edit, or an object the API - created, and once at startup or when a GitTarget first names it. GitTargets - name it with spec.applierClaimRef. + FluxClaim lets GitOps Reverser decide when a Flux Kustomization applies. + The Kustomization stays suspended, and GitOps Reverser resumes it for each + apply it starts, always after the edits pending on the branch are + published. It starts one: + + - after a commit that GitOps Reverser did not push; + - after a refused edit, on a GitTarget with spec.onRefusal "Reapply"; + - after an apply reverted an edit made through the API; + - after objects were created through the API in its folders; + - once at startup, and when a GitTarget first names this FluxClaim. + + GitTargets name it with spec.applierClaimRef. properties: apiVersion: description: |- diff --git a/config/crd/bases/configbutler.ai_gitbranches.yaml b/config/crd/bases/configbutler.ai_gitbranches.yaml index e2007671..79205e15 100644 --- a/config/crd/bases/configbutler.ai_gitbranches.yaml +++ b/config/crd/bases/configbutler.ai_gitbranches.yaml @@ -250,14 +250,18 @@ spec: rule: self.name != '' parentBranch: description: |- - ParentBranch is the branch spec.branch is created from, and compared against - while spec.branch does not exist on the remote. Omitted, it is the remote's - default branch, or no history at all in an empty repository. When set, it - must exist on the remote for spec.branch to be created; otherwise this - GitBranch reports ParentBranchNotFound and writes nothing. Once spec.branch - exists, the parent branch is no longer followed. It is only read, so - spec.allowedGitBranches need not allow it. Immutable, including adding or - removing it: delete and recreate the GitBranch to change it. + ParentBranch is the branch spec.branch is created from, and compared + against while spec.branch does not exist on the remote. Omitted, it is the + remote's default branch, or no history at all in an empty repository. + + When set, it must exist on the remote for spec.branch to be created; + otherwise this GitBranch reports Stalled=True, reason ParentBranchNotFound, + and writes nothing. Once spec.branch exists, the parent branch is no + longer followed. It is only read, so the GitProvider's + spec.allowedGitBranches need not allow it. + + Immutable, including adding or removing it: delete and recreate the + GitBranch to change it. minLength: 1 type: string required: diff --git a/config/crd/bases/configbutler.ai_gitproviders.yaml b/config/crd/bases/configbutler.ai_gitproviders.yaml index f9c59936..1c8fdbd1 100644 --- a/config/crd/bases/configbutler.ai_gitproviders.yaml +++ b/config/crd/bases/configbutler.ai_gitproviders.yaml @@ -302,9 +302,15 @@ spec: description: |- KnownHostsRef names a ConfigMap or Secret, in this namespace, holding SSH known_hosts, so several GitProviders on one host can share them. SSH only. - Host keys come from the credentials Secret's own known_hosts first, then - this reference, then the installation's default; without valid keys, SSH - fails. + + Host keys come from the first of these that has any: + + 1. the known_hosts key of the credentials Secret, spec.secretRef; + 2. this reference; + 3. the installation's default. + + Without host keys, SSH fails unless the installation sets + --insecure-allow-missing-known-hosts. properties: kind: default: ConfigMap diff --git a/config/crd/bases/configbutler.ai_gittargets.yaml b/config/crd/bases/configbutler.ai_gittargets.yaml index c55b3bd8..2c74ff96 100644 --- a/config/crd/bases/configbutler.ai_gittargets.yaml +++ b/config/crd/bases/configbutler.ai_gittargets.yaml @@ -368,21 +368,26 @@ spec: type: string placement: description: |- - Placement decides where a new resource is written: one with no document in - the folder yet. A resource that already has a document is updated where that - document is, so a change here affects only resources created afterward, and - labeling a resource later never moves its file. + Placement decides where a new resource is written: one with no document + in the folder yet. A resource that already has a document is updated + where that document is. So a change here affects only resources created + afterward, and labeling a resource later never moves its file. - byType and default are path templates over the resource's identity - ({namespace}, {name}, {group}, {version}, {resource}, {kind} and more), and + byType and default are path templates over the resource's identity: + {namespace}, {name}, {group}, {version}, {resource}, {kind} and more. They may read a label: "{label:app.kubernetes.io/instance}/configmaps.yaml". - "{label:key}" renders "_unlabeled" for a resource without the label, and - "{namespace}" renders "_cluster" for a cluster-scoped one. Append - "|fallback" to either to choose that value: "{label:team|unassigned}", - "{namespace|_global}". A fallback is up to 63 characters of [A-Za-z0-9._-], - is not "." or "..", and may be empty to drop the path segment. A {namespace} - fallback that names a real namespace files cluster-scoped resources in that - namespace's folder. A fallback on any other variable is rejected. + + Two variables can be absent, and each then renders a built-in value: + + - "{label:key}" renders "_unlabeled" for a resource without the label. + - "{namespace}" renders "_cluster" for a cluster-scoped resource. + + Append "|fallback" to either to choose that value instead: + "{label:team|unassigned}", "{namespace|_global}". A fallback is up to 63 + characters of [A-Za-z0-9._-], is not "." or "..", and may be empty to + drop the path segment. A {namespace} fallback that names a real namespace + files cluster-scoped resources in that namespace's folder. A fallback on + any other variable is rejected. properties: byType: additionalProperties: @@ -425,10 +430,13 @@ spec: mode: default: OnEvent description: |- - Mode decides which deletions are mirrored. "Never" removes nothing. - "OnEvent" mirrors a delete observed on the source cluster, but a resync - removes nothing. "Always" also lets a resync remove a document that has no - resource on the cluster. Defaults to "OnEvent". + Mode decides which deletions are mirrored. Defaults to "OnEvent". + + - "OnEvent": mirrors a delete observed on the source cluster; a resync + removes nothing. + - "Never": removes nothing. + - "Always": as "OnEvent", and a resync also removes a document that has + no resource on the cluster. enum: - Never - OnEvent @@ -456,13 +464,18 @@ spec: type: boolean suspend: description: |- - Suspend stops this GitTarget from making new commits, without deleting it. - Watches keep running and status.placement stays current; status.retention is - not published while it is set. A commit already made locally is still - pushed, so one more commit can appear seconds after it is set: suspend stops - the next write and does not undo the last one. Clearing it resumes from the - current cluster state at the next resync; writes skipped while suspended are - not replayed. Defaults to false. + Suspend stops this GitTarget from making new commits, without deleting + it. Defaults to false. + + While it is set, watches keep running and status.placement stays current, + but status.retention is not published. + + A commit already made locally is still pushed, so one more commit can + appear seconds after it is set: suspend stops the next write and does not + undo the last one. + + Clearing it resumes from the current cluster state at the next resync. + Writes skipped while suspended are not replayed. type: boolean required: - gitBranchRef diff --git a/config/crd/bases/configbutler.ai_kubeproviders.yaml b/config/crd/bases/configbutler.ai_kubeproviders.yaml index 12a480bf..bab72e85 100644 --- a/config/crd/bases/configbutler.ai_kubeproviders.yaml +++ b/config/crd/bases/configbutler.ai_kubeproviders.yaml @@ -462,15 +462,21 @@ spec: in the API server's audit webhook URL, /audit-webhook/. When several logical clusters share one webhook backend, it is instead the value of the audit-event annotation that --author-attribution-audit-route-annotation-key - names. KubeProviders with the same route read the same cluster's attribution - facts. + names. KubeProviders with the same route read the same cluster's + attribution facts. Empty means .. Set it when several KubeProviders in this - namespace connect to one cluster, since an API server posts under one route. - A route belongs to one namespace: the one it starts with (.) when - a KubeProvider there carries it, and otherwise the namespace of the oldest - KubeProvider carrying it. KubeProviders in another namespace that carry it - report Stalled=True, reason AuditRouteConflict, and are not used. + namespace connect to one cluster, since an API server posts under one + route. + + A route belongs to one namespace: + + - the namespace the route starts with (.), when a KubeProvider + there carries it; + - otherwise, the namespace of the oldest KubeProvider carrying it. + + KubeProviders in another namespace that carry the route report + Stalled=True, reason AuditRouteConflict, and are not used. maxLength: 253 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string From 3be56e3c726b6fb01d27cbbb4a3d476cef2d4e10 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 08:23:03 +0000 Subject: [PATCH 11/15] docs(flux): give repeated ClaimedResourceReconciled events a section The troubleshooting row kept the symptom, cause, ownership detail and fix in one table cell. The row now links to a section of its own, which names flux resume as the ordinary cause before the whole-spec Update case. The flux resume bullet under Overriding by hand states the unchecked apply and the claim's response in separate sentences, and the UPGRADING entry for the removed message templates separates the removal, client behavior and the migration. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/UPGRADING.md | 16 ++++++++++------ docs/bi-directional-flux.md | 29 ++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index ffd651c5..28d8a890 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -407,12 +407,16 @@ both. ## `eventTemplate` and `groupTemplate` are gone from the schema -`GitTarget.spec.commit.message.eventTemplate` and `groupTemplate`, refused since -`liveTemplate` replaced them, are removed. `kubectl apply` refuses them as unknown fields, and a -client that does not validate fields strictly has them pruned. A stored `GitTarget` that still -carries one loses it and returns to `Validated=True`, committing with `liveTemplate` or the -built-in template; [migrate the template](#one-live-commit-message-template) first if you want to -keep its wording. +`GitTarget.spec.commit.message.eventTemplate` and `groupTemplate` are removed from the schema. +Earlier releases kept them only to refuse them, after `liveTemplate` replaced them. + +- `kubectl apply` refuses either field as unknown. A client that does not validate fields + strictly has them pruned, without an error. +- A stored `GitTarget` that still carries one loses it and returns to `Validated=True`. It then + commits with its `liveTemplate`, or with the built-in template. + +To keep a template's wording, [move it to `liveTemplate`](#one-live-commit-message-template) +before you upgrade. ## `allowAnySourceNamespace` is gone; `allowCrossNamespace.sources` replaces it diff --git a/docs/bi-directional-flux.md b/docs/bi-directional-flux.md index 6e436f0c..e070e3f7 100644 --- a/docs/bi-directional-flux.md +++ b/docs/bi-directional-flux.md @@ -476,10 +476,11 @@ tip, the `Kustomization`s GitOps Reverser holds ignore the new artifact, and one ## Overriding by hand -- **`flux resume ks checkout`** applies the `Kustomization` at once, unchecked. Within a minute the - applier claim suspends it again, records a `ClaimedResourceReconciled` Event, counts it in - `claimed_resource_reconciled_total`, and shows the apply as an `Unresolved` operation that completes - once nothing it started is running. It does so every time. +- **`flux resume ks checkout`** applies the `Kustomization` at once, without GitOps Reverser + checking the commit first. Within a minute the applier claim suspends it again: it records a + `ClaimedResourceReconciled` Event, counts it in `claimed_resource_reconciled_total`, and shows the + apply as an `Unresolved` operation that completes once nothing it started is running. It does so + every time. - **`flux reconcile ks checkout`** does nothing visible while the `Kustomization` is suspended: Flux marks the request handled without applying. - **`flux suspend ks checkout`** is taken back with a token of GitOps Reverser's. @@ -514,7 +515,7 @@ flux get ks checkout -n flux-system | --- | --- | --- | | `FluxClaim` `Configured=False`, `ApplierNotFound` | No `Kustomization` with that namespace and name in the applier claim's cluster, or Flux is not installed there | Create it, or recreate the applier claim with the right `kustomizationRef`; an omitted namespace is the applier claim's | | `FluxClaim` `Configured=False`, `SuspendOwnedElsewhere` | The manifest that applies the `Kustomization` sets `spec.suspend` | Remove the field from that manifest | -| Repeated `ClaimedResourceReconciled` Events (a rising `claimed_resource_reconciled_total`), each followed by applier claim `OperationUnresolved` | A controller writes the `Kustomization` with an Update of its whole spec that omits `spec.suspend`, such as `controllerutil.CreateOrUpdate` with a spec it builds from scratch. An Update takes no ownership of a field it removes, so `SuspendOwnedElsewhere` cannot report it, and Flux applies unchecked on each of that controller's writes until the applier claim suspends it again | Make that controller copy `spec.suspend` from the object it read into the spec it writes, or apply with server-side apply and leave the field out | +| Repeated `ClaimedResourceReconciled` Events, each followed by applier claim `OperationUnresolved` | Something keeps undoing the applier claim's suspension | See [repeated `ClaimedResourceReconciled` Events](#repeated-claimedresourcereconciled-events) | | `ApplierClaimReady=False`, `SourceMismatch`, or applier claim `UnsupportedSource` | The message names the broken [`GitRepository` rule](#the-gitrepository) | Fix the `GitRepository`; the applier claim then adopts the `Kustomization`, and the apply that was waiting is asked for | | `FluxClaim` `Configured=False`, `ApplierNamespaceNotAllowed` | The `Kustomization` is in a namespace the grants do not give, such as `flux-system` | Add it to `applierNamespaces` in the `KubeProvider`'s `spec.allowedApplierClaims` (Helm: `kubeProvider.allowedApplierClaims`) | | `FluxClaim` `Configured=Unknown`, `NotBound` | A `GitTarget` names the applier claim, but cannot be judged yet: its `GitProvider` or `KubeProvider` does not exist | Create the missing object; the message names it | @@ -530,6 +531,24 @@ flux get ks checkout -n flux-system | A Git-side push takes minutes to reach the cluster | No receiver, so the refresh finds it | Configure [the push webhook](bi-directional.md#telling-gitops-reverser-that-somebody-else-pushed) | | A refused edit is not reverted | The target is suspended, `onRefusal` is not `Reapply`, or the refusal is not eligible | See [reverting a refused edit](configuration.md#reverting-a-refused-edit-speconrefusal) | +### Repeated `ClaimedResourceReconciled` events + +Repeated `ClaimedResourceReconciled` Events mean something keeps undoing the applier claim's +suspension. A rising `claimed_resource_reconciled_total` counts these corrections. A person who +runs `flux resume` causes one each time, as [overriding by hand](#overriding-by-hand) describes. If +they keep coming and the applier claim also reports `OperationUnresolved`, look at the controller +that writes the `Kustomization`. + +A controller that replaces the whole spec removes `spec.suspend` by leaving it out, for example +`controllerutil.CreateOrUpdate` with a spec it builds from scratch. On each such write, Flux applies +unchecked until GitOps Reverser suspends the `Kustomization` again. Removing a field takes no +ownership of it, so `SuspendOwnedElsewhere` does not report this case. + +Change that controller to do one of these: + +- Copy `spec.suspend` from the object it read into the spec it writes. +- Use server-side apply, and leave `spec.suspend` out of the fields it manages. + ## While GitOps Reverser is down **A `Kustomization` waits suspended while GitOps Reverser is down.** A Git-side push waits until From 81ace6c4cb2ee7676404703f81b648764e152e52 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 08:23:47 +0000 Subject: [PATCH 12/15] docs(api): list the serializeNamespace choices as bullets The omitted, true and false cases each get a bullet, so no paragraph opens with a bare lowercase literal, and the one-source-namespace rule for false follows the list. GitTarget.spec.onRefusal keeps its paragraphs: it has two values, and the restrictions on "Reapply" read better as prose. Co-Authored-By: Claude Opus 5.5 (1M context) --- api/v1alpha3/gittarget_types.go | 28 +++++++++---------- .../crd/bases/configbutler.ai_gittargets.yaml | 28 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/api/v1alpha3/gittarget_types.go b/api/v1alpha3/gittarget_types.go index 9029e5cd..6d0c41ad 100644 --- a/api/v1alpha3/gittarget_types.go +++ b/api/v1alpha3/gittarget_types.go @@ -135,22 +135,22 @@ type GitTargetSpec struct { // *bool because neither default is safe — false breaks a flat folder, true writes a redundant // line into a kustomize folder that already supplies one. - // SerializeNamespace decides whether a namespaced resource's document carries - // metadata.namespace. It applies to every write, not just the first, and is - // ignored for cluster-scoped resources. + // SerializeNamespace decides whether a namespaced resource's document + // carries metadata.namespace. It applies to every write, not just the + // first, and is ignored for cluster-scoped resources. // - // Omitted, it is decided per document: the namespace is left out only when the - // kustomization governing the document's path sets namespace: to that same - // namespace. This suits a folder of nested kustomize roots that each set their - // own namespace. + // - Omitted: decided per document. The namespace is left out only when the + // kustomization governing the document's path sets namespace: to that + // same namespace. This suits a folder of nested kustomize roots that each + // set their own namespace. + // - true: always written. Use it for a flat folder applied as is. + // - false: never written, for a folder whose namespace something else + // supplies: a Flux Kustomization's targetNamespace, an Argo CD + // Application's destination.namespace, or a kustomization. // - // true always writes it. Use it for a flat folder applied as is. - // - // false never writes it, for a folder whose namespace something else supplies: - // a Flux Kustomization's targetNamespace, an Argo CD Application's - // destination.namespace, or a kustomization. Such a folder holds one source - // namespace; a WatchRule bringing a second one is refused with - // GitPathAccepted=False, reason MultipleSourceNamespaces. + // With false, the folder holds one source namespace. A WatchRule bringing a + // second one is refused with GitPathAccepted=False, reason + // MultipleSourceNamespaces. // +optional SerializeNamespace *bool `json:"serializeNamespace,omitempty"` diff --git a/config/crd/bases/configbutler.ai_gittargets.yaml b/config/crd/bases/configbutler.ai_gittargets.yaml index 2c74ff96..f1c2572c 100644 --- a/config/crd/bases/configbutler.ai_gittargets.yaml +++ b/config/crd/bases/configbutler.ai_gittargets.yaml @@ -445,22 +445,22 @@ spec: type: object serializeNamespace: description: |- - SerializeNamespace decides whether a namespaced resource's document carries - metadata.namespace. It applies to every write, not just the first, and is - ignored for cluster-scoped resources. + SerializeNamespace decides whether a namespaced resource's document + carries metadata.namespace. It applies to every write, not just the + first, and is ignored for cluster-scoped resources. - Omitted, it is decided per document: the namespace is left out only when the - kustomization governing the document's path sets namespace: to that same - namespace. This suits a folder of nested kustomize roots that each set their - own namespace. + - Omitted: decided per document. The namespace is left out only when the + kustomization governing the document's path sets namespace: to that + same namespace. This suits a folder of nested kustomize roots that each + set their own namespace. + - true: always written. Use it for a flat folder applied as is. + - false: never written, for a folder whose namespace something else + supplies: a Flux Kustomization's targetNamespace, an Argo CD + Application's destination.namespace, or a kustomization. - true always writes it. Use it for a flat folder applied as is. - - false never writes it, for a folder whose namespace something else supplies: - a Flux Kustomization's targetNamespace, an Argo CD Application's - destination.namespace, or a kustomization. Such a folder holds one source - namespace; a WatchRule bringing a second one is refused with - GitPathAccepted=False, reason MultipleSourceNamespaces. + With false, the folder holds one source namespace. A WatchRule bringing a + second one is refused with GitPathAccepted=False, reason + MultipleSourceNamespaces. type: boolean suspend: description: |- From 5a7ad2c80039c420f675cda73344a7eaf0e55ab0 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 19:28:04 +0000 Subject: [PATCH 13/15] docs(readme): restructure around why, how it works, and adoption Lead with what the operator does and why, then group the supported layouts, commit windows, and Flux/Argo CD coordination under "How it works". The principles move into the architecture ground rules, which already held the encryption and applier rules. Cut repeated points, say that audit attribution needs kube-apiserver flags, and drop the stale one-GitProvider-per-repository advice (a second writer gets BranchConflict). Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 230 +++++++++++++++++++------------------------ docs/architecture.md | 9 +- 2 files changed, 112 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index da566595..60d78da1 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,15 @@ # GitOps Reverser -GitOps Reverser watches selected Kubernetes resources and commits a clean YAML representation to Git. -It strips `status`, `managedFields`, and runtime metadata. For supported layouts, it updates existing -manifests in place, preserving comments and document structure. +GitOps Reverser is a Kubernetes operator that turns cluster activity into clean YAML commits. -Use it to capture live changes, bring an existing cluster into Git, or experiment with API-first -workflows alongside Flux or Argo CD. +GitOps keeps the desired state of a cluster in Git, so every change has to arrive as a commit. That +requires everyone who makes a change to know Git, YAML, and the layout of your repo. With +GitOps Reverser they use the Kubernetes API instead: `kubectl`, a GUI, a CI job, or an AI agent over +MCP. It commits the result within seconds, and Flux or Argo CD keeps deploying from the repo. +The operator is **deterministic**, with **no AI at runtime**. -[Quick start](docs/quickstart.md): install the operator and capture your first ConfigMap in Git. - -It is pre-1.0: the CRDs are `v1alpha3` and the configuration surface can still change between -releases. [Before you adopt it](#before-you-adopt-it) covers what to weigh. +[Quick start](docs/quickstart.md) · [Documentation](docs/README.md)
Inspect an [example commit](https://github.com/ConfigButler/example-audit/commit/800a51e5a8edcccbc85c94d5fef7ef7cc8381b7b). -Named Kubernetes actors in Git history require optional -[audit attribution](docs/attribution-setup-guide.md); the default uses a configured Git identity. +Its named Kubernetes actor comes from optional [audit attribution](docs/attribution-setup-guide.md); +the default uses a configured Git identity. -## How it works +## Why GitOps Reverser + +Use it to: -Changes reach the Kubernetes API however your users make them: `kubectl`, a GUI, a CI job, or an -agent over MCP. +- **Let people change the cluster without learning Git.** A GUI, a CI job, or an MCP agent works + through the Kubernetes API, under its RBAC and admission control, and the change still lands in + Git with the rest of your history. +- **Bring an existing cluster into Git.** A new target commits the selected resources already in the + cluster, then follows their changes. +- **Keep Git in step with live edits.** A `kubectl` fix made during an incident becomes a commit + instead of drift. + +What sets it apart: + +- **Your files keep their shape.** An update edits the existing YAML document in place: key order, + comments, and untouched documents in multi-document files survive. In a supported Kustomize + layout it edits the source manifest, not the rendered output. See [what it can write](#what-it-can-write). +- **Authors are established, never asserted.** With [audit attribution](docs/attribution-setup-guide.md), + a commit names the identity the API server authenticated, and no field lets anyone type a name in. + It needs kube-apiserver flags, so managed control planes that hide them cannot use it. See + [attribution](docs/spec/attribution.md#authorship-is-established-never-asserted). +- **A commit can say why.** Changes are grouped into commit windows, and a `CommitRequest` lets the + person making a change explain **why** it was necessary. See [commit windows](#commit-windows). + +## How it works ![Overview diagram: humans, kubectl, and MCP clients change resources through the Kubernetes API, which GitOps Reverser watches and commits to Git](docs/images/overview.excalidraw.svg) 1. **Watch** the Kubernetes API for the types each `GitTarget` claims, the single source of object - state. A new target also reconciles all existing resources of those types into Git. -2. **Sanitize** the change and compare it against what Git already holds. + state. +2. **Sanitize** the change, stripping `status`, `managedFields`, and runtime metadata, and compare it + against what Git already holds. 3. **Write** stable YAML to the target folder and push, grouping a burst of changes into one commit. -Custom resources configure it: `GitProvider` for repository and credentials, `GitBranch` for one -branch of that repository, `GitTarget` for a folder on it, `WatchRule` or `ClusterWatchRule` for what -to capture, `KubeProvider` for the source cluster, created as `local` in the operator's namespace on -install, and optionally `GitReceiver` for the Git host's push webhook. A rule selects whole resource -collections, including their deletions; the `GitTarget`'s prune mode decides whether a deletion -removes the file from Git. -See [Configuration](docs/configuration.md). - -By default it watches its own cluster. Add more of each as you need them: one operator can serve -several source clusters, repositories, branches, and folders. - -## Principles - -- **Creates Git commits.** The output is Git commits that reflect the state of your cluster. - Deploying is Flux's and Argo CD's job: run one alongside to bring changes from Git back into the - cluster. Only a folder that both GitOps Reverser writes and Flux or Argo CD applies needs more: an - applier claim, so GitOps Reverser decides when it is applied. See - [bi-directional usage](docs/bi-directional.md). -- **API-first, not API-only.** Most changes are assumed to arrive through the API, so that is what - publishing is tuned for. Your changes end up in your remote Git repo within seconds. Other - authors can push to the same branch, and that is handled without conflicts. If an API edit and a - Git commit touch the same resource (which is rare!), then the API wins. See - [API-first publication](docs/api-first-publication.md) for more depth. -- **Authors are established, never asserted.** An attributed commit names the identity the API - server authenticated for the change; everything else carries the configured Git identity, or an - explicit unknown when attribution is on but cannot resolve. No field lets anyone type a name in. See - [attribution](docs/spec/attribution.md#authorship-is-established-never-asserted). -- **Resources classified as sensitive require encryption before commit.** If encryption fails, or no - encryptor is configured, the write is rejected. See [SOPS and age](docs/sops-age-guide.md). - -## Features +A `GitTarget` names a folder on a Git branch, and a `WatchRule` or `ClusterWatchRule` says what to +capture into it. A few more custom resources hold the repo, its credentials, and the source +cluster. By default it watches its own cluster, and one operator can serve several source clusters, +repos, branches, and folders. See [Configuration](docs/configuration.md). -- **Capture existing resources and future changes.** A new target captures the selected resources - already in the cluster, then follows changes, including deletions under a - [deletion policy](docs/configuration.md#deletion-policy-specprunemode). -- **Mirror only the objects you label.** A rule's `objectSelector` lets the API server choose which - objects of a type are mirrored; an object that loses its label leaves the mirror like a deleted - one. See [selecting objects by label](docs/configuration.md#selecting-objects-by-label-rulesobjectselector). -- **Each object reaches a target once.** A target watches each object through one collection, so an - edit is never delivered twice and a status-only update never splits a commit. A rule that would - overlap another on the same target is refused, naming both. See - [overlapping collections](docs/configuration.md#overlapping-collections). -- **In-place edits keep the shape of your file.** Updates preserve key order, comments, and - untouched documents in multi-document files. -- **Inspect a repository before you point at it.** `manifest-analyzer --mode scan-repo` classifies - every candidate folder under a repository root, read-only and with no cluster. See - [`cmd/manifest-analyzer/`](cmd/manifest-analyzer/). -- **SOPS and age, set up on the fly.** The operator creates the encryption configuration and can - generate a missing age key in your chosen Kubernetes `Secret`. See - [SOPS and age](docs/sops-age-guide.md). -- **Optional audit attribution identifies Kubernetes actors.** Without it, commits carry the - configured Git identity; when attribution cannot be resolved, they carry an explicit unknown - author. See [audit attribution](docs/attribution-setup-guide.md). -- **Custom commit messages.** Separate templates for live windows, reconciles, and save requests. - See [message templates](docs/commit-messages.md). -- **SSH-signed commits.** Configured through `GitProvider.spec.commit.signing`. See - [commit signing](docs/commit-signing.md). -- **`kubectl wait` works on every resource.** Conditions follow the kstatus convention, so `Ready`, - `Reconciling`, and `Stalled` mean what GitOps tooling expects. See - [status conditions](docs/spec/status-conditions-guide.md). -- **Metrics on a standard endpoint.** Scrape `/metrics` with whatever you already run, or turn on - the chart's `ServiceMonitor`. The docs carry example queries for the questions operators ask, and - a named list of what is deliberately not instrumented. See - [interpreting metrics](docs/interpreting-metrics.md). - -## What it can write - -Choose the resources to watch and the repository, branch, and folder to write into. +### What it can write | Your source in Git | What you can do | |---|---| @@ -122,19 +80,39 @@ Choose the resources to watch and the repository, branch, and folder to write in | A Flux `HelmRelease` or Argo CD `Application` | Capture the declaration, including chart versions and inline values | | Helm templates or standalone `values.yaml` | No writeback from rendered workloads | -Kustomize support includes local bases and overlays, with the base kept read-only when targeting an -overlay. Generators, components, remote bases, and several other transforms are unsupported: a -folder that uses them is refused with `Stalled=True` and a reason, while the folder is still -untouched. See the [supported subset](docs/configuration.md#kustomize-support-in-the-target-path). +Local Kustomize bases and overlays work, with the base kept read-only. Generators, components, and +remote bases are unsupported; such a folder is refused with `Stalled=True` before anything is +written. See the [supported subset](docs/configuration.md#kustomize-support-in-the-target-path). + +Watch the resources that express your intent, such as a `HelmRelease` rather than the workloads it +renders: the operator cannot tell authored resources from generated ones. See +[choosing what to capture](docs/installing-apps-as-krm.md#the-design-decision-capture-intent-not-the-rendered-output). -Select the resources that express your intent. For example, watch a `HelmRelease` to capture chart -settings. The operator cannot automatically distinguish authored resources from controller-generated -ones. See [choosing what to capture](docs/installing-apps-as-krm.md#the-design-decision-capture-intent-not-the-rendered-output). +### Commit windows + +Changes for the same target and author share a commit window, and each window becomes one commit. +When a window closes is yours to tune: its timers are set per `GitTarget`, and a `CommitRequest` can +bring its own timers along with the commit message. + +![Kubernetes resource changes flow through GitOps Reverser's commit window into Git](docs/images/commit-window.excalidraw.svg) + +The picture shows the [commit-window example](docs/demo/commit-window.md) running on top of the +[quick start](#quick-start). + +### Alongside Flux and Argo CD + +GitOps Reverser writes commits; Flux or Argo CD deploys them. Others can push to the same branch: +the operator replays its writes on top of their commits, so history stays linear with no merge +commits. If both touch the same resource, the API wins +([API-first publication](docs/api-first-publication.md)). + +A folder that Flux or Argo CD also applies needs an [applier claim](docs/bi-directional.md), so an +apply never reverts a live edit that has not reached Git yet. ## Quick start Follow the [installation walkthrough](docs/quickstart.md) to capture ConfigMaps from a demo namespace -into a disposable repository. You need a Kubernetes cluster, `kubectl`, Helm 3, and Git write access. +into a disposable repo. You need a Kubernetes cluster, `kubectl`, Helm 3, and Git write access. The walkthrough includes cert-manager setup; Redis and audit delivery are optional. The demo captures a live change: @@ -143,40 +121,43 @@ The demo captures a live change: kubectl create configmap test-config --from-literal=key=value -n gitops-reverser-quickstart-demo ``` -Inspect the commit under `live-cluster/` in your repository. Then edit the ConfigMap and inspect the +Inspect the commit under `live-cluster/` in your repo. Then edit the ConfigMap and inspect the next diff. The walkthrough includes status checks, troubleshooting, and cleanup. -## Batch changes into a commit - -Changes for the same target and author share a commit window, and each window becomes one commit. -When a window closes is yours to tune: its timers are set per `GitTarget`, and a `CommitRequest` can -bring its own timers along with the commit message. This allows the person making a change to explain -**why** the change was necessary. +## Features -![Kubernetes resource changes flow through GitOps Reverser's commit window into Git](docs/images/commit-window.excalidraw.svg) +- **Mirror only the objects you label.** A rule's `objectSelector` lets the API server choose which + objects of a type are mirrored; an object that loses its label leaves the mirror like a deleted + one. See [selecting objects by label](docs/configuration.md#selecting-objects-by-label-rulesobjectselector). +- **Secrets never reach Git in plaintext.** Resources classified as sensitive are encrypted with + SOPS and age before commit, or the write is rejected. The operator creates the encryption + configuration and can generate a missing age key. See [SOPS and age](docs/sops-age-guide.md). +- **Inspect a repo before you point at it.** `manifest-analyzer --mode scan-repo` classifies + every candidate folder under a repo root, read-only and with no cluster. See + [`cmd/manifest-analyzer/`](cmd/manifest-analyzer/). +- **The operational basics.** [SSH-signed commits](docs/commit-signing.md) and + [custom commit message templates](docs/commit-messages.md). Conditions follow the kstatus + convention, so `kubectl wait` works on every resource + ([status conditions](docs/spec/status-conditions-guide.md)). Metrics are on a standard + `/metrics` endpoint, with an optional `ServiceMonitor` and + [example queries](docs/interpreting-metrics.md). -The picture shows the [commit-window example](docs/demo/commit-window.md) running. It carries the -manifests: a `GitTarget` with a `window`, a `WatchRule` selecting the two types, and a save request -made after the changes or before them. It runs on top of the quickstart above. +## Adopting it -## Try it with your existing repo +### Try it with your existing repo Start with a [scratch branch](docs/configuration.md#seeing-what-a-target-will-do-before-it-does-it) containing your existing manifests, with nothing deploying that branch. Select a small resource scope and one destination folder. Inspect the initial commits before making a live edit, then check which source files changed. -The operator pushes directly to the configured branch. Flux or Argo CD deploying other folders is -unaffected. If one of them deploys the folder you write, read the -[bi-directional guide](docs/bi-directional.md) first. Without an applier claim, live edits can be -reverted by Flux or Argo CD, and [replaying captured state](docs/api-first-publication.md) can -overwrite concurrent Git edits to the same object. Decide per folder which side is authoritative. +The operator pushes directly to the configured branch. If Flux or Argo CD deploys the folder you +write, read [alongside Flux and Argo CD](#alongside-flux-and-argo-cd) first. -## Before you adopt it +### Limits before 1.0 -The CRDs are `v1alpha3`, so the configuration surface can change between releases; -[`docs/UPGRADING.md`](docs/UPGRADING.md) carries each migration. Keep one `GitProvider` per -repository, so that two of them never write the same paths. +It is pre-1.0: the CRDs are `v1alpha3`, so the configuration surface can change between releases; +[`docs/UPGRADING.md`](docs/UPGRADING.md) carries each migration. - **Access:** the chart defaults to cluster-wide read access, including Secrets. Review [RBAC](docs/rbac.md) to restrict watched types and understand the remaining credential permissions. @@ -184,36 +165,33 @@ repository, so that two of them never write the same paths. be lost on restart; recovery captures current state. Git history is not a complete event log. - **Deletes:** the default mirrors observed delete events but retains documents absent from a reconnect snapshot. Choose a [deletion policy](docs/configuration.md#deletion-policy-specprunemode) - that fits your repository. + that fits your repo. +- **Availability:** it runs one replica, with no standby failover: the chart rejects + `replicaCount > 1` rather than let two instances write the same repo. - **Versions:** the test suites run against Kubernetes `1.37`, at the API level with `envtest` and end-to-end on `k3s`. Other versions may work but are not in the matrix. - Importing this repo as a Go module carries its own version floor; see - [`docs/UPGRADING.md`](docs/UPGRADING.md). -It runs one replica, with no standby failover: the chart rejects `replicaCount > 1` rather than let -two instances write the same repository. Ownership coordination and a durable worker queue are on -the way to 1.0. The backlog is in [docs/TODO.md](docs/TODO.md), and longer-range directions in +The road to 1.0 is in [docs/TODO.md](docs/TODO.md), and longer-range directions in [docs/future/](docs/future/). ## Rather have it managed? ConfigButler can run a small, secure, public-facing Kubernetes API for you: we operate GitOps -Reverser and authorize your end users, with forward-deployed engineers to get you started. You keep a -clean, self-owned Git repo where your users express their intent. +Reverser and authorize your end users, and our engineers work with your team to get you started. +You keep a clean, self-owned Git repo where your users express their intent. ## Documentation and feedback - [Configuration](docs/configuration.md): resource selection, Git destinations, and commit behavior - [Helm chart](charts/gitops-reverser/README.md): installation options and upgrades +- [Architecture](docs/architecture.md#ground-rules): the design principles and how the pieces fit - [Documentation index](docs/README.md): setup guides and operational details - [Reverse GitOps](https://reversegitops.dev/): the manifesto for the broader pattern Trying it with a real repo? [Open an issue](https://github.com/ConfigButler/gitops-reverser/issues) -with the layout you tried, the diff you expected, and what happened. Install attempts, first-commit -experience, audit delivery, Git output shape, and CRD ergonomics are the most useful reports at this -stage. Contributions are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). - -Or connect on [LinkedIn](https://www.linkedin.com/in/simonkoudijs/): feedback, questions, and ideas -are all welcome. +with the layout you tried, the diff you expected, and what happened, or reach out on +[LinkedIn](https://www.linkedin.com/in/simonkoudijs/). Install attempts, Git output shape, and CRD +ergonomics are the most useful reports at this stage. Contributions are welcome; see +[CONTRIBUTING.md](CONTRIBUTING.md). Licensed under Apache 2.0. diff --git a/docs/architecture.md b/docs/architecture.md index 10c3b7e5..3d65fc1e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -52,6 +52,11 @@ deleting the definition (and every object of the kind with it) and recreating bo These are the design decisions to keep in your head while reading or changing the code: +**GitOps Reverser writes Git; Flux or Argo CD deploys.** The output is commits that reflect the +cluster's state. Bringing a change from Git back into the cluster is the job of a GitOps tool running +alongside. GitOps Reverser only decides *when* that tool applies a folder it also writes, through an +applier claim (see "GitOps Reverser decides when Git is applied" below). + **The Kubernetes API is the source of truth for what GitOps Reverser writes.** Git is a materialized mirror of desired state from the API, ingested by **watch**; GitOps Reverser never reads Git as object state. An edit made in Git reaches the cluster only through Flux or Argo CD. @@ -60,7 +65,9 @@ publishes the API edits it has decided, then asks the applier to apply. The watc result, and the applier's own writes during that apply (its echoes) are not mirrored, because Git already holds them. When a push conflicts with a newer remote commit, the operator fetches the new remote state, resets its local clone, and -replays its retained writes from the API. +replays its retained writes from the API. So the API comes first without being the only author: other +authors can push to the same branch and their commits stay, and only where an API edit and a Git +commit touch the same object does the API's version win. **Watch is the only object-state source.** Each `GitTarget` names one `KubeProvider` and opens one Kubernetes watch per claimed `(GVR, scope)` against that source, with `sendInitialEvents=true`. For a From 1430cdbef78065b3cb79bd914627909746d0de48 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 19:30:41 +0000 Subject: [PATCH 14/15] docs(api): any grant can keep this namespace's access, not only from: Same The README no longer promises an apply never reverts an unpublished edit; it describes the flush and the repair instead. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 5 +++-- api/v1alpha3/gitbranch_types.go | 4 ++-- api/v1alpha3/kubeprovider_types.go | 8 ++++---- config/crd/bases/configbutler.ai_gitbranches.yaml | 4 ++-- config/crd/bases/configbutler.ai_kubeproviders.yaml | 8 ++++---- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 60d78da1..9593ebde 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,9 @@ the operator replays its writes on top of their commits, so history stays linear commits. If both touch the same resource, the API wins ([API-first publication](docs/api-first-publication.md)). -A folder that Flux or Argo CD also applies needs an [applier claim](docs/bi-directional.md), so an -apply never reverts a live edit that has not reached Git yet. +A folder that Flux or Argo CD also applies needs an [applier claim](docs/bi-directional.md): GitOps +Reverser then decides when the applier runs and publishes pending edits first. An edit made while +that apply runs keeps its value in Git and is repaired on the cluster afterwards. ## Quick start diff --git a/api/v1alpha3/gitbranch_types.go b/api/v1alpha3/gitbranch_types.go index 796e8c4e..ad454b9e 100644 --- a/api/v1alpha3/gitbranch_types.go +++ b/api/v1alpha3/gitbranch_types.go @@ -71,8 +71,8 @@ type GitBranchSpec struct { // - Empty list ([]): no GitTarget. // - A list of grants: the GitTargets that at least one grant allows. // - // A list replaces the default, so it needs a grant with from: "Same" for - // the GitTargets in this namespace to keep access. + // A list replaces the default: the GitTargets in this namespace keep access + // only if at least one grant allows them. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 diff --git a/api/v1alpha3/kubeprovider_types.go b/api/v1alpha3/kubeprovider_types.go index a8f4b4fb..fe31a03e 100644 --- a/api/v1alpha3/kubeprovider_types.go +++ b/api/v1alpha3/kubeprovider_types.go @@ -59,8 +59,8 @@ type KubeProviderSpec struct { // - A list of grants: the GitTargets that at least one grant allows, with // what every grant that allows them permits. // - // A list replaces the default, so it needs a grant with from: "Same" for - // the GitTargets in this namespace to keep access. + // A list replaces the default: the GitTargets in this namespace keep access + // only if at least one grant allows them. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 @@ -78,8 +78,8 @@ type KubeProviderSpec struct { // - A list of grants: the claims that at least one grant allows, holding // appliers in the applierNamespaces of every grant that allows them. // - // A list replaces the default, so it needs a grant with from: "Same" for - // the claims in this namespace to keep access. + // A list replaces the default: the claims in this namespace keep access + // only if at least one grant allows them. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 diff --git a/config/crd/bases/configbutler.ai_gitbranches.yaml b/config/crd/bases/configbutler.ai_gitbranches.yaml index 79205e15..e7e9d345 100644 --- a/config/crd/bases/configbutler.ai_gitbranches.yaml +++ b/config/crd/bases/configbutler.ai_gitbranches.yaml @@ -93,8 +93,8 @@ spec: - Empty list ([]): no GitTarget. - A list of grants: the GitTargets that at least one grant allows. - A list replaces the default, so it needs a grant with from: "Same" for - the GitTargets in this namespace to keep access. + A list replaces the default: the GitTargets in this namespace keep access + only if at least one grant allows them. items: description: |- GitBranchGitTargetGrant admits the GitTargets of some namespaces to write diff --git a/config/crd/bases/configbutler.ai_kubeproviders.yaml b/config/crd/bases/configbutler.ai_kubeproviders.yaml index bab72e85..d32aa10b 100644 --- a/config/crd/bases/configbutler.ai_kubeproviders.yaml +++ b/config/crd/bases/configbutler.ai_kubeproviders.yaml @@ -87,8 +87,8 @@ spec: - A list of grants: the claims that at least one grant allows, holding appliers in the applierNamespaces of every grant that allows them. - A list replaces the default, so it needs a grant with from: "Same" for - the claims in this namespace to keep access. + A list replaces the default: the claims in this namespace keep access + only if at least one grant allows them. items: description: |- KubeProviderApplierClaimGrant admits the FluxClaims and ArgoCDClaims of some @@ -263,8 +263,8 @@ spec: - A list of grants: the GitTargets that at least one grant allows, with what every grant that allows them permits. - A list replaces the default, so it needs a grant with from: "Same" for - the GitTargets in this namespace to keep access. + A list replaces the default: the GitTargets in this namespace keep access + only if at least one grant allows them. items: description: |- KubeProviderGitTargetGrant admits the GitTargets of some namespaces to From 4439f911d58e4586987174218a6c5b0ad81696f7 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 19:33:11 +0000 Subject: [PATCH 15/15] docs(readme): link the Flux and Argo CD bi-directional guides Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9593ebde..ae777150 100644 --- a/README.md +++ b/README.md @@ -99,16 +99,15 @@ bring its own timers along with the commit message. The picture shows the [commit-window example](docs/demo/commit-window.md) running on top of the [quick start](#quick-start). -### Alongside Flux and Argo CD +### Bi-directional GitOps with Flux and Argo CD GitOps Reverser writes commits; Flux or Argo CD deploys them. Others can push to the same branch: the operator replays its writes on top of their commits, so history stays linear with no merge commits. If both touch the same resource, the API wins ([API-first publication](docs/api-first-publication.md)). -A folder that Flux or Argo CD also applies needs an [applier claim](docs/bi-directional.md): GitOps -Reverser then decides when the applier runs and publishes pending edits first. An edit made while -that apply runs keeps its value in Git and is repaired on the cluster afterwards. +- [Bi-directional GitOps with Flux](docs/bi-directional-flux.md) +- [Bi-directional GitOps with Argo CD](docs/bi-directional-argocd.md) ## Quick start @@ -153,7 +152,8 @@ scope and one destination folder. Inspect the initial commits before making a li which source files changed. The operator pushes directly to the configured branch. If Flux or Argo CD deploys the folder you -write, read [alongside Flux and Argo CD](#alongside-flux-and-argo-cd) first. +write, read +[bi-directional GitOps with Flux and Argo CD](#bi-directional-gitops-with-flux-and-argo-cd) first. ### Limits before 1.0