From c685a4036c96644542b61a6e49495200c080c797 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 06:06:51 +0000 Subject: [PATCH 1/3] chore(log): lower the three Failure A diagnostics to V(1) The "GitTarget status published", "render scope result accepted" and "superseded by a newer resync" lines were raised to Info while Failure A of watch-plane-status-convergence-failures.md was open. A is fixed, so the release would otherwise ship them as Info noise; -v=1 brings them back when reproducing Failure B. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/controller/gittarget_controller.go | 11 ++++------- internal/watch/event_router.go | 10 ++++------ internal/watch/render_fidelity_gate.go | 12 ++++-------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/internal/controller/gittarget_controller.go b/internal/controller/gittarget_controller.go index 6c979b14..9328c144 100644 --- a/internal/controller/gittarget_controller.go +++ b/internal/controller/gittarget_controller.go @@ -278,13 +278,10 @@ func (r *GitTargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( // the WINNER's older answer and nothing re-enqueues it, because the For() predicate filters // status-only updates by design. requeue := st.requeueAfter(gitTargetRequeue(rd)) - // TEMPORARY at Info, while Failure A is open. The data plane can converge and this status not - // follow it: a run has shown the render gate reaching True and both this GitTarget and every - // WatchRule on it still publishing "Rechecking" two minutes later, with no dropped reconcile - // request to explain it. This is the only place that publishes the axis, so it is the only - // place that can say what it published and how long it intends to wait before saying anything - // again. - log.Info("GitTarget status published", + // This is the only place that publishes the axis, so it is the only place that can say what it + // published and how long it intends to wait before saying anything again (the diagnostic that + // named Failure A in docs/design/watch-plane-status-convergence-failures.md). + log.V(1).Info("GitTarget status published", "writeLost", st.writeLost(), "gitTarget", target.Namespace+"/"+target.Name, "render", string(observed.axes.Render.Status)+"/"+observed.axes.Render.Reason, diff --git a/internal/watch/event_router.go b/internal/watch/event_router.go index 21a22e21..062d491f 100644 --- a/internal/watch/event_router.go +++ b/internal/watch/event_router.go @@ -473,12 +473,10 @@ func (r *EventRouter) handleScopedResyncError( // the replacement's own report is then accepted, and a report is refused when it arrives // under a revision the plan has moved past. // - // TEMPORARY at Info, restored. It is normally V(1) because coalescing is routine under - // exactly the load it protects against, and it was lowered once on the assumption that the - // render-fidelity condition had made every lost report visible. It has not: this path - // skips the RETENTION report too, and lowering it re-blinded that path in the very local - // reproduction of Failure B that followed. Lower it again only when B is closed. - r.Log.Info("per-type "+kind+" superseded by a newer resync; its roll-up reports were skipped", + // V(1) because coalescing is routine under exactly the load it protects against. This path + // skips the RETENTION report too, so raise the verbosity when reproducing Failure B of + // docs/design/watch-plane-status-convergence-failures.md. + r.Log.V(1).Info("per-type "+kind+" superseded by a newer resync; its roll-up reports were skipped", "gitDest", gitDest.String(), "collection", collection.String()) return } diff --git a/internal/watch/render_fidelity_gate.go b/internal/watch/render_fidelity_gate.go index d43b72fb..1ce8a36b 100644 --- a/internal/watch/render_fidelity_gate.go +++ b/internal/watch/render_fidelity_gate.go @@ -78,14 +78,10 @@ func (m *Manager) MarkTargetRenderFidelityScopeClean( m.logUnappliedFidelityReport(target, collection, revision, "clean") return } - // TEMPORARY at Info, while Failure A is open. The refusal paths above are logged and the - // accept path was not, so SILENCE from this function was ambiguous between "never called" and - // "called and accepted" -- and the reproduction that finally carried every other diagnostic - // produced exactly that silence, which decided nothing. - // - // It is bounded: a scope accepts one report per revision, and a revision only moves when the - // plan restarts the collection. Lower it to V(1) once A is named. - m.Log.WithName("render-fidelity").Info("render scope result accepted", + // Logged so that silence from this function is not ambiguous between "never called" and + // "called and accepted". It is bounded: a scope accepts one report per revision, and a revision + // only moves when the plan restarts the collection. + m.Log.WithName("render-fidelity").V(1).Info("render scope result accepted", "gitDest", target.String(), "collection", collection.String(), "revision", revision, "state", string(status.State), "status", status.Message) // Publish the gate's CURRENT status, not the one this drain observed. Sibling drains record From 532c44eb4b7ccb5e947b47ec263351bcd2d43265 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 06:06:51 +0000 Subject: [PATCH 2/3] refactor(api): write the CRD descriptions for users, drop the loud-rejection fields Every field, kind and array-item description is rewritten in one style for an operator reading kubectl explain: what it is, what omitting it does, the gotchas, immutability last. Design rationale moves into comments the schema does not carry. The text is about 18% shorter, and the longest description drops from 316 to 144 words. RepairPending no longer leans on "flush", which the CRDs never define. AGENTS.md records the conventions. The fields kept only to refuse an old spelling are removed with their CEL rules: GitTarget's gitProviderRef, branch, parentBranch and clusterProviderRef, FluxClaim's and ArgoCDClaim's clusterProviderRef, and GitProvider's receiver. None was in v0.53.0. kubectl apply now refuses them as unknown fields, and UPGRADING.md says so. Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 10 + api/v1alpha3/access_grants.go | 133 ++--- api/v1alpha3/applierclaim_types.go | 22 +- api/v1alpha3/argocdclaim_types.go | 109 ++-- api/v1alpha3/clusterwatchrule_types.go | 88 ++- api/v1alpha3/commitrequest_types.go | 86 ++- api/v1alpha3/fluxclaim_types.go | 97 ++-- api/v1alpha3/gitbranch_types.go | 120 ++-- api/v1alpha3/gitprovider_types.go | 178 +++--- api/v1alpha3/gitreceiver_types.go | 44 +- api/v1alpha3/gittarget_types.go | 410 +++++--------- api/v1alpha3/kubeprovider_types.go | 123 ++-- api/v1alpha3/prune_policy.go | 6 +- api/v1alpha3/shared_types.go | 16 +- api/v1alpha3/watchrule_types.go | 130 ++--- api/v1alpha3/zz_generated.deepcopy.go | 44 +- .../bases/configbutler.ai_argocdclaims.yaml | 134 ++--- .../configbutler.ai_clusterwatchrules.yaml | 112 ++-- .../bases/configbutler.ai_commitrequests.yaml | 91 ++- .../crd/bases/configbutler.ai_fluxclaims.yaml | 122 ++-- .../bases/configbutler.ai_gitbranches.yaml | 145 +++-- .../bases/configbutler.ai_gitproviders.yaml | 149 +++-- .../bases/configbutler.ai_gitreceivers.yaml | 44 +- .../crd/bases/configbutler.ai_gittargets.yaml | 529 +++++++----------- .../bases/configbutler.ai_kubeproviders.yaml | 252 ++++----- .../crd/bases/configbutler.ai_watchrules.yaml | 152 ++--- docs/TODO.md | 14 - docs/UPGRADING.md | 13 +- hack/crdfields/fields.yaml | 14 - .../superseded_fields_admission_test.go | 74 +-- 30 files changed, 1385 insertions(+), 2076 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9b73cd15..f2ccfd89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,6 +111,16 @@ See [Documentation checks](./CONTRIBUTING.md#documentation-checks). gotchas — not why it was designed that way. Put design rationale in a comment block separated from the doc comment **by a blank line**: Go does not treat it as the doc comment, so controller-gen leaves it out of the schema while it stays in the source. +- **Write CRD descriptions in one house style**, for an operator reading `kubectl explain`: + the first sentence says what the field is or does (`GitBranchRef names the GitBranch ...`); + then what omitting it does (`Omitted, ...`, or `Defaults to "5s".` for a schema default); + then the gotchas; immutability last (`Immutable: delete and recreate the X to change it.`). + 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 + 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 the doc comment. On a TYPE, put it above the `+kubebuilder:` marker block, never between the markers and the doc — that displaces the markers and they are dropped without any error diff --git a/api/v1alpha3/access_grants.go b/api/v1alpha3/access_grants.go index a6c39a4c..d13113bb 100644 --- a/api/v1alpha3/access_grants.go +++ b/api/v1alpha3/access_grants.go @@ -44,23 +44,22 @@ const ConsumerNamespaceVariable = "{consumer.namespace}" // name bounds nothing on its own; beside the namespace it is a naming convention. const ConsumerNameVariable = "{consumer.name}" -// GrantNamespace is one control-cluster namespace a grant admits by name. +// 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 one namespace in a source or applier scope: a namespace name, -// "{consumer.namespace}" for the namespace of the same name as 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 names a cluster-scoped resource type: resource.group, a bare resource for -// the core group, or "*" for every one. +// 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 @@ -69,9 +68,9 @@ 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 is a pattern matched against a whole branch name: literal text, '*' for any -// sequence of characters ('/' included), 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~^:?\[\\]*$` @@ -94,41 +93,39 @@ 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 from: Same (the namespace of the object holding the grant, not - // the consumer's), 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"` - // Names are the control-cluster namespaces admitted with from: Names, by exact name. + // Names are the namespaces admitted with from: "Names", by exact name. // +optional // +listType=set // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=64 Names []GrantNamespace `json:"names,omitempty"` - // Selector matches labels on control-cluster Namespaces, with from: Selector. It must select - // something: 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 GitBranches in some control-cluster namespaces to claim the -// branches its patterns match through a GitProvider. +// GitProviderGitBranchGrant admits the GitBranches of some namespaces to claim, through a +// GitProvider, the branches its patterns match. type GitProviderGitBranchGrant struct { - // ConsumerNamespaces are the control-cluster 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, as patterns matched against - // the whole branch name. '*' matches any sequence of characters, '/' included, so "*" alone is - // every branch, and a name with no '*', such as "main", matches only itself. - // '{consumer.namespace}' and '{consumer.name}' are the claiming GitBranch's namespace and name, - // each a whole path segment: "teams/{consumer.namespace}/*". '{consumer.name}' needs - // '{consumer.namespace}' in the same pattern, "teams/{consumer.namespace}/{consumer.name}": it is - // a naming convention, not a bound, since 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 @@ -136,59 +133,47 @@ type GitProviderGitBranchGrant struct { BranchPatterns []BranchPattern `json:"branchPatterns"` } -// GitBranchGitTargetGrant admits GitTargets in some control-cluster namespaces to write through a -// GitBranch. +// GitBranchGitTargetGrant admits the GitTargets of some namespaces to write through a GitBranch. type GitBranchGitTargetGrant struct { - // ConsumerNamespaces are the control-cluster 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 GitTargets in some control-cluster namespaces to mirror through a -// KubeProvider, and bounds what they may mirror. +// 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 control-cluster 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 source-cluster namespaces that a WatchRule feeding an admitted GitTarget may - // name in rules[].sourceNamespace. Entries are exact names, '{consumer.namespace}' for the - // namespace named like the GitTarget's, or '*'. Omitted, this grant gives no namespace. + // 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 effective scope is the union of sourceNamespaces from all grants admitting the GitTarget, - // after expanding variables. Every WatchRule item must be allowed by that union, an omitted - // sourceNamespace included (it names the WatchRule's own namespace). This is a permission, not a - // selection: listing a namespace watches nothing until a WatchRule names it. - // - // It is not a filter on "*". A rule item with sourceNamespace "*" needs '*' in at least one - // admitting grant; otherwise it is refused, never narrowed to the namespaces listed. With '*' - // granted, such an item is one cluster-wide watch, bounded only by this KubeProvider credential's - // RBAC. - // - // The check runs when the WatchRule reconciles, and again when a grant changes, never at - // admission. A WatchRule with any item outside the union is refused as a whole, in its status - // (SourceNamespaceNotAllowed), and watches nothing. The scope bounds what is read, never where - // objects are written in Git, and the credential's RBAC still applies inside it. + // 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 resource types a ClusterWatchRule may mirror through an - // admitted GitTarget: resource.group (clusterroles.rbac.authorization.k8s.io), a bare resource for - // the core group only (nodes), or '*' for every one, including CRDs installed later. An identity - // 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 effective scope is the union of clusterResources from all grants admitting the GitTarget. - // This is a permission, not a selection: listing a type mirrors nothing until a ClusterWatchRule - // asks for it. A ClusterWatchRule item with '*' in apiGroups or resources needs '*' in at least - // one admitting grant; otherwise it is refused, never narrowed to the types listed. The check runs - // when the ClusterWatchRule reconciles, never at admission, and a rule asking for any type outside - // the union is refused as a whole, in its status (ClusterResourceNotAllowed). + // 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 @@ -196,24 +181,20 @@ type KubeProviderGitTargetGrant struct { ClusterResources []ClusterResourceIdentity `json:"clusterResources,omitempty"` } -// KubeProviderApplierClaimGrant admits applier claims in some control-cluster namespaces to hold an -// applier through a KubeProvider, and bounds where that applier may live. +// 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 control-cluster namespaces whose applier claims 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 namespaces in this provider's cluster in which an admitted applier claim's - // Kustomization or Application may live: exact names, '{consumer.namespace}' for the namespace - // named like the applier claim's, or '*' for every namespace. - // - // The effective scope is the union of applierNamespaces from all grants admitting the applier - // claim, after expanding variables. This is a permission, not a selection: it is checked against - // one namespace, that of the applier claim's kustomizationRef or applicationRef. The check runs - // when the applier claim reconciles, and again when a grant changes, never at admission. An - // applier outside the union is refused, in the applier claim's status - // (ApplierNamespaceNotAllowed), and an applier 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 diff --git a/api/v1alpha3/applierclaim_types.go b/api/v1alpha3/applierclaim_types.go index 07261f26..c2ef0b6b 100644 --- a/api/v1alpha3/applierclaim_types.go +++ b/api/v1alpha3/applierclaim_types.go @@ -38,7 +38,7 @@ const ( // ApplierClaimReference names an applier claim in the GitTarget's own namespace. type ApplierClaimReference struct { - // Kind is the applier claim's kind: FluxClaim or ArgoCDClaim. + // Kind is the applier claim's kind: "FluxClaim" or "ArgoCDClaim". // +required // +kubebuilder:validation:Enum=FluxClaim;ArgoCDClaim Kind string `json:"kind"` @@ -111,14 +111,18 @@ func (p ApplyOperationPhase) Completed() bool { // 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 and Unresolved are in - // flight: while one is, an edit to an object the applier applies can still be reverted by it, - // and the applier is asked nothing else. Succeeded and Failed have completed. + // 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. With several GitTargets on the applier 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"` @@ -132,13 +136,13 @@ type ApplyOperation struct { // +optional Token string `json:"token,omitempty"` - // SuspendToken is the reconcile.fluxcd.io/requestedAt value the suspend after the apply carried. - // Once the Kustomization reports it handled, nothing started before the suspend is still + // 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 it applied once the operation completed. + // 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"` diff --git a/api/v1alpha3/argocdclaim_types.go b/api/v1alpha3/argocdclaim_types.go index 56abedc7..0c8f0a47 100644 --- a/api/v1alpha3/argocdclaim_types.go +++ b/api/v1alpha3/argocdclaim_types.go @@ -18,40 +18,32 @@ import ( // applies) has none. const ArgoCDClaimFinalizer = "configbutler.ai/argocd-claim" -// spec.clusterProviderRef was replaced by spec.kubeProviderRef, with the cluster-scoped kind it -// named. It stays in the schema only to be refused by name; the rule sits on spec so CRD validation -// ratcheting cannot wave a stored value through on the next edit. - // ArgoCDClaimSpec names the Argo CD Application the applier claim holds, and the cluster it is in. -// +kubebuilder:validation:XValidation:rule="!has(self.clusterProviderRef)",message="spec.clusterProviderRef was replaced by spec.kubeProviderRef, which names a namespaced KubeProvider" type ArgoCDClaimSpec struct { // Immutable for the reason FluxClaimSpec's references are: the applier claim holds what it names. - // KubeProviderRef names the KubeProvider whose cluster holds the Application, such as a - // management cluster running Argo CD for the source cluster. Namespace omitted means this - // applier claim's own namespace. The provider's spec.allowedApplierClaims must admit this applier - // claim's namespace, with the Application's namespace in applierNamespaces. Its identity needs - // get, update and patch on the Application. Immutable. + // 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"` - // RemovedClusterProviderRef is the replaced spelling of kubeProviderRef. It is never read; - // setting it is refused. - // +optional - RemovedClusterProviderRef *meta.LocalObjectReference `json:"clusterProviderRef,omitempty"` - - // ApplicationRef is the Argo CD Application that applies the folders of the GitTargets naming - // this applier claim. It must have a single spec.source whose targetRevision names a branch. The - // applier claim holds its automated sync off: it applies spec.syncPolicy.automated.enabled: false, - // which keeps automated.prune for the syncs GitOps Reverser starts, switches it off again when - // somebody switches it on, and gives it up when it lets go. An Application an ApplicationSet - // generated, or whose switch another field manager applies, must have automated sync off - // already: the switch is theirs, and the applier claim only checks it. GitOps Reverser starts each - // sync by setting the Application's operation, as Argo CD's automated sync does, pinned to the - // commit it checked, and never replaces an operation that is running. Its namespace defaults to - // the applier claim's namespace. Immutable. + // 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. + // + // 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" @@ -64,21 +56,20 @@ type ArgoCDClaimStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are Configured (an eligible GitTarget names the applier claim; the Application exists, - // syncs a single source, has automated sync off or a switch the applier claim may hold, may be - // written, and no other ArgoCDClaim holds it), InControl (whether GitOps Reverser decides, right - // now, when the Application syncs. True: AutomatedSyncOff, automated sync is off, held by GitOps - // Reverser or kept off by the switch's owner. False: Releasing, being handed back; Revoking, being - // handed back because a grant no longer admits it; NotReferenced. NotConfigured: the applier claim - // is not Configured, or automated sync was switched on since it was), and the kstatus - // Ready/Reconciling/Stalled trio. An applier claim acts only while an - // eligible GitTarget references it: one in its namespace that names it, is admitted by its - // KubeProviders, and writes the repository and branch the Application syncs. An applier claim no - // GitTarget names reads nothing in the Application's cluster and is Configured=Unknown, - // Ready=True, both NotReferenced; one named only by GitTargets that are not eligible is - // Configured NotBound or a refusal. Of the ArgoCDClaims in line for one Application in one - // cluster, one holds it: the one holding it already, then the one created first. The others are - // Configured=False (HeldByAnotherClaim) until it lets go, and the next in line takes it over. + // 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. + // + // 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. // +optional // +listType=map // +listMapKey=type @@ -86,8 +77,8 @@ 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, published while an eligible GitTarget - // references the applier claim. A GitTarget naming this applier claim must write the repository it syncs, on + // 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"` @@ -97,18 +88,17 @@ type ArgoCDClaimStatus struct { // +optional Operation *ApplyOperation `json:"operation,omitempty"` - // RepairPending is true while a Repair sync is pending for the Application: a sync reverted an - // API edit made after its flush, so the cluster runs an older value than Git 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 the applier claim first switched its - // automated sync off, which handing it back restores: NotSuspended (automated sync was on) or - // Suspended (it was off). Recorded with the first switch-off, and cleared once the Application is - // handed back. Absent, a hand-back gives the switch up and leaves what the Application's own spec - // says. Either way, a switch that an ApplicationSet or another field manager owns by then keeps - // the value that owner set. + // 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"` } @@ -119,8 +109,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 is 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"` } @@ -139,10 +129,11 @@ 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 holds an Argo CD Application's automated sync off for GitOps Reverser, which starts -// every sync it runs, each after publishing the edits pending on the branch: after a commit -// GitOps Reverser did not push, after a refused edit, after a reverted edit, and to adopt what the -// API created or converge the Application (CatchUp). 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"` @@ -150,11 +141,11 @@ type ArgoCDClaim struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the Application the applier claim holds + // spec defines the desired state of ArgoCDClaim. // +required Spec ArgoCDClaimSpec `json:"spec"` - // status defines the observed state of ArgoCDClaim + // status defines the observed state of ArgoCDClaim. // +optional Status ArgoCDClaimStatus `json:"status,omitempty,omitzero"` } diff --git a/api/v1alpha3/clusterwatchrule_types.go b/api/v1alpha3/clusterwatchrule_types.go index 383eff56..638a7839 100644 --- a/api/v1alpha3/clusterwatchrule_types.go +++ b/api/v1alpha3/clusterwatchrule_types.go @@ -23,75 +23,54 @@ const ( // ClusterWatchRuleSpec defines the desired state of ClusterWatchRule. type ClusterWatchRuleSpec struct { - // GitTargetRef names the GitTarget this rule feeds. A ClusterWatchRule has no namespace of its - // own, so the namespace is required here rather than defaulted. + // 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 define which CLUSTER-SCOPED resources to watch. - // Multiple rules create a logical OR - a resource matching ANY rule is watched. - // A rule that resolves to no cluster-scoped type simply watches nothing; use a WatchRule with - // spec.rules[].sourceNamespace 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"` } -// ClusterResourceRule defines which CLUSTER-SCOPED resources to watch. It deliberately has no -// sourceNamespace: cluster-scoped objects have no namespace, so there is nothing to select. +// 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. type ClusterResourceRule struct { - // APIGroups to match. Empty string ("") matches the core API group. - // If omitted, GitOps Reverser resolves the resource name across all served API groups. - // Wildcards supported: "*" matches all groups. - // Examples: - // - [""] matches core API (nodes, namespaces) - // - ["rbac.authorization.k8s.io"] matches RBAC resources - // - ["*"] matches all groups - // - [] resolves a named resource only when it is served by one API group + // 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 to match. If empty, uses the preferred served version for each group/resource. - // Wildcards supported: "*" matches all versions. - // Examples: - // - ["v1"] matches only v1 version - // - ["*"] matches all served versions - // - [] matches the preferred served version + // 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 to match (plural names like "nodes", "clusterroles"). - // This field is required and determines which resource types trigger this rule. - // Wildcard semantics follow Kubernetes admission webhook patterns: - // - "*" matches all resources - // - "nodes" matches exactly nodes - // - "pods" matches exactly pods (for namespaced scope) - // - // Note: Subresources cannot be added here. Values containing "/" (for example - // "pods/log" or "pods/*") are rejected by the API because subresources are - // not supported for list/watch snapshot planning. Prefix/suffix wildcards - // like "pod*" or "*.example.com" are NOT supported. Use exact matches or the - // "*" wildcard for broad matching. + // 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 // +kubebuilder:validation:items:Pattern=`^[^/]*$` Resources []string `json:"resources"` - // ObjectSelector selects which objects of the matched types are mirrored, by label. The API - // server applies it to every LIST and WATCH this item opens, with list/watch semantics: an - // object that stops matching leaves the collection exactly as a deleted one does, and the - // GitTarget's spec.prune.mode decides what that does to its Git document. - // It is a standard Kubernetes label selector: matchLabels and matchExpressions, ANDed. - // Omitted or {} selects every object. A selector that does not parse refuses the whole rule - // (ResourcesResolved=False, reason InvalidObjectSelector). - // - // A selected mirror owns every document of its type, including documents the selector never - // matched: under prune.mode Always, a complete snapshot removes them. - // Two rules feeding one GitTarget that match the same type must use the same selector; the - // newer rule is refused. + // 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). // +optional ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` } @@ -102,7 +81,9 @@ type ClusterWatchRuleStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions represent the latest available observations of the ClusterWatchRule's state. + // 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 @@ -110,7 +91,7 @@ type ClusterWatchRuleStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Streams is the bounded stream-readiness roll-up for the types this rule resolves. + // Streams counts the types this ClusterWatchRule resolves, by watch state. // +optional Streams *StreamsStatus `json:"streams,omitempty"` } @@ -131,14 +112,9 @@ 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 on the source cluster its GitTarget mirrors -// from — Nodes, PersistentVolumes, StorageClasses, ClusterRoles, CRDs, and the like. Scope is -// carried by the rule KIND, so it has no per-rule scope choice and no source-namespace selection. -// -// It is cluster-scoped and requires cluster-admin permissions. Its gitTargetRef names a GitTarget -// (namespace required), whose namespace must be admitted by that target's KubeProvider. To -// mirror NAMESPACED resources use a WatchRule in the tenant namespace and set -// spec.rules[].sourceNamespace, whose "*" reaches every namespace the source credential can read. +// 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 6c28d19d..9327a381 100644 --- a/api/v1alpha3/commitrequest_types.go +++ b/api/v1alpha3/commitrequest_types.go @@ -23,19 +23,16 @@ import ( // +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 whose commit window this request attaches to. - // The GitTarget must be in the same namespace as this CommitRequest. + // 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"` - // Message is an optional commit message for the finalized commit. When - // omitted, the target's liveTemplate is used. Template-like text remains literal. - // - // When present it is limited to 1-1024 Unicode characters and used - // verbatim as the commit message. Newlines are allowed so a subject and - // body can be supplied; all other ASCII control characters (including tab - // and carriage return) are rejected. Whitespace-only messages are rejected. + // 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. // +optional // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=1024 @@ -47,20 +44,19 @@ type CommitRequestSpec struct { // 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 attached. They replace the GitTarget's timers - // for that window, shorter or longer. Omitted, it is the defaults of every field below: attach - // to the author's current or next window within 2s, then close it 2s later. + // 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 the request does when it ends with nothing to commit: - // because attachTimeout ran out with no eligible window, or because the window it attached to - // closed, for any reason, without changing Git. Resolve finishes without a commit; CommitEmpty - // records spec.message in an empty commit, and the Ready reason still says which of the two - // happened. A window that belonged to another author never falls back to an empty commit. A - // request on a GitTarget that may not be written (suspended, or render fidelity not established) - // fails with that reason under either value. 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 @@ -100,19 +96,19 @@ const ( // +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 window if one is open - // when the worker registers the request, and otherwise to the next one; Next first closes the - // author's open window, under its own message, and attaches to the next one. 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 to attach to, counted from when - // the branch worker registers it, as a Go duration string. "0s" attaches to a window already - // open, or gives up at once. 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 fact. + // 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))+$" @@ -120,17 +116,17 @@ 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 after this much silence; every write it collects - // restarts it. Omitted, the window has no idle close and maxDuration alone ends it. At most - // "5m". + // 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 much - // keeps arriving. "0s" finalizes right after the attach. 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))+$" @@ -156,13 +152,15 @@ type CommitRequestWindow struct { // 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 most recent generation observed by the controller. + // ObservedGeneration is the latest generation observed by the controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions report the request's progress and terminal outcome: the Ready - // summary, the kstatus Reconciling/Stalled pair, and the domain conditions - // AuthorAttributed and Pushed. + // 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 @@ -170,12 +168,11 @@ type CommitRequestStatus struct { // +patchMergeKey=type Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Branch is the Git branch the GitTarget commits to. Populated once the - // finalize resolves. + // Branch is the branch the commit is made on. Set once the request ends. // +optional Branch string `json:"branch,omitempty"` - // Commit is the resulting commit hash. Set when the commit was pushed (Pushed=True). + // Commit is the commit hash. Set once the commit is pushed (Pushed=True). // +optional Commit string `json:"commit,omitempty"` } @@ -192,10 +189,9 @@ 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 is a one-shot "save" signal. It attaches to its author's current or next -// commit window on the referenced GitTarget, closes that window on its own timers instead of the -// target's, and gives the resulting commit its message. The commit hash is reported in status -// once the commit 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"` @@ -203,11 +199,11 @@ type CommitRequest struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the desired state of CommitRequest + // spec defines the desired state of CommitRequest. It is immutable after creation. // +required Spec CommitRequestSpec `json:"spec"` - // status defines the observed state of CommitRequest + // status defines the observed state of CommitRequest. // +optional Status CommitRequestStatus `json:"status,omitempty,omitzero"` } diff --git a/api/v1alpha3/fluxclaim_types.go b/api/v1alpha3/fluxclaim_types.go index dfe3b82c..2a1f9fe1 100644 --- a/api/v1alpha3/fluxclaim_types.go +++ b/api/v1alpha3/fluxclaim_types.go @@ -17,35 +17,26 @@ import ( // Kustomization has none. const FluxClaimFinalizer = "configbutler.ai/flux-claim" -// spec.clusterProviderRef was replaced by spec.kubeProviderRef, with the cluster-scoped kind it -// named. It stays in the schema only to be refused by name; the rule sits on spec so CRD validation -// ratcheting cannot wave a stored value through on the next edit. - // FluxClaimSpec names the Flux Kustomization the applier claim holds, and the cluster it is in. -// +kubebuilder:validation:XValidation:rule="!has(self.clusterProviderRef)",message="spec.clusterProviderRef was replaced by spec.kubeProviderRef, which names a namespaced KubeProvider" 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 whose cluster holds the Kustomization, such as a - // management cluster running Flux for the source cluster. Namespace omitted means this - // applier claim's own namespace. The provider's spec.allowedApplierClaims must admit this applier - // claim's namespace, with the Kustomization's namespace in applierNamespaces. Its identity needs - // get and patch on the Kustomization and its GitRepository. Immutable. + // 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"` - // RemovedClusterProviderRef is the replaced spelling of kubeProviderRef. It is never read; - // setting it is refused. - // +optional - RemovedClusterProviderRef *meta.LocalObjectReference `json:"clusterProviderRef,omitempty"` - - // KustomizationRef is the Flux Kustomization that applies the folders of the GitTargets naming - // this applier claim. Its spec.sourceRef must be a GitRepository that follows a branch. Its namespace - // defaults to the applier claim's namespace. Immutable. + // 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" @@ -58,21 +49,20 @@ type FluxClaimStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions are Configured (an eligible GitTarget names the applier claim; the Kustomization exists, - // applies a GitRepository that follows a branch, may be patched, and no other FluxClaim holds - // it), InControl (whether GitOps Reverser decides, right now, when the Kustomization applies. True: - // Suspended, suspended under GitOps Reverser with the last suspend handled; Applying, resumed for - // an apply GitOps Reverser started. False: Suspending, a suspend is not handled yet; Releasing, - // being handed back; Revoking, being handed back because a grant no longer admits it; - // NotReferenced. Unknown: NotConfigured), and the kstatus - // Ready/Reconciling/Stalled trio. An applier claim acts only while an eligible GitTarget references it: - // one in its namespace that names it, is admitted by its KubeProviders, and writes the - // repository and branch the GitRepository fetches. An applier claim no GitTarget names reads nothing - // in the Kustomization's cluster and is Configured=Unknown, Ready=True, both NotReferenced; one - // named only by GitTargets that are not eligible is Configured NotBound or a refusal. Of the - // FluxClaims in line for one Kustomization in one cluster, one holds it: the one with an apply - // in flight, then the one holding it already, then the one created first. The others are - // Configured=False (HeldByAnotherClaim) until it lets go, and the next in line takes it over. + // 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. + // + // 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. // +optional // +listType=map // +listMapKey=type @@ -80,9 +70,9 @@ 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, published while an - // eligible GitTarget references the applier claim. A GitTarget naming this applier claim 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"` @@ -91,23 +81,23 @@ type FluxClaimStatus struct { // +optional Operation *ApplyOperation `json:"operation,omitempty"` - // RepairPending is true while a Repair apply is pending for the Kustomization: an apply reverted - // an API edit made after its flush, so the cluster runs an older value than Git 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 the applier claim first suspended it, - // which handing it back restores: one that was Suspended stays suspended, one that was - // NotSuspended is resumed. Recorded with the first suspend, and cleared once the Kustomization is - // handed back. Absent, a hand-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, its namespace defaulted. + // GitRepository is the Kustomization's spec.sourceRef, with its namespace filled in. // +required GitRepository meta.NamespacedObjectReference `json:"gitRepository"` @@ -123,9 +113,9 @@ 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 are the other objects in the cluster that apply this GitRepository and that no - // FluxClaim holds: Kustomizations, and HelmReleases whose chart comes from it. Each applies a - // new artifact the moment it arrives. 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"` @@ -145,10 +135,11 @@ 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 holds a Flux Kustomization for GitOps Reverser, which starts every apply it makes, -// each after publishing the edits pending on the branch: after a commit GitOps Reverser did not -// push, after a refused edit, after a reverted edit, and to adopt what the API created or converge -// the Kustomization (CatchUp). 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"` @@ -156,11 +147,11 @@ type FluxClaim struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the Kustomization the applier claim holds + // spec defines the desired state of FluxClaim. // +required Spec FluxClaimSpec `json:"spec"` - // status defines the observed state of FluxClaim + // status defines the observed state of FluxClaim. // +optional Status FluxClaimStatus `json:"status,omitempty,omitzero"` } diff --git a/api/v1alpha3/gitbranch_types.go b/api/v1alpha3/gitbranch_types.go index ef557fa2..ee380666 100644 --- a/api/v1alpha3/gitbranch_types.go +++ b/api/v1alpha3/gitbranch_types.go @@ -21,20 +21,19 @@ import ( // +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 this branch is written - // with. Namespace omitted means this GitBranch's own namespace. The GitBranch claims its branch - // only while that GitProvider's spec.allowedGitBranches admits this namespace and the 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 one of the - // patterns the GitProvider's spec.allowedGitBranches give this GitBranch's namespace. Only one - // GitBranch writes a repository branch: the oldest admitted one - // whose credential proves it can open a push session holds it, and any other GitBranch for the - // same repository and branch reports Stalled with reason BranchConflict. - // Immutable: delete and recreate the GitBranch to change it. + // 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 Branch string `json:"branch"` @@ -44,23 +43,23 @@ 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 the branch compared against while - // spec.branch does not exist on the remote. Omitted: the branch the remote's HEAD points at (its - // default branch), or a branch with no history when the repository is empty. When set, it must - // exist on the remote for spec.branch to be created; otherwise this GitBranch reports - // ParentBranchNotFound and nothing is written. Once spec.branch exists, it is used as it is and - // the parent is not 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. + // 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"` - // AllowedGitTargets says which GitTargets may write through this GitBranch. Omitted, it defaults - // to one grant for this GitBranch's own namespace (consumerNamespaces: {from: Same}). A list you - // write is the whole policy: nothing is added to it, so this namespace is admitted only if the - // list grants it access, and [] admits no GitTarget. Grants combine by union. Mutable: claiming - // a branch includes the right to share it. + // 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. // +optional // +listType=atomic // +kubebuilder:validation:MaxItems=64 @@ -74,8 +73,8 @@ type GitBranchStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions report whether this GitBranch holds its repository branch and can write it: the - // Ready summary plus the kstatus Reconciling/Stalled pair. + // 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 @@ -83,32 +82,28 @@ type GitBranchStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Repository is the GitProvider's repository URL, normalized: host and path, with no scheme, - // user or ".git" suffix. Together with spec.branch it is the key only one GitBranch may hold, so - // two GitProviders spelling 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 to this branch, which made the - // branch worker look at once instead of at its next refresh. It records receipt only: whether - // that look reached the remote is status.remote's to say. Absent means no delivery has reached - // this branch: no GitReceiver is set up, the Git host's webhook does not reach one, the delivery - // names the repository by a URL other than the GitProvider's spec.url, or no branch worker writes - // the branch because no GitTarget references this GitBranch. It advances at most once a minute, - // and is cleared when the GitProvider is recreated for another repository. + // 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 proved. It is - // renewed by every confirmed look: a successful push as well as a fetch. Absent means nothing - // has proved anything yet: nothing has looked, or the GitProvider was recreated against a - // different repository and what was published no longer describes the one it names. + // 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 reference this GitBranch, from any namespace. More than - // one means they share its branch worker; each reports its own folder's health on 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"` } @@ -119,9 +114,8 @@ type GitBranchStatus struct { // 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 at all, which is not an error: a branch does not exist without a commit, and a - // branch nothing has written to has nothing there yet. + // 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"` @@ -129,17 +123,15 @@ type GitBranchRemoteStatus struct { // +optional LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"` - // VerifiedBy is what proved it: `Push` means the server accepted a ref update of ours, so - // this commit is our own work; `Fetch` means we went and looked, and this is what was - // there. A `Fetch` next to a commit no publication of yours produced is how a foreign push - // to the branch is read off kubectl. + // 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, observed with it: present only while the - // remote does not carry the branch. Once the branch exists its parent is not followed, so this - // is omitted. + // 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"` } @@ -163,19 +155,19 @@ const ( // 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's tip), Missing (the parent - // is not on the remote; nothing is written) or Unborn (an empty repository, with no refs at - // all, and no parent configured; 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 branch the remote's HEAD names 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: the commit 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"` } @@ -194,9 +186,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 through a GitProvider's credential. It holds -// the branch for every GitTarget that references it: one branch worker, one credential, one -// checkout. 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"` @@ -204,11 +196,11 @@ type GitBranch struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the desired state of GitBranch + // spec defines the desired state of GitBranch. // +required Spec GitBranchSpec `json:"spec"` - // status defines the observed state of GitBranch + // status defines the observed state of GitBranch. // +optional Status GitBranchStatus `json:"status,omitempty,omitzero"` } diff --git a/api/v1alpha3/gitprovider_types.go b/api/v1alpha3/gitprovider_types.go index 4c12436a..42a64857 100644 --- a/api/v1alpha3/gitprovider_types.go +++ b/api/v1alpha3/gitprovider_types.go @@ -19,78 +19,58 @@ import ( // 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" -// -// spec.receiver was replaced by the GitReceiver kind, which wakes every GitProvider whose spec.url -// its deliveries name. A pruned field would apply cleanly and leave a provider that used to be -// notified silently unnotified, so the old spelling stays in the schema only to be refused by name. -// The rule sits on spec, not on the field, so CRD validation ratcheting cannot wave a stored value -// through on the next edit. -// +kubebuilder:validation:XValidation:rule="!has(self.receiver)",message="spec.receiver was replaced by a GitReceiver: create one with this type and secretRef, and remove spec.receiver; deliveries wake every GitProvider whose spec.url they name" type GitProviderSpec struct { - // URL of the repository (HTTP/SSH). - // Immutable: delete and recreate the GitProvider to point at a different repository. + // 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"` - // SecretRef for authentication credentials (may be nil for public repos) - // - // An OMITTED secretRef means anonymous access. An empty name is refused rather than treated as - // one: a typo there would otherwise downgrade a private repository to anonymous silently, and - // the failure would surface as an authentication error against the remote rather than as a - // mistake in this object. + // An empty name is refused rather than read as anonymous access: a typo there would otherwise + // 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. // +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 optionally points at a namespace-local ConfigMap or Secret holding SSH - // known_hosts, so host trust can be centralized across GitProviders on the same host instead - // of repeated in every credentials Secret. It is used only for SSH and ignored for HTTP auth. - // Host keys are resolved in priority order: the credentials Secret's own known_hosts, then this - // ref, then the install-level default known-hosts ConfigMap; if none yields valid keys, SSH - // fails closed. + // 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, and which branches: a GitBranch claims only while a grant admits its namespace and - // one of that grant's branchPatterns matches its spec.branch. It has no default, unlike the grant - // lists of GitBranch and KubeProvider, because which branches a namespace may claim is always - // written: omitted or empty, no GitBranch claims through it, this GitProvider's own namespace - // included. Grants combine by union. Who may write through a claimed branch is that GitBranch's - // spec.allowedGitTargets. + // 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 configures the commit identity and signing behavior this connection uses. Message - // formatting moved to GitTarget.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"` - - // RemovedReceiver is the old place of a GitReceiver's type and secretRef. It is never read; - // setting it is refused. - // +optional - RemovedReceiver *RemovedGitProviderReceiver `json:"receiver,omitempty"` } -// RemovedGitProviderReceiver keeps spec.receiver in the schema so that setting it is refused rather -// than pruned. Any value is kept as written until the refusal names its replacement. -// +kubebuilder:pruning:PreserveUnknownFields -type RemovedGitProviderReceiver struct{} - // 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 of the referent: ConfigMap (default) or Secret. + // Kind is "ConfigMap" or "Secret". Defaults to "ConfigMap". // +optional // +kubebuilder:validation:Enum=ConfigMap;Secret // +kubebuilder:default=ConfigMap Kind string `json:"kind,omitempty"` - // Name of the ConfigMap or Secret. + // 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"` @@ -98,46 +78,49 @@ type KnownHostsReference struct { // EncryptionSpec configures Secret encryption behavior for git writes. type EncryptionSpec struct { - // Provider selects the encryption provider. + // Provider is the encryption tool. Only "sops" is supported. Defaults to "sops". // +kubebuilder:default=sops // +kubebuilder:validation:Enum=sops Provider string `json:"provider"` - // SecretRef references namespace-local Secret data used by the encryption provider. + // 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"` - // Age configures age-specific encryption behavior for SOPS. + // Age configures age recipients for SOPS. // +optional Age *AgeEncryptionSpec `json:"age,omitempty"` } // AgeEncryptionSpec configures age recipient resolution behavior. type AgeEncryptionSpec struct { - // Enabled toggles age-based recipient resolution and bootstrap behavior. + // 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"` - // Recipients defines how recipients are resolved. + // Recipients says where the age recipients come from. // +optional Recipients AgeRecipientsSpec `json:"recipients,omitempty"` } // AgeRecipientsSpec defines age recipient source and key generation behavior. type AgeRecipientsSpec struct { - // PublicKeys is a static list of age recipients (age1...). + // PublicKeys is a fixed list of age recipients ("age1..."). // +optional // +kubebuilder:validation:items:MinLength=1 PublicKeys []string `json:"publicKeys,omitempty"` - // ExtractFromSecret derives recipients from all *.agekey entries in encryption.secretRef. + // 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 no *.agekey exists. + // 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"` @@ -149,8 +132,9 @@ type GitProviderStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions report repository validation and commit configuration readiness: - // the Ready summary plus the kstatus Reconciling/Stalled pair. + // 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 @@ -158,19 +142,16 @@ type GitProviderStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // SigningPublicKey is the operator's SSH signing public key in authorized_keys format. - // Register this as a signing key on your git platform. - // Only populated when commit.signing is configured and a signing 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"` - // LastVerifiedAt is when the credential and the repository were last proved together: the - // connectivity check listed the remote's refs with this provider's credential and the remote - // answered. It is never cleared — a failure is already carried by Ready=False with a reason — - // so the pair "not ready, last verified 11 minutes ago" says how long the connection has been - // broken, which is the sentence an operator otherwise has to infer. - // // 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. // +optional LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"` } @@ -179,44 +160,40 @@ type GitProviderStatus struct { // lives on the GitTarget (spec.commit.message), because it describes the folder rather than the // connection. type CommitSpec struct { - // Committer configures the operator identity written as the commit committer. - // When signing is enabled, Email must be a verified address on 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"` - // Signing configures commit signing. + // Signing configures SSH commit signing. // +optional Signing *CommitSigningSpec `json:"signing,omitempty"` } // CommitterSpec configures the bot identity used as the commit committer. type CommitterSpec struct { - // Name is the git committer name. + // Name is the committer name. Defaults to "GitOps Reverser". // +optional // +kubebuilder:default="GitOps Reverser" Name string `json:"name,omitempty"` - // Email is the git committer email. + // Email is the committer email. Defaults to "noreply@configbutler.ai". // +optional // +kubebuilder:default="noreply@configbutler.ai" Email string `json:"email,omitempty"` } -// CommitMessageSpec configures commit message formatting. It is set on -// GitTarget.spec.commit.message; the identically-shaped GitProvider.spec.commit.message is -// retained only to reject a manifest that still sets it there. +// CommitMessageSpec configures commit message formatting, on GitTarget.spec.commit.message. // +kubebuilder:validation:XValidation:rule="!has(self.eventTemplate)",message="eventTemplate is retired; migrate to liveTemplate" // +kubebuilder:validation:XValidation:rule="!has(self.groupTemplate)",message="groupTemplate is retired; migrate to liveTemplate" type CommitMessageSpec struct { - // LiveTemplate formats every live window, including one resource and a 0s window. - // Fields: Author (raw username, possibly empty), GitTarget, Count, Operations, Resources. - // Count is retained input entries after coalescing, before comparison with Git. - // Resources exposes Operation, Group, Version, Resource, Namespace, Name, APIVersion, - // ResourceVersion — the version of the state this commit wrote, which lags the object's - // current one and is empty for a producer that observed none — and Generation, the desired - // state's counter, which is 0 for a kind without a spec. Guard both with {{with}}. - // A literal CommitRequest message overrides this template without changing authorship. + // 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. // +optional LiveTemplate string `json:"liveTemplate,omitempty"` @@ -228,47 +205,40 @@ 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 frames a CommitRequest's message instead of committing it verbatim. It - // renders only for a commit window whose attached request supplied a message; every other - // window uses liveTemplate or reconcileTemplate. - // - // It receives the same fields as liveTemplate, plus RequestMessage carrying the request's - // message unaltered. That message is never parsed as a template, so template syntax inside it - // stays literal. - // - // Omitted, a request's message is committed exactly as supplied. A template that never renders - // RequestMessage is rejected. One that fails to render at commit time commits the message - // verbatim instead, counted as message_source="commit_request_fallback". + // 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 formats atomic snapshots and resyncs. - // Fields: Count, GitTarget, Group, Version, Resource, APIVersion, Namespace, ResourceVersion. - // ResourceVersion is the snapshot LIST's version, not any one object's. Type and Namespace - // fields are empty for whole-target snapshots, and ResourceVersion can be empty. - // Guard optional fields so the message remains meaningful for every snapshot scope. - // Revision was renamed to ResourceVersion in v0.48.0; a template still naming it is rejected. + // 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"` - // EventTemplate is retired. Remove it and migrate resource fields into liveTemplate's Resources. + // EventTemplate is retired and rejected. Move it to liveTemplate, reading resource fields from + // Resources. // +optional EventTemplate string `json:"eventTemplate,omitempty"` - // GroupTemplate is retired. Remove it and copy its value to liveTemplate. + // GroupTemplate is retired and rejected. Move its value to liveTemplate. // +optional GroupTemplate string `json:"groupTemplate,omitempty"` } // CommitSigningSpec configures commit signing. type CommitSigningSpec struct { - // SecretRef references the Secret containing the signing key material. + // 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" - // Expected keys will be defined by the signing implementation. SecretRef meta.LocalObjectReference `json:"secretRef"` - // GenerateWhenMissing causes the operator to generate signing key material - // in the referenced Secret when it is missing. + // GenerateWhenMissing creates a signing key in the Secret when it holds none. Defaults to + // false. // +optional // +kubebuilder:default=false GenerateWhenMissing bool `json:"generateWhenMissing,omitempty"` @@ -283,7 +253,9 @@ 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 the Schema for the gitproviders API. +// 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"` @@ -291,11 +263,11 @@ type GitProvider struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the desired state of GitProvider + // spec defines the desired state of GitProvider. // +required Spec GitProviderSpec `json:"spec"` - // status defines the observed state of GitProvider + // status defines the observed state of GitProvider. // +optional Status GitProviderStatus `json:"status,omitempty,omitzero"` } diff --git a/api/v1alpha3/gitreceiver_types.go b/api/v1alpha3/gitreceiver_types.go index d3fc26f1..2d697bd2 100644 --- a/api/v1alpha3/gitreceiver_types.go +++ b/api/v1alpha3/gitreceiver_types.go @@ -17,17 +17,17 @@ import ( // GitReceiverSpec defines the desired state of GitReceiver. type GitReceiverSpec struct { - // Type is the payload and signature the Git host sends. "github" checks X-Hub-Signature-256, - // "gitlab" checks X-Gitlab-Token, "gitea" checks X-Gitea-Signature, and "generic-hmac" checks - // X-Signature: sha256= over a {"repository", "branch", "after"} body, - // for a relay of your own. + // 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 a Secret in this GitReceiver's namespace whose "token" key is the webhook - // secret. The token also derives 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"` @@ -73,9 +73,9 @@ type GitReceiverStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions report whether this GitReceiver serves deliveries: the Ready summary plus the - // kstatus Reconciling/Stalled pair. Ready is True once the token Secret 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,10 +83,9 @@ 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 this GitReceiver's namespace and name, keyed by the token. Point the - // Git host's push webhook at the listener plus this path. Empty while the token Secret or its - // "token" key 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"` } @@ -108,13 +107,12 @@ 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 on a push. A delivery to it authenticates against -// its token, names a repository and a branch, and wakes the branch worker of every GitProvider, in -// any namespace, whose spec.url the delivery names, for that branch. A provider reaching its host -// through an alias (another host name or port) is not matched, and finds pushes by its periodic -// refresh. A delivery is only a hint: the branch worker reads the remote with its own provider's -// credential to find out what happened, so nothing a delivery says can choose a URL, a credential -// or a commit. GitBranch.status.lastPushNoticeAt shows that deliveries reach a branch. +// 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"` @@ -122,11 +120,11 @@ type GitReceiver struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the desired state of GitReceiver + // spec defines the desired state of GitReceiver. // +required Spec GitReceiverSpec `json:"spec"` - // status defines the observed state of GitReceiver + // status defines the observed state of GitReceiver. // +optional Status GitReceiverStatus `json:"status,omitempty,omitzero"` } diff --git a/api/v1alpha3/gittarget_types.go b/api/v1alpha3/gittarget_types.go index aff29180..5336aa91 100644 --- a/api/v1alpha3/gittarget_types.go +++ b/api/v1alpha3/gittarget_types.go @@ -21,57 +21,27 @@ import ( // // +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" -// -// spec.gitProviderRef, spec.branch and spec.parentBranch moved to a GitBranch. A pruned field would -// apply cleanly and leave the GitTarget naming no branch, so the old spellings stay in the schema -// only to be refused by name. The rule sits on spec, not on the fields, so CRD validation -// ratcheting cannot wave a stored value through on the next edit. -// +kubebuilder:validation:XValidation:rule="!has(self.gitProviderRef) && !has(self.branch) && !has(self.parentBranch)",message="spec.gitProviderRef, spec.branch and spec.parentBranch moved to a GitBranch; set spec.gitBranchRef to it" -// -// spec.clusterProviderRef was replaced by spec.kubeProviderRef, with the cluster-scoped kind it -// named. A pruned field would apply cleanly and leave a manifest written for the old API looking -// accepted, so it stays in the schema only to be refused by name, on spec for the same ratcheting -// reason as the branch fields above. -// +kubebuilder:validation:XValidation:rule="!has(self.clusterProviderRef)",message="spec.clusterProviderRef was replaced by spec.kubeProviderRef, which names a namespaced KubeProvider" // +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 target writes: the repository branch, and the credential - // it is written with. Namespace omitted means this GitTarget's own namespace. The GitBranch is - // used only while its spec.allowedGitTargets admit this GitTarget's namespace, its own included. - // Many GitTargets may reference one GitBranch; they share its branch worker. - // Immutable: delete and recreate the GitTarget to change its destination. + // 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"` - // RemovedGitProviderRef is the old place of GitBranch.spec.gitProviderRef. It is never read; - // setting it is refused. - // +optional - RemovedGitProviderRef *meta.NamespacedObjectReference `json:"gitProviderRef,omitempty"` + // 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. - // RemovedBranch is the old place of GitBranch.spec.branch. It is never read; setting it is - // refused. - // +optional - RemovedBranch string `json:"branch,omitempty"` - - // RemovedParentBranch is the old place of GitBranch.spec.parentBranch. It is never read; - // setting it is refused. - // +optional - RemovedParentBranch string `json:"parentBranch,omitempty"` - - // Path within the repository to write resources to, relative to the repository - // root. Required and must be non-empty — there is no default, so a GitTarget can - // never silently write to the repository root. To deliberately target the - // repository root, set it to "." (the ArgoCD/Flux convention); an empty string is - // rejected because it is too easy to leave blank by accident to be a deliberate - // root choice. Any leading slash (absolute path) and ".." are rejected, and a - // trailing slash is normalized away. - // Immutable: delete and recreate the GitTarget to change its destination. + // 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 defines encryption settings for Secret resource writes. + // Encryption configures how Secrets are encrypted before they are written to Git. // +optional Encryption *EncryptionSpec `json:"encryption,omitempty"` @@ -79,39 +49,30 @@ 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 applier claim, in this GitTarget's namespace, that holds the applier - // of this folder: a FluxClaim (a Flux Kustomization) or an ArgoCDClaim (an Argo CD - // Application). With it set, GitOps Reverser decides when that applier applies, always after - // publishing the edits it holds: after a commit it did not push, after a refused edit - // (spec.onRefusal: Reapply), after an apply reverted a newer API edit, after the API created an - // object in the folder (so the applier owns it and prunes it later), and once when the operator - // starts or the applier claim is first named, after the folder's initial snapshots are in Git. Objects - // another applier owns are not mirrored into the folder. The applier claim's applier must apply this target's repository and - // branch; the ApplierClaimReady condition says whether it does. Several GitTargets may name one - // applier claim, and it is asked once for all of them. + // 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"` - // OnRefusal decides what happens when a live edit is refused because it has no legal - // destination in the Git folder. A refusal produces no commit, so nothing reverts the edit - // until something applies Git over it. - // - // "Ignore", the default, records the refusal in conditions and events and does nothing else. + // Reapply is limited to edits of documents the folder already holds, in writes that remove no + // 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. // - // "Reapply" asks this target's applier (through spec.applierClaimRef, which it requires) to - // apply the current tip after publishing the edits pending on the branch, which reverts the - // refused edit and nothing that was waiting to be published. Only this - // target's applier is asked. If its GitRepository has to fetch to reach the tip, the - // Kustomizations a FluxClaim holds stay suspended and ignore the new artifact; only the objects - // no applier claim holds, listed in the applier claim's status.source.alsoAppliedBy, apply it, as - // they would for any push. + // "Ignore" (the default) reports the refusal in conditions and events and does nothing else. // - // It fires only for an edit to a document the folder ALREADY holds, in a write that removes no - // document anywhere: re-applying would otherwise prune, or put back, an object somebody else is - // deleting. A folder-level refusal (unparseable YAML, a foreign file) never fires it, because - // re-applying Git cannot repair a folder. A standing refusal is requested once, not once per - // recheck; a different refused edit, or the same edit re-made after a write of that type was - // accepted, is a new request. + // "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 @@ -139,36 +100,19 @@ type GitTargetSpec struct { // identity inside a file and the write-time guards still refuse a sensitive document in a // shared one. - // Placement declares where NEW resources are written. It has no effect on a - // resource that already has a document in Git — that document is always - // updated in place at its existing location, wherever that is. Mutable: a - // change only affects resources created after the change. - // - // Its byType and default templates share one variable language (docs/configuration.md). - // Besides the resource's identity, a template may read one of its labels: - // "{label:app.kubernetes.io/instance}/configmaps.yaml". - // - // Two variables can be absent for a resource that is otherwise placeable, and both are - // still placed rather than skipped: "{label:key}" on a resource that does not set the - // label (or sets it empty) renders the built-in "_unlabeled" bucket, and "{namespace}" on - // a cluster-scoped resource renders "_cluster". Neither is a value its kind of source can - // hold, so neither is ever confused with a real 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. // - // Append "|fallback" to either to name that bucket yourself — "{label:team|unassigned}", - // "{namespace|_global}". A fallback is at most 63 characters of [A-Za-z0-9._-] and is - // neither "." nor "..", so it can add no directory and escape no path; it may start with - // "_" to stay collision-proof, or be empty ("{label:team|}", "{namespace|}") to render - // nothing, collapsing the segment. A {namespace} fallback may name a real namespace - // ("{namespace|team-a}"), which files cluster-scoped resources in that folder; begin it with - // "_" instead when you want a bucket no namespace can reach. Only these two variables take a - // fallback, and one written on any other is rejected rather than silently ignored — not - // because the others always have a value ({groupPath} renders empty for a core resource) but - // because an empty group segment collapses by design, which is the built-in path's intent. - // - // A label is never identity, so it does not contribute to the identity-completeness a - // sensitive route requires (a {namespace} fallback does not cost it either). Because - // placement runs only for a resource with no document yet, labeling one afterwards never - // moves the file already written. + // 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"` @@ -177,87 +121,63 @@ 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 declares whether a committed document carries its own - // metadata.namespace. It governs every write this target makes, not just the first one, and it - // applies to NAMESPACED resources only — a cluster-scoped document has no namespace, so the - // field is ignored for it rather than being an error. + // 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, the namespace is INFERRED per document, which is the behavior a target that says - // nothing has always had: metadata.namespace is omitted only when the kustomization governing - // the document's path sets namespace: to that resource's own namespace, and written explicitly - // in every other case. Leave it unset for a folder that is legitimately non-uniform — a tree of - // nested kustomize roots, each supplying its own namespace — because inference resolves each - // document against the root governing its own path. + // 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: the setting for a flat folder applied directly, where nothing - // downstream supplies a namespace and a document without one is ambiguous. + // true always writes it. Use it for a flat folder applied as is. // - // false never writes it, and is a claim about the whole folder: something outside this - // repository — a Flux Kustomization's targetNamespace, an Argo Application's - // destination.namespace, or a kustomization this target maintains itself — supplies the - // namespace instead. Because a namespace-less document takes its namespace from a single - // supplier, an explicit false admits exactly ONE source namespace: a second WatchRule bringing - // another namespace to this target is refused, with GitPathAccepted=False and 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 it is read with. Namespace omitted means this GitTarget's own namespace. The - // KubeProvider is used only while its spec.allowedGitTargets admit this GitTarget's namespace, - // its own included. Immutable: a folder's source cluster is part of what the folder means; - // 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" KubeProviderRef meta.NamespacedObjectReference `json:"kubeProviderRef"` - // RemovedClusterProviderRef is the replaced spelling of kubeProviderRef. It is never read; - // setting it is refused. - // +optional - RemovedClusterProviderRef *meta.LocalObjectReference `json:"clusterProviderRef,omitempty"` - // 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 controls which deletion paths may remove documents from this target's folder: an - // explicit source DELETE event, and the resync mark-and-sweep that infers a deletion from a - // desired snapshot. Omitted, it is `mode: OnEvent` — observed deletes are mirrored, inferred - // ones are not — for a stored GitTarget as well as a new one. + // 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 target's writes are batched into commits, and how those commits - // are phrased. Omitted, writes coalesce over a 5s rolling silence window, a window closes at - // most 1m after it opened, and commits use the built-in message templates. + // 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"` // The scan keeps running while writes are suspended: freezing status.placement too would leave // a stale answer at the moment it costs most. A suspended target is Ready=True reason - // Suspended, because not writing is the configured outcome rather than ill health. - - // Suspend stops this target from writing to Git, without deleting it. It is the knob to turn - // when something is wrong and the writes have to stop now: watches keep running, events keep - // arriving, the folder keeps being scanned and status.placement keeps being maintained, and no - // new commit is planned while it is set. - // - // It takes effect at the next planning boundary, not instantly. Work already committed - // locally when suspend is set is still pushed, so a target suspended during a push cooldown - // can publish one more commit seconds later. That is deliberate: a local commit that is never - // pushed would sit in the worker's checkout indefinitely and surface later, out of order, on - // resume. Suspend is a valve on new work, not an undo — it stops the next write, it does not - // revert the last one. - // - // While it is set, status.retention is not published at all: nothing is swept, so nothing is - // measured, and reporting zero retained documents would read as "converged" when nothing was - // counted. - // - // Omitted, it is false. Clearing it resumes writing from the current cluster state on the next - // resync; the writes suppressed while suspended are not replayed. + // Suspended, because not writing is the configured outcome rather than ill health. A local + // commit is still pushed because one never pushed would sit in the checkout and surface later, + // 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. // +optional Suspend bool `json:"suspend,omitempty"` } @@ -278,16 +198,14 @@ 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 configures when this target's commit windows close. A commit window opens on an - // author's first write to this target and collects that author's writes until it closes: after - // idleTimeout of silence, or maxDuration after it opened, whichever comes first. Omitted, it is - // {idleTimeout: 5s, maxDuration: 1m}. 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 configures how this target's commit messages are formatted. Omitted, or with any - // individual template left empty, the built-in templates are used. + // Message sets the commit message templates. A template left empty uses the built-in one. // +optional Message *CommitMessageSpec `json:"message,omitempty"` } @@ -296,9 +214,8 @@ type GitTargetCommitSpec struct { // 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 after this much silence, as a Go duration string ("5s", - // "750ms"). Every write the window collects restarts it. "0s" closes the window right after - // each write, a 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))+$" @@ -306,10 +223,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 much keeps arriving, as a - // Go duration string ("1m", "90s"). It keeps continuous activity from postponing a commit - // indefinitely. "0s" closes the window 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))+$" @@ -339,53 +255,36 @@ type CommitWindow struct { // versions of the same group/resource and wants them separated must use a // ByType/Default template that includes {version}. type GitTargetPlacementSpec struct { - // ByType maps an exact resource type key ("{group}/{version}/{resource}", e.g. - // "v1/configmaps", "apps/v1/deployments", or "v1/secrets"; core resources omit - // the group) to the path template used for a new resource of that type. A path - // selected for a sensitive resource (Secrets, plus any operator-configured - // sensitive type) must be identity-complete so it cannot collide two distinct - // sensitive resources onto one 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 used for a new resource whose type has no ByType - // entry. Omitted, it falls through to the folder's one supported kustomization - // root, if it has exactly one, and then to the built-in path. - // A bundling default (one that is not identity-complete, - // such as "all.yaml") is only valid when a sensitive resource can never reach it - // — give every sensitive type an explicit identity-complete 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"` + // A created root adopts the whole folder, because a root naming one file would leave every + // other file out of every render. A second root is never created, because two roots make an + // ambiguous folder that stops accepting new documents. + // // The trap: false does NOT mean "leave kustomize alone". Registering a new file with the // kustomization already governing its directory is an invariant, not a setting, so it happens // 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 declares that this folder is a kustomize folder whose root the operator - // maintains. It controls one thing: what happens when NO kustomization governs the path a new - // document lands at. - // - // Omitted or false, the document is written and nothing else is touched. True, a - // kustomization.yaml is created at spec.path and the new document is registered in it as part - // of the same commit. - // - // A created root ADOPTS the folder: its resources: lists every managed document already there - // as well as the new one. Turning a folder into a kustomize folder means the folder, and a - // root naming one file would leave every other file in Git but out of every render. - // - // A created root carries NO namespace:. It holds an apiVersion, a kind and the resources: list - // and nothing else, so the namespace still comes from the documents (serializeNamespace unset - // or true) or from whatever installs the folder (serializeNamespace false). - // - // A folder that already has a kustomize render root never gains a second one, because two - // render roots is an ambiguous folder that stops accepting new documents altogether. If a - // byType or default template places a document outside the existing root, that placement is - // REFUSED rather than committed as a file no kustomization would render. - // - // It has NO bearing on a folder that already has a root. A new file is always registered with - // the nearest kustomization governing it, whatever chose its path, because a file no - // kustomization lists is a file kustomize never builds. + // 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"` } @@ -396,7 +295,8 @@ type GitTargetStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions represent the latest available observations of an object's state + // 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 @@ -404,30 +304,29 @@ type GitTargetStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Streams is the bounded data-plane roll-up over this GitTarget's tracked types. - // Counts, never a per-type list, so it stays bounded however many types are watched. + // Streams counts the types this GitTarget watches, by watch state. // +optional Streams *StreamsStatus `json:"streams,omitempty"` // 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 reports documents a resync kept because this target's spec.prune.mode suppressed - // the mark-and-sweep. It covers the INFERRED deletion path only: under `never`, a suppressed - // source DELETE is not counted here, so a `never` target can report zero while still declining - // to mirror deletes. It is informational either way — retention is the configured behavior, - // never a fault, and no condition changes state because of it. + // 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 what the last scan resolved about this folder's layout: whether a kustomize - // render root governs new documents, which one, and whether the folder renders a base it may - // not write to. It is an observation, like streams and retention, and the LayoutResolved - // condition carries 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"` } +// resolvedAtCommit and resolvedAt are not re-stamped on every scan: republishing an unchanged +// resolution would write status once per commit to the branch, whichever target made the commit. +// // Two rules for anything added here. A field earns its place only if a reader cannot get it from // the spec AND it varies with this folder. And nothing may depend on a placement having HAPPENED: // every field is a fact from the last scan, available before the target has written a byte. @@ -435,57 +334,46 @@ type GitTargetStatus struct { // GitTargetPlacementStatus is what the last scan resolved about a GitTarget folder's layout. type GitTargetPlacementStatus struct { - // Mode is how this folder is written, and it is the field that predicts the surprises. + // Mode is how this folder is written. // - // `Plain` — no kustomization governs the folder. A new document is written and nothing else - // is touched; a delete removes it and nothing else is touched. + // "Plain": no kustomization governs the folder. Documents are written and removed, and + // nothing else is touched. // - // `KustomizeRoot` — exactly one kustomization governs the folder and the folder is - // self-contained. A new document is also registered in that root's `resources:`; a delete - // also drops its entry; and every write is proved by re-rendering, so kustomize itself 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 additionally renders a base outside - // it (ReadOnlyBases). The base is read-only input: an edit to a field the base owns is - // authored into the overlay when it is expressible there (`images:`, `replicas:`) and refused - // with WriteBoundaryRefused otherwise, and DELETING an object the overlay inherits authors a - // `$patch: delete` into the overlay rather than removing anything. + // "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 covers more than one render root, which is LayoutResolved=Ambiguous: - // there is no single answer, and no new document is placed until the target is pointed 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 in this folder, - // relative to spec.path; "." is the folder itself. Empty for Plain (there is no root) and - // under LayoutResolved=Ambiguous (there is no single one). + // 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 never write to, - // relative to spec.path — so they lead with "../". Non-empty exactly when Mode is - // KustomizeOverlay, and it is what makes a WriteBoundaryRefused message predictable instead of - // surprising: an edit that lands on a document under one of these paths cannot be written - // where it lives. + // 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 hash this resolution was first observed at. It is not - // re-stamped on every scan: a resolution that has not changed is not republished, because - // doing so would write status once per commit to the branch, whichever target caused the - // commit. So it dates the RESOLUTION, not the last scan — a commit older than the branch - // head means the folder's layout has not changed since, not that nothing has looked. - // - // It is empty when the branch had no commit at the time (a folder nothing has written to yet) - // and is filled in by the first scan that finds one. + // 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 resolution was computed. Like ResolvedAtCommit it dates the - // resolution rather than the last scan, so a timestamp well in the past means the folder's - // shape has been stable, not that scanning stopped. + // 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"` } @@ -510,22 +398,18 @@ const ( // GitTargetRetentionStatus is a bounded roll-up of what this GitTarget's prune policy kept. type GitTargetRetentionStatus struct { - // Mode is the EFFECTIVE spec.prune.mode this roll-up was produced under. It is reported here - // rather than left to be read from the spec because a GitTarget that predates spec.prune has - // no stored value at all, so the spec alone cannot explain why documents are being kept. + // 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 managed documents the policy kept that a converged mirror - // would not hold. Zero means a resync ran and found nothing to retain — the mirror is - // converged. An ABSENT retention block means something different: 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 records when the reported retention count or effective prune mode last - // changed. It does NOT indicate when retention was last evaluated: a resync that re-reports the - // same count leaves it untouched, so an old timestamp is equally consistent with stable - // retention and with nothing having measured it since. + // 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"` } @@ -551,7 +435,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 is the Schema for the gittargets API. +// 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"` @@ -559,11 +445,11 @@ type GitTarget struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the desired state of GitTarget + // spec defines the desired state of GitTarget. // +required Spec GitTargetSpec `json:"spec"` - // status defines the observed state of GitTarget + // status defines the observed state of GitTarget. // +optional Status GitTargetStatus `json:"status,omitempty,omitzero"` } diff --git a/api/v1alpha3/kubeprovider_types.go b/api/v1alpha3/kubeprovider_types.go index 7dcff0de..bc50a258 100644 --- a/api/v1alpha3/kubeprovider_types.go +++ b/api/v1alpha3/kubeprovider_types.go @@ -27,57 +27,56 @@ import ( // 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 a Secret in this KubeProvider's namespace holding a kubeconfig for the source - // cluster. When key is empty the kubeconfig is read from "value", then "value.yaml" (Flux's - // order). Unsafe kubeconfigs (exec auth, insecure-skip-tls-verify, credentials named by file - // path) are refused with Validated=False unless the operator opts in through its flags. - // Exactly one of secretRef and inCluster is set. Immutable. + // 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 provider 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 with reason InClusterNotAuthorized and is never used. Exactly one - // of secretRef and inCluster is set. Immutable. + // 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 provider, and what: each grant - // admits some control-cluster namespaces and bounds what the WatchRules feeding a GitTarget there - // may watch (sourceNamespaces), and what a ClusterWatchRule may mirror through it - // (clusterResources). Omitted, it defaults to one grant for this KubeProvider's own namespace, - // mirroring the namespace of the same name (consumerNamespaces: {from: Same}, sourceNamespaces: - // ["{consumer.namespace}"]); it grants no cluster-scoped resource. A list you write is the whole - // policy: nothing is added to it, and [] admits no GitTarget. 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 applier claims (FluxClaims and ArgoCDClaims) may hold an applier - // through this provider, and in which namespaces that applier may live (applierNamespaces). - // Admission to mirror does not admit to apply. Omitted, it defaults to one grant for this - // KubeProvider's own namespace, holding appliers in the namespace of the same name - // (consumerNamespaces: {from: Same}, applierNamespaces: ["{consumer.namespace}"]). A list you - // write is the whole policy: nothing is added to it, and [] admits no applier claim. 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 overrides the operator's outgoing kube-client throttles for this cluster. Omitted, - // the built-in defaults apply (20 QPS, burst 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"` - // Attribution groups this cluster's author-attribution settings. The block is spelled - // "attribution" rather than "authorAttribution" even though the operator flags are - // --author-attribution-*: the prefix groups a flat flag namespace, and a block on a + // The block is spelled "attribution" rather than "authorAttribution" even though the operator + // 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. // +optional Attribution *KubeProviderAttribution `json:"attribution,omitempty"` } @@ -88,14 +87,12 @@ type KubeProviderInCluster struct{} // KubeProviderClient holds the per-cluster kube-client throttles. type KubeProviderClient struct { - // QPS overrides the operator's outgoing kube-client query-per-second throttle for this - // cluster's watches and discovery. Omitted, the built-in default of 20 applies. + // 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 overrides the operator's outgoing kube-client burst for this cluster. Omitted, the - // built-in default of 30 applies. + // 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"` @@ -104,21 +101,17 @@ type KubeProviderClient struct { // 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 sender is the API server's - // webhook backend (https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/#webhook-backend), - // and this is the segment its configured URL ends in: /audit-webhook/. When - // several logical clusters share one backend it is instead the value of the audit-event - // annotation named by --author-attribution-audit-route-annotation-key. - // - // It partitions the attribution facts, so two KubeProviders carrying the same route read one - // cluster's facts, and two carrying different routes can never cross-credit an author. + // 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 ., which no provider in another namespace can resolve to. Set it - // when several KubeProviders in this namespace name one cluster, since an API server has one - // webhook backend and so posts under one route. A route is held by one namespace: providers in - // another namespace that carry it report Stalled=True with reason AuditRouteConflict and are not - // used. The namespace a route starts with (.) holds it when a provider there carries - // it; otherwise the namespace of the oldest provider carrying it does. + // 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])?)*$` @@ -139,26 +132,23 @@ 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 this provider's attribution facts are partitioned under, as resolved: - // spec.attribution.auditRoute, or . when that is empty. It is the route the - // AuditFactsReceived condition describes, so when it changes that condition starts over — - // proof that one route delivered says nothing about another. + // 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 report the provider's readiness: Validated (the credential is safe and - // resolvable, asserted without a network dial) plus the aggregated Ready and the kstatus - // Reconciling/Stalled pair. Stalled=True with reason InClusterNotAuthorized or - // AuditRouteConflict means the provider is not used at all. + // 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 reports whether an attribution fact has ever been accepted and recorded on - // this provider's audit route. It is True (Received) from the first one on and never goes back: - // later silence is a quiet cluster, not a fault. Until then it is Unknown, with the reason - // naming which silence it is and therefore where to look: TransportUnavailable (the fact - // transport is refusing writes, so no route can be judged), NoAuditDelivery (nothing is posting - // audit to this operator at all), or RouteUnused (audit is arriving and has never carried this - // route). It is absent when the operator runs with author attribution disabled. It is not part - // of Ready. + // 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 @@ -177,11 +167,10 @@ 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 GitTarget -// mirrors from it; an applier claim reaches its applier through it. It holds the credential that -// reaches it (a kubeconfig Secret in its own namespace, or the operator's own identity where the -// installation allows it), who may use it and for what (spec.allowedGitTargets and -// spec.allowedApplierClaims), and the audit route its attribution facts are partitioned under. +// 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 eee26903..05dbf06c 100644 --- a/api/v1alpha3/prune_policy.go +++ b/api/v1alpha3/prune_policy.go @@ -32,9 +32,9 @@ 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 selects which deletion paths are enabled. `Never` removes nothing; `OnEvent` mirrors an - // observed source DELETE but never infers a deletion from a resync snapshot; `Always` enables - // both, restoring full desired-state convergence. Omitted, it is `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 99cd59ba..b6f81c26 100644 --- a/api/v1alpha3/shared_types.go +++ b/api/v1alpha3/shared_types.go @@ -5,27 +5,27 @@ 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. type StreamsStatus struct { - // Summary is the display-only ready/total ratio, e.g. "3/4". - // // 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. - // Do not compute anything from it — read ready and total. + + // Summary is ready/total for display, such as "3/4". Read ready and total instead of parsing + // it. // +optional Summary string `json:"summary,omitempty"` - // Total is how many types are tracked. + // Total is how many types are watched. Total int32 `json:"total"` - // Ready is how many tracked types are Streaming. + // Ready is how many of them are streaming events. Ready int32 `json:"ready"` - // Replaying is how many tracked types are still replaying their initial events. + // Replaying is how many of them are still receiving their initial list. Replaying int32 `json:"replaying"` - // Blocked is how many tracked types cannot currently be watched. + // Blocked is how many of them cannot be watched right now. Blocked int32 `json:"blocked"` - // PendingSample is a bounded sample of types not yet ready. + // PendingSample names up to five types that are not ready yet. // +optional // +kubebuilder:validation:MaxItems=5 PendingSample []string `json:"pendingSample,omitempty"` diff --git a/api/v1alpha3/watchrule_types.go b/api/v1alpha3/watchrule_types.go index 0dc03d0d..ef55b4a8 100644 --- a/api/v1alpha3/watchrule_types.go +++ b/api/v1alpha3/watchrule_types.go @@ -10,92 +10,60 @@ import ( ) // WatchRuleSpec defines the desired state of WatchRule. -// WatchRule selects NAMESPACED resources on its GitTarget's source cluster. Each rules[] item -// carries its own source namespace: omitted for this WatchRule's own namespace, an explicit name, -// or "*" for every namespace the source credential can read. Each is watched only while the -// KubeProvider's spec.allowedGitTargets give the GitTarget's namespace that source namespace. type WatchRuleSpec struct { - // GitTargetRef names the GitTarget this rule feeds, in this WatchRule's own namespace. + // 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 define which resources to watch, and in which source namespaces. - // Multiple rules create a logical OR - a resource matching ANY rule is watched. - // Each rule can specify API groups, versions, resource types, and a source namespace. - // A selected resource collection is observed through its whole lifecycle; the GitTarget's - // spec.prune.mode decides whether an observed removal 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 defines a set of namespaced resources to watch. -// Omitted API groups and versions are resolved from the served Kubernetes API surface. -// All fields except Resources are optional. +// ResourceRule selects namespaced resources by type, and optionally by API group, version, source +// namespace and label. type ResourceRule struct { - // APIGroups to match. Empty string ("") matches the core API group. - // If omitted, GitOps Reverser resolves the resource name across all served API groups. - // Wildcards supported: "*" matches all groups. - // Examples: - // - [""] matches core API (pods, services, configmaps) - // - ["apps"] matches apps API group (deployments, statefulsets) - // - ["", "apps"] matches both core and apps groups - // - ["*"] matches all groups - // - [] resolves a named resource only when it is served by one API group + // 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 to match. If empty, uses the preferred served version for each group/resource. - // Wildcards supported: "*" matches all versions. - // Examples: - // - ["v1"] matches only v1 version - // - ["v1", "v1beta1"] matches both versions - // - ["*"] matches all served versions - // - [] matches the preferred served version - // - // Multi-version note: the built-in cold-start Git path is versionless, so two - // objects that differ only by API version resolve to the same file. To watch - // several versions of a group/resource and keep them in separate files, give the - // GitTarget a placement template that includes {version} (see GitTargetPlacementSpec). + // 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 to match (plural names like "pods", "configmaps"). - // This field is required and determines which resource types trigger this rule. - // Wildcard semantics follow Kubernetes admission webhook patterns: - // - "*" matches all resources - // - "pods" matches exactly pods (case-insensitive) - // - // For custom resources, use the exact plural resource name and set apiGroups - // when more than one served API group exposes that name. - // - // Note: Subresources cannot be added here. Values containing "/" (for example - // "pods/log" or "pods/*") are rejected by the API because subresources are - // not supported for list/watch snapshot planning. Prefix/suffix wildcards - // like "pod*" or "*.example.com" are NOT supported. Use exact matches or the - // "*" wildcard for broad matching. + // 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 // +kubebuilder:validation:items:MinLength=1 // +kubebuilder:validation:items:Pattern=`^[^/]*$` Resources []string `json:"resources"` - // ObjectSelector selects which objects of the matched types are mirrored, by label. The API - // server applies it to every LIST and WATCH this item opens, with list/watch semantics: an - // object that stops matching leaves the collection exactly as a deleted one does, and the - // GitTarget's spec.prune.mode decides what that does to its Git document. - // It is a standard Kubernetes label selector: matchLabels and matchExpressions, ANDed. - // Omitted or {} selects every object. A selector that does not parse refuses the whole rule + // 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). - // - // A selected mirror owns every document of its type in its namespace scope, including documents - // the selector never matched: under prune.mode Always, a complete snapshot removes them. - // Two rules feeding one GitTarget whose collections overlap (same type, and the same namespace or - // one of them all namespaces) must use the same selector; the newer rule is refused. // +optional ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` + // "*" is one cluster-wide list and watch rather than a set of per-namespace ones, so its bound + // is the credential's own RBAC and nothing else. + // // A denied explicit name refuses the WHOLE WatchRule rather than trimming that item: mirroring // two of the three namespaces a rule asked for is worse than a loud failure. // @@ -104,22 +72,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 namespace this item watches IN THE SOURCE CLUSTER its GitTarget - // mirrors from: omitted for this WatchRule's own namespace, an exact name for one other, or - // "*" for every namespace the source credential can read. - // - // "*" is one cluster-wide list and watch rather than a set of per-namespace ones, so its bound - // is the credential's own RBAC and nothing else. - // - // Every value, this rule's own namespace included, must be in the union of sourceNamespaces from - // all grants in the GitTarget's KubeProvider spec.allowedGitTargets that admit the GitTarget - // ("{consumer.namespace}" there is the target's own namespace). "*" needs "*" in at least one of - // them; a union without it refuses the rule, it does not narrow the watch. One item outside the - // union refuses the whole WatchRule (SourceNamespaceNotAllowed). The check runs when the rule - // reconciles, never at admission. + // 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. // - // This changes only which namespace is WATCHED, never where objects are written: Git placement - // follows each mirrored object's own namespace. + // 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])?)$` @@ -171,7 +132,9 @@ type WatchRuleStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Conditions represent the latest available observations of an object's state + // 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 @@ -179,7 +142,7 @@ type WatchRuleStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Streams is the bounded stream-readiness roll-up for the types this rule resolves. + // Streams counts the types this WatchRule resolves, by watch state. // +optional Streams *StreamsStatus `json:"streams,omitempty"` } @@ -200,14 +163,9 @@ 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 on the source cluster its GitTarget mirrors from, with -// filtering by API group, version, source namespace, and object labels. Scope is carried by the rule -// KIND: a WatchRule never selects cluster-scoped types — use a ClusterWatchRule for those. -// -// Each spec.rules[] item watches its own source namespace: this WatchRule's OWN namespace when -// omitted, an explicit name, or "*" for every namespace the GitTarget admits. Anything other than -// its own namespace passes the gate described on rules[].sourceNamespace. RBAC controls who may -// create or modify WatchRules per namespace. +// 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"` @@ -215,11 +173,11 @@ type WatchRule struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` - // spec defines the desired state of WatchRule + // spec defines the desired state of WatchRule. // +required Spec WatchRuleSpec `json:"spec"` - // status defines the observed state of WatchRule + // status defines the observed state of WatchRule. // +optional Status WatchRuleStatus `json:"status,omitempty"` } diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index 057072f6..62900075 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -88,7 +88,7 @@ func (in *ArgoCDClaim) DeepCopyInto(out *ArgoCDClaim) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } @@ -161,11 +161,6 @@ func (in *ArgoCDClaimSource) DeepCopy() *ArgoCDClaimSource { func (in *ArgoCDClaimSpec) DeepCopyInto(out *ArgoCDClaimSpec) { *out = *in out.KubeProviderRef = in.KubeProviderRef - if in.RemovedClusterProviderRef != nil { - in, out := &in.RemovedClusterProviderRef, &out.RemovedClusterProviderRef - *out = new(meta.LocalObjectReference) - **out = **in - } out.ApplicationRef = in.ApplicationRef } @@ -634,7 +629,7 @@ func (in *FluxClaim) DeepCopyInto(out *FluxClaim) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } @@ -713,11 +708,6 @@ func (in *FluxClaimSource) DeepCopy() *FluxClaimSource { func (in *FluxClaimSpec) DeepCopyInto(out *FluxClaimSpec) { *out = *in out.KubeProviderRef = in.KubeProviderRef - if in.RemovedClusterProviderRef != nil { - in, out := &in.RemovedClusterProviderRef, &out.RemovedClusterProviderRef - *out = new(meta.LocalObjectReference) - **out = **in - } out.KustomizationRef = in.KustomizationRef } @@ -1036,11 +1026,6 @@ func (in *GitProviderSpec) DeepCopyInto(out *GitProviderSpec) { *out = new(CommitSpec) (*in).DeepCopyInto(*out) } - if in.RemovedReceiver != nil { - in, out := &in.RemovedReceiver, &out.RemovedReceiver - *out = new(RemovedGitProviderReceiver) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitProviderSpec. @@ -1329,11 +1314,6 @@ func (in *GitTargetRetentionStatus) DeepCopy() *GitTargetRetentionStatus { func (in *GitTargetSpec) DeepCopyInto(out *GitTargetSpec) { *out = *in out.GitBranchRef = in.GitBranchRef - if in.RemovedGitProviderRef != nil { - in, out := &in.RemovedGitProviderRef, &out.RemovedGitProviderRef - *out = new(meta.NamespacedObjectReference) - **out = **in - } if in.Encryption != nil { in, out := &in.Encryption, &out.Encryption *out = new(EncryptionSpec) @@ -1355,11 +1335,6 @@ func (in *GitTargetSpec) DeepCopyInto(out *GitTargetSpec) { **out = **in } out.KubeProviderRef = in.KubeProviderRef - if in.RemovedClusterProviderRef != nil { - in, out := &in.RemovedClusterProviderRef, &out.RemovedClusterProviderRef - *out = new(meta.LocalObjectReference) - **out = **in - } if in.Prune != nil { in, out := &in.Prune, &out.Prune *out = new(PrunePolicy) @@ -1681,21 +1656,6 @@ func (in *PrunePolicy) DeepCopy() *PrunePolicy { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RemovedGitProviderReceiver) DeepCopyInto(out *RemovedGitProviderReceiver) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemovedGitProviderReceiver. -func (in *RemovedGitProviderReceiver) DeepCopy() *RemovedGitProviderReceiver { - if in == nil { - return nil - } - out := new(RemovedGitProviderReceiver) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceRule) DeepCopyInto(out *ResourceRule) { *out = *in diff --git a/config/crd/bases/configbutler.ai_argocdclaims.yaml b/config/crd/bases/configbutler.ai_argocdclaims.yaml index ffbc38ce..07bc43d6 100644 --- a/config/crd/bases/configbutler.ai_argocdclaims.yaml +++ b/config/crd/bases/configbutler.ai_argocdclaims.yaml @@ -57,10 +57,11 @@ spec: schema: openAPIV3Schema: description: |- - ArgoCDClaim holds an Argo CD Application's automated sync off for GitOps Reverser, which starts - every sync it runs, each after publishing the edits pending on the branch: after a commit - GitOps Reverser did not push, after a refused edit, after a reverted edit, and to adopt what the - API created or converge the Application (CatchUp). 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: |- @@ -80,20 +81,22 @@ spec: metadata: type: object spec: - description: spec defines the Application the applier claim holds + description: spec defines the desired state of ArgoCDClaim. properties: applicationRef: description: |- - ApplicationRef is the Argo CD Application that applies the folders of the GitTargets naming - this applier claim. It must have a single spec.source whose targetRevision names a branch. The - applier claim holds its automated sync off: it applies spec.syncPolicy.automated.enabled: false, - which keeps automated.prune for the syncs GitOps Reverser starts, switches it off again when - somebody switches it on, and gives it up when it lets go. An Application an ApplicationSet - generated, or whose switch another field manager applies, must have automated sync off - already: the switch is theirs, and the applier claim only checks it. GitOps Reverser starts each - sync by setting the Application's operation, as Argo CD's automated sync does, pinned to the - commit it checked, and never replaces an operation that is running. Its namespace defaults to - the applier claim's namespace. Immutable. + 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. + + An omitted namespace means this ArgoCDClaim's namespace. Immutable: delete and recreate the + ArgoCDClaim to change it. properties: name: description: Name of the referent. @@ -111,24 +114,13 @@ spec: rule: self == oldSelf - message: spec.applicationRef.name must not be empty rule: self.name != '' - clusterProviderRef: - description: |- - RemovedClusterProviderRef is the replaced spelling of kubeProviderRef. It is never read; - setting it is refused. - properties: - name: - description: Name of the referent. - type: string - required: - - name - type: object kubeProviderRef: description: |- - KubeProviderRef names the KubeProvider whose cluster holds the Application, such as a - management cluster running Argo CD for the source cluster. Namespace omitted means this - applier claim's own namespace. The provider's spec.allowedApplierClaims must admit this applier - claim's namespace, with the Application's namespace in applierNamespaces. Its identity needs - get, update and patch on the Application. Immutable. + 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. @@ -150,30 +142,25 @@ spec: - applicationRef - kubeProviderRef type: object - x-kubernetes-validations: - - message: spec.clusterProviderRef was replaced by spec.kubeProviderRef, - which names a namespaced KubeProvider - rule: '!has(self.clusterProviderRef)' status: - description: status defines the observed state of ArgoCDClaim + description: status defines the observed state of ArgoCDClaim. properties: conditions: description: |- - Conditions are Configured (an eligible GitTarget names the applier claim; the Application exists, - syncs a single source, has automated sync off or a switch the applier claim may hold, may be - written, and no other ArgoCDClaim holds it), InControl (whether GitOps Reverser decides, right - now, when the Application syncs. True: AutomatedSyncOff, automated sync is off, held by GitOps - Reverser or kept off by the switch's owner. False: Releasing, being handed back; Revoking, being - handed back because a grant no longer admits it; NotReferenced. NotConfigured: the applier claim - is not Configured, or automated sync was switched on since it was), and the kstatus - Ready/Reconciling/Stalled trio. An applier claim acts only while an - eligible GitTarget references it: one in its namespace that names it, is admitted by its - KubeProviders, and writes the repository and branch the Application syncs. An applier claim no - GitTarget names reads nothing in the Application's cluster and is Configured=Unknown, - Ready=True, both NotReferenced; one named only by GitTargets that are not eligible is - Configured NotBound or a refusal. Of the ArgoCDClaims in line for one Application in one - cluster, one holds it: the one holding it already, then the one created first. The others are - Configured=False (HeldByAnotherClaim) until it lets go, and the next in line takes it over. + 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. + + 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. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -244,7 +231,7 @@ spec: properties: appliedRevision: description: |- - AppliedRevision is the revision the applier reports it applied once the operation completed. + 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: @@ -262,9 +249,10 @@ spec: type: string phase: description: |- - Phase is where the operation stands. Requested, Running, Settling and Unresolved are in - flight: while one is, an edit to an object the applier applies can still be reverted by it, - and the applier is asked nothing else. Succeeded and Failed have completed. + 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 @@ -275,8 +263,11 @@ spec: type: string reason: description: |- - Reason is why the apply was asked for. With several GitTargets on the applier 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 @@ -289,8 +280,8 @@ spec: type: string suspendToken: description: |- - SuspendToken is the reconcile.fluxcd.io/requestedAt value the suspend after the apply carried. - Once the Kustomization reports it handled, nothing started before the suspend is still + 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: @@ -304,26 +295,25 @@ spec: type: object previousSuspendState: description: |- - PreviousSuspendState is how the Application stood before the applier claim first switched its - automated sync off, which handing it back restores: NotSuspended (automated sync was on) or - Suspended (it was off). Recorded with the first switch-off, and cleared once the Application is - handed back. Absent, a hand-back gives the switch up and leaves what the Application's own spec - says. Either way, a switch that an ApplicationSet or another field manager owns by then keeps - the value that owner set. + 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 - NotSuspended type: string repairPending: description: |- - RepairPending is true while a Repair sync is pending for the Application: a sync reverted an - API edit made after its flush, so the cluster runs an older value than Git 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, published while an eligible GitTarget - references the applier claim. A GitTarget naming this applier claim must write the repository it syncs, on + 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: @@ -331,8 +321,8 @@ spec: type: string targetRevision: description: |- - TargetRevision is the Application's spec.source.targetRevision, as written: a branch name, or - refs/heads/ and the name. Empty is 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 5d8753b1..c2e690c9 100644 --- a/config/crd/bases/configbutler.ai_clusterwatchrules.yaml +++ b/config/crd/bases/configbutler.ai_clusterwatchrules.yaml @@ -46,14 +46,9 @@ spec: schema: openAPIV3Schema: description: |- - ClusterWatchRule selects CLUSTER-SCOPED resources on the source cluster its GitTarget mirrors - from — Nodes, PersistentVolumes, StorageClasses, ClusterRoles, CRDs, and the like. Scope is - carried by the rule KIND, so it has no per-rule scope choice and no source-namespace selection. - - It is cluster-scoped and requires cluster-admin permissions. Its gitTargetRef names a GitTarget - (namespace required), whose namespace must be admitted by that target's KubeProvider. To - mirror NAMESPACED resources use a WatchRule in the tenant namespace and set - spec.rules[].sourceNamespace, whose "*" reaches every namespace the source credential can read. + 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: |- @@ -77,8 +72,8 @@ spec: properties: gitTargetRef: description: |- - GitTargetRef names the GitTarget this rule feeds. A ClusterWatchRule has no namespace of its - own, so the namespace is required here rather than defaulted. + 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. @@ -98,53 +93,38 @@ spec: rule: has(self.namespace) && self.namespace != '' rules: description: |- - Rules define which CLUSTER-SCOPED resources to watch. - Multiple rules create a logical OR - a resource matching ANY rule is watched. - A rule that resolves to no cluster-scoped type simply watches nothing; use a WatchRule with - spec.rules[].sourceNamespace 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 defines which CLUSTER-SCOPED resources to watch. It deliberately has 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. properties: apiGroups: description: |- - APIGroups to match. Empty string ("") matches the core API group. - If omitted, GitOps Reverser resolves the resource name across all served API groups. - Wildcards supported: "*" matches all groups. - Examples: - - [""] matches core API (nodes, namespaces) - - ["rbac.authorization.k8s.io"] matches RBAC resources - - ["*"] matches all groups - - [] resolves a named resource only when it is served by one API group + 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 to match. If empty, uses the preferred served version for each group/resource. - Wildcards supported: "*" matches all versions. - Examples: - - ["v1"] matches only v1 version - - ["*"] matches all served versions - - [] matches the preferred served version + APIVersions are the versions to match, or "*" for every served version. Omitted, each + resource's preferred version is used. items: type: string type: array objectSelector: description: |- - ObjectSelector selects which objects of the matched types are mirrored, by label. The API - server applies it to every LIST and WATCH this item opens, with list/watch semantics: an - object that stops matching leaves the collection exactly as a deleted one does, and the - GitTarget's spec.prune.mode decides what that does to its Git document. - It is a standard Kubernetes label selector: matchLabels and matchExpressions, ANDed. - Omitted or {} selects every object. A selector that does not parse refuses the whole rule - (ResourcesResolved=False, reason InvalidObjectSelector). - - A selected mirror owns every document of its type, including documents the selector never - matched: under prune.mode Always, a complete snapshot removes them. - Two rules feeding one GitTarget that match the same type must use the same selector; the - newer rule is refused. + 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). properties: matchExpressions: description: matchExpressions is a list of label selector @@ -191,18 +171,9 @@ spec: x-kubernetes-map-type: atomic resources: description: |- - Resources to match (plural names like "nodes", "clusterroles"). - This field is required and determines which resource types trigger this rule. - Wildcard semantics follow Kubernetes admission webhook patterns: - - "*" matches all resources - - "nodes" matches exactly nodes - - "pods" matches exactly pods (for namespaced scope) - - Note: Subresources cannot be added here. Values containing "/" (for example - "pods/log" or "pods/*") are rejected by the API because subresources are - not supported for list/watch snapshot planning. Prefix/suffix wildcards - like "pod*" or "*.example.com" are NOT supported. Use exact matches or the - "*" wildcard for broad matching. + 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: ^[^/]*$ @@ -222,8 +193,10 @@ spec: description: status defines the observed state of ClusterWatchRule. properties: conditions: - description: Conditions represent the latest available observations - of the ClusterWatchRule's state. + 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. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -288,40 +261,37 @@ spec: format: int64 type: integer streams: - description: Streams is the bounded stream-readiness roll-up for the - types this rule resolves. + description: Streams counts the types this ClusterWatchRule resolves, + by watch state. properties: blocked: - description: Blocked is how many tracked types cannot currently - be watched. + description: Blocked is how many of them cannot be watched right + now. format: int32 type: integer pendingSample: - description: PendingSample is a bounded sample of types not yet - ready. + description: PendingSample names up to five types that are not + ready yet. items: type: string maxItems: 5 type: array ready: - description: Ready is how many tracked types are Streaming. + description: Ready is how many of them are streaming events. format: int32 type: integer replaying: - description: Replaying is how many tracked types are still replaying - their initial events. + description: Replaying is how many of them are still receiving + their initial list. format: int32 type: integer summary: description: |- - Summary is the display-only ready/total ratio, e.g. "3/4". - - 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. - Do not compute anything from it — read ready and total. + 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 tracked. + description: Total is how many types are watched. format: int32 type: integer required: diff --git a/config/crd/bases/configbutler.ai_commitrequests.yaml b/config/crd/bases/configbutler.ai_commitrequests.yaml index b8fddc5b..9cb8ea53 100644 --- a/config/crd/bases/configbutler.ai_commitrequests.yaml +++ b/config/crd/bases/configbutler.ai_commitrequests.yaml @@ -50,10 +50,9 @@ spec: schema: openAPIV3Schema: description: |- - CommitRequest is a one-shot "save" signal. It attaches to its author's current or next - commit window on the referenced GitTarget, closes that window on its own timers instead of the - target's, and gives the resulting commit its message. The commit hash is reported in status - once the commit 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: |- @@ -73,12 +72,13 @@ spec: metadata: type: object spec: - description: spec defines the desired state of CommitRequest + description: spec defines the desired state of CommitRequest. It is immutable + after creation. properties: gitTargetRef: description: |- - GitTargetRef names the GitTarget whose commit window this request attaches to. - The GitTarget must be in the same namespace as this CommitRequest. + GitTargetRef names the GitTarget, in this namespace, whose commit window this request + attaches to. properties: name: description: Name of the referent. @@ -91,13 +91,10 @@ spec: rule: self.name != '' message: description: |- - Message is an optional commit message for the finalized commit. When - omitted, the target's liveTemplate is used. Template-like text remains literal. - - When present it is limited to 1-1024 Unicode characters and used - verbatim as the commit message. Newlines are allowed so a subject and - body can be supplied; all other ASCII control characters (including tab - and carriage return) are rejected. Whitespace-only messages are rejected. + 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. maxLength: 1024 minLength: 1 pattern: ^[^\x00-\x09\x0B-\x1F\x7F]*$ @@ -108,13 +105,12 @@ spec: whenNothingToCommit: default: Resolve description: |- - WhenNothingToCommit decides what the request does when it ends with nothing to commit: - because attachTimeout ran out with no eligible window, or because the window it attached to - closed, for any reason, without changing Git. Resolve finishes without a commit; CommitEmpty - records spec.message in an empty commit, and the Ready reason still says which of the two - happened. A window that belonged to another author never falls back to an empty commit. A - request on a GitTarget that may not be written (suspended, or render fidelity not established) - fails with that reason under either value. 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 @@ -126,17 +122,17 @@ spec: 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 attached. They replace the GitTarget's timers - for that window, shorter or longer. Omitted, it is the defaults of every field below: attach - to the author's current or next window within 2s, then close it 2s later. + 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 window if one is open - when the worker registers the request, and otherwise to the next one; Next first closes the - author's open window, under its own message, and attaches to the next one. 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 @@ -144,10 +140,10 @@ spec: attachTimeout: default: 2s description: |- - AttachTimeout is how long the request waits for a window to attach to, counted from when - the branch worker registers it, as a Go duration string. "0s" attaches to a window already - open, or gives up at once. 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 fact. + 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: @@ -155,9 +151,8 @@ spec: rule: duration(self) <= duration('5m') idleTimeout: description: |- - IdleTimeout closes the attached window after this much silence; every write it collects - restarts it. Omitted, the window has no idle close and maxDuration alone ends it. At most - "5m". + 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: @@ -166,8 +161,9 @@ spec: maxDuration: default: 2s description: |- - MaxDuration closes the attached window this long after the request attached, however much - keeps arriving. "0s" finalizes right after the attach. 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: @@ -200,22 +196,23 @@ spec: rule: '!has(self.whenNothingToCommit) || self.whenNothingToCommit != ''CommitEmpty'' || has(self.message)' status: - description: status defines the observed state of CommitRequest + description: status defines the observed state of CommitRequest. properties: branch: - description: |- - Branch is the Git branch the GitTarget commits to. Populated once the - finalize resolves. + description: Branch is the branch the commit is made on. Set once + the request ends. type: string commit: - description: Commit is the resulting commit hash. Set when the commit - was pushed (Pushed=True). + description: Commit is the commit hash. Set once the commit is pushed + (Pushed=True). type: string conditions: description: |- - Conditions report the request's progress and terminal outcome: the Ready - summary, the kstatus Reconciling/Stalled pair, and the domain conditions - AuthorAttributed and Pushed. + 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. @@ -275,7 +272,7 @@ spec: - type x-kubernetes-list-type: map observedGeneration: - description: ObservedGeneration is the most recent generation observed + description: ObservedGeneration is the latest generation observed by the controller. format: int64 type: integer diff --git a/config/crd/bases/configbutler.ai_fluxclaims.yaml b/config/crd/bases/configbutler.ai_fluxclaims.yaml index caf0015f..113a3e4d 100644 --- a/config/crd/bases/configbutler.ai_fluxclaims.yaml +++ b/config/crd/bases/configbutler.ai_fluxclaims.yaml @@ -57,10 +57,11 @@ spec: schema: openAPIV3Schema: description: |- - FluxClaim holds a Flux Kustomization for GitOps Reverser, which starts every apply it makes, - each after publishing the edits pending on the branch: after a commit GitOps Reverser did not - push, after a refused edit, after a reverted edit, and to adopt what the API created or converge - the Kustomization (CatchUp). 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: |- @@ -80,26 +81,15 @@ spec: metadata: type: object spec: - description: spec defines the Kustomization the applier claim holds + description: spec defines the desired state of FluxClaim. properties: - clusterProviderRef: - description: |- - RemovedClusterProviderRef is the replaced spelling of kubeProviderRef. It is never read; - setting it is refused. - properties: - name: - description: Name of the referent. - type: string - required: - - name - type: object kubeProviderRef: description: |- - KubeProviderRef names the KubeProvider whose cluster holds the Kustomization, such as a - management cluster running Flux for the source cluster. Namespace omitted means this - applier claim's own namespace. The provider's spec.allowedApplierClaims must admit this applier - claim's namespace, with the Kustomization's namespace in applierNamespaces. Its identity needs - get and patch on the Kustomization and its GitRepository. Immutable. + 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. @@ -119,9 +109,10 @@ spec: rule: self == oldSelf kustomizationRef: description: |- - KustomizationRef is the Flux Kustomization that applies the folders of the GitTargets naming - this applier claim. Its spec.sourceRef must be a GitRepository that follows a branch. Its namespace - defaults to the applier claim's namespace. Immutable. + 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. @@ -143,30 +134,25 @@ spec: - kubeProviderRef - kustomizationRef type: object - x-kubernetes-validations: - - message: spec.clusterProviderRef was replaced by spec.kubeProviderRef, - which names a namespaced KubeProvider - rule: '!has(self.clusterProviderRef)' status: - description: status defines the observed state of FluxClaim + description: status defines the observed state of FluxClaim. properties: conditions: description: |- - Conditions are Configured (an eligible GitTarget names the applier claim; the Kustomization exists, - applies a GitRepository that follows a branch, may be patched, and no other FluxClaim holds - it), InControl (whether GitOps Reverser decides, right now, when the Kustomization applies. True: - Suspended, suspended under GitOps Reverser with the last suspend handled; Applying, resumed for - an apply GitOps Reverser started. False: Suspending, a suspend is not handled yet; Releasing, - being handed back; Revoking, being handed back because a grant no longer admits it; - NotReferenced. Unknown: NotConfigured), and the kstatus - Ready/Reconciling/Stalled trio. An applier claim acts only while an eligible GitTarget references it: - one in its namespace that names it, is admitted by its KubeProviders, and writes the - repository and branch the GitRepository fetches. An applier claim no GitTarget names reads nothing - in the Kustomization's cluster and is Configured=Unknown, Ready=True, both NotReferenced; one - named only by GitTargets that are not eligible is Configured NotBound or a refusal. Of the - FluxClaims in line for one Kustomization in one cluster, one holds it: the one with an apply - in flight, then the one holding it already, then the one created first. The others are - Configured=False (HeldByAnotherClaim) until it lets go, and the next in line takes it over. + 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. + + 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. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -237,7 +223,7 @@ spec: properties: appliedRevision: description: |- - AppliedRevision is the revision the applier reports it applied once the operation completed. + 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: @@ -255,9 +241,10 @@ spec: type: string phase: description: |- - Phase is where the operation stands. Requested, Running, Settling and Unresolved are in - flight: while one is, an edit to an object the applier applies can still be reverted by it, - and the applier is asked nothing else. Succeeded and Failed have completed. + 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 @@ -268,8 +255,11 @@ spec: type: string reason: description: |- - Reason is why the apply was asked for. With several GitTargets on the applier 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 @@ -282,8 +272,8 @@ spec: type: string suspendToken: description: |- - SuspendToken is the reconcile.fluxcd.io/requestedAt value the suspend after the apply carried. - Once the Kustomization reports it handled, nothing started before the suspend is still + 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: @@ -297,31 +287,31 @@ spec: type: object previousSuspendState: description: |- - PreviousSuspendState is how the Kustomization stood before the applier claim first suspended it, - which handing it back restores: one that was Suspended stays suspended, one that was - NotSuspended is resumed. Recorded with the first suspend, and cleared once the Kustomization is - handed back. Absent, a hand-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 for the Kustomization: an apply reverted - an API edit made after its flush, so the cluster runs an older value than Git 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, published while an - eligible GitTarget references the applier claim. A GitTarget naming this applier claim 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 are the other objects in the cluster that apply this GitRepository and that no - FluxClaim holds: Kustomizations, and HelmReleases whose chart comes from it. Each applies a - new artifact the moment it arrives. 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 @@ -352,7 +342,7 @@ spec: type: string gitRepository: description: GitRepository is the Kustomization's spec.sourceRef, - its namespace defaulted. + 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 2acbfc8c..982d8fb0 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 through a GitProvider's credential. It holds - the branch for every GitTarget that references it: one branch worker, one credential, one - checkout. 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: |- @@ -79,33 +79,31 @@ spec: metadata: type: object spec: - description: spec defines the desired state of GitBranch + description: spec defines the desired state of GitBranch. properties: allowedGitTargets: default: - consumerNamespaces: from: Same description: |- - AllowedGitTargets says which GitTargets may write through this GitBranch. Omitted, it defaults - to one grant for this GitBranch's own namespace (consumerNamespaces: {from: Same}). A list you - write is the whole policy: nothing is added to it, so this namespace is admitted only if the - list grants it access, and [] admits no GitTarget. Grants combine by union. Mutable: 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. items: - description: |- - GitBranchGitTargetGrant admits GitTargets in some control-cluster 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 control-cluster 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 from: Same (the namespace of the object holding the grant, not - the consumer's), 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 @@ -113,11 +111,11 @@ spec: - All type: string names: - description: 'Names are the control-cluster namespaces admitted - with from: Names, by exact name.' + description: 'Names are the namespaces admitted with from: + "Names", by exact name.' items: - description: GrantNamespace is one control-cluster namespace - a grant admits by name. + description: GrantNamespace is a namespace a grant admits, + by exact name. A variable and "*" are refused. maxLength: 63 type: string x-kubernetes-validations: @@ -139,8 +137,8 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches labels on control-cluster Namespaces, with from: Selector. It must select - something: 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 @@ -211,20 +209,19 @@ spec: x-kubernetes-list-type: atomic branch: description: |- - Branch is the branch on the remote that this GitBranch writes. It must match one of the - patterns the GitProvider's spec.allowedGitBranches give this GitBranch's namespace. Only one - GitBranch writes a repository branch: the oldest admitted one - whose credential proves it can open a push session holds it, and any other GitBranch for the - same repository and branch reports Stalled with reason BranchConflict. - Immutable: delete and recreate the GitBranch to change it. + 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 this branch is written - with. Namespace omitted means this GitBranch's own namespace. The GitBranch claims its branch - only while that GitProvider's spec.allowedGitBranches admits this namespace and the 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. @@ -241,14 +238,13 @@ spec: rule: self.name != '' parentBranch: description: |- - ParentBranch is the branch spec.branch is created from, and the branch compared against while - spec.branch does not exist on the remote. Omitted: the branch the remote's HEAD points at (its - default branch), or a branch with no history when the repository is empty. When set, it must - exist on the remote for spec.branch to be created; otherwise this GitBranch reports - ParentBranchNotFound and nothing is written. Once spec.branch exists, it is used as it is and - the parent is not 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. + 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,12 +263,12 @@ spec: rule: has(self.parentBranch) == has(oldSelf.parentBranch) && (!has(self.parentBranch) || self.parentBranch == oldSelf.parentBranch) status: - description: status defines the observed state of GitBranch + description: status defines the observed state of GitBranch. properties: conditions: description: |- - Conditions report whether this GitBranch holds its repository branch and can write it: the - Ready summary plus the kstatus Reconciling/Stalled pair. + 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. @@ -333,20 +329,17 @@ spec: x-kubernetes-list-type: map gitTargetCount: description: |- - GitTargetCount is how many GitTargets reference this GitBranch, from any namespace. More than - one means they share its branch worker; each reports its own folder's health on 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 to this branch, which made the - branch worker look at once instead of at its next refresh. It records receipt only: whether - that look reached the remote is status.remote's to say. Absent means no delivery has reached - this branch: no GitReceiver is set up, the Git host's webhook does not reach one, the delivery - names the repository by a URL other than the GitProvider's spec.url, or no branch worker writes - the branch because no GitTarget references this GitBranch. It advances at most once a minute, - and is cleared when the GitProvider is recreated for another repository. + 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: @@ -356,16 +349,14 @@ spec: type: integer remote: description: |- - Remote is where this branch is on the Git remote, and when that was last proved. It is - renewed by every confirmed look: a successful push as well as a fetch. Absent means nothing - has proved anything yet: nothing has looked, or the GitProvider was recreated against a - different repository and what was published no longer describes the one it names. + 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 at all, which is not an error: a branch does not exist without a commit, and a - branch nothing has written to has nothing there yet. + 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. @@ -373,25 +364,24 @@ spec: type: string parent: description: |- - Parent is what the branch would be created from, observed with it: present only while the - remote does not carry the branch. Once the branch exists its parent is not followed, so this - is omitted. + 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 branch the remote's HEAD names 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: the commit 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's tip), Missing (the parent - is not on the remote; nothing is written) or Unborn (an empty repository, with no refs at - all, and no parent configured; 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 @@ -402,10 +392,9 @@ spec: type: object verifiedBy: description: |- - VerifiedBy is what proved it: `Push` means the server accepted a ref update of ours, so - this commit is our own work; `Fetch` means we went and looked, and this is what was - there. A `Fetch` next to a commit no publication of yours produced is how a foreign push - to the branch is read off kubectl. + 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 @@ -413,9 +402,9 @@ spec: type: object repository: description: |- - Repository is the GitProvider's repository URL, normalized: host and path, with no scheme, - user or ".git" suffix. Together with spec.branch it is the key only one GitBranch may hold, so - two GitProviders spelling 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 e435bc1e..e9ce641a 100644 --- a/config/crd/bases/configbutler.ai_gitproviders.yaml +++ b/config/crd/bases/configbutler.ai_gitproviders.yaml @@ -38,7 +38,10 @@ spec: name: v1alpha3 schema: openAPIV3Schema: - description: GitProvider is the Schema for the gitproviders API. + 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. properties: apiVersion: description: |- @@ -58,37 +61,33 @@ spec: metadata: type: object spec: - description: spec defines the desired state of GitProvider + description: spec defines the desired state of GitProvider. properties: allowedGitBranches: description: |- AllowedGitBranches says which GitBranches may claim a branch of this repository through this - GitProvider, and which branches: a GitBranch claims only while a grant admits its namespace and - one of that grant's branchPatterns matches its spec.branch. It has no default, unlike the grant - lists of GitBranch and KubeProvider, because which branches a namespace may claim is always - written: omitted or empty, no GitBranch claims through it, this GitProvider's own namespace - included. Grants combine by union. Who may write through a claimed branch is that GitBranch's - spec.allowedGitTargets. + 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 GitBranches in some control-cluster namespaces to claim the - branches its patterns match through a GitProvider. + 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, as patterns matched against - the whole branch name. '*' matches any sequence of characters, '/' included, so "*" alone is - every branch, and a name with no '*', such as "main", matches only itself. - '{consumer.namespace}' and '{consumer.name}' are the claiming GitBranch's namespace and name, - each a whole path segment: "teams/{consumer.namespace}/*". '{consumer.name}' needs - '{consumer.namespace}' in the same pattern, "teams/{consumer.namespace}/{consumer.name}": it is - a naming convention, not a bound, since 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 is a pattern matched against a whole branch name: literal text, '*' for any - sequence of characters ('/' included), 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~^:?\[\\]*$ @@ -129,14 +128,14 @@ spec: x-kubernetes-list-type: set consumerNamespaces: description: |- - ConsumerNamespaces are the control-cluster 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 from: Same (the namespace of the object holding the grant, not - the consumer's), 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 @@ -144,11 +143,11 @@ spec: - All type: string names: - description: 'Names are the control-cluster namespaces admitted - with from: Names, by exact name.' + description: 'Names are the namespaces admitted with from: + "Names", by exact name.' items: - description: GrantNamespace is one control-cluster namespace - a grant admits by name. + description: GrantNamespace is a namespace a grant admits, + by exact name. A variable and "*" are refused. maxLength: 63 type: string x-kubernetes-validations: @@ -170,8 +169,8 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches labels on control-cluster Namespaces, with from: Selector. It must select - something: 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 @@ -243,37 +242,36 @@ spec: x-kubernetes-list-type: atomic commit: description: |- - Commit configures the commit identity and signing behavior this connection uses. Message - formatting moved to GitTarget.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 configures the operator identity written as the commit committer. - When signing is enabled, Email must be a verified address on 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 - description: Email is the git committer email. + description: Email is the committer email. Defaults to "noreply@configbutler.ai". type: string name: default: GitOps Reverser - description: Name is the git committer name. + description: Name is the committer name. Defaults to "GitOps + Reverser". type: string type: object signing: - description: Signing configures commit signing. + description: Signing configures SSH commit signing. properties: generateWhenMissing: default: false description: |- - GenerateWhenMissing causes the operator to generate signing key material - in the referenced Secret when it is missing. + GenerateWhenMissing creates a signing key in the Secret when it holds none. Defaults to + false. type: boolean secretRef: - description: |- - SecretRef references the Secret containing the signing key material. - Expected keys will be defined by the signing implementation. + description: SecretRef names the Secret, in this namespace, + that holds the SSH signing key. properties: name: description: Name of the referent. @@ -290,41 +288,31 @@ spec: type: object knownHostsRef: description: |- - KnownHostsRef optionally points at a namespace-local ConfigMap or Secret holding SSH - known_hosts, so host trust can be centralized across GitProviders on the same host instead - of repeated in every credentials Secret. It is used only for SSH and ignored for HTTP auth. - Host keys are resolved in priority order: the credentials Secret's own known_hosts, then this - ref, then the install-level default known-hosts ConfigMap; if none yields valid keys, SSH - fails closed. + 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 - description: 'Kind of the referent: ConfigMap (default) or Secret.' + description: Kind is "ConfigMap" or "Secret". Defaults to "ConfigMap". enum: - ConfigMap - Secret type: string name: - description: Name of the ConfigMap or Secret. + 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. minLength: 1 type: string required: - name type: object - receiver: - description: |- - RemovedReceiver is the old place of a GitReceiver's type and secretRef. It is never read; - setting it is refused. - type: object - x-kubernetes-preserve-unknown-fields: true secretRef: description: |- - SecretRef for authentication credentials (may be nil for public repos) - - An OMITTED secretRef means anonymous access. An empty name is refused rather than treated as - one: a typo there would otherwise downgrade a private repository to anonymous silently, and - the failure would 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. properties: name: description: Name of the referent. @@ -338,8 +326,8 @@ spec: rule: self.name != '' url: description: |- - URL of the repository (HTTP/SSH). - Immutable: delete and recreate the GitProvider to point at a different repository. + URL is the repository's URL, over HTTPS or SSH. Immutable: delete and recreate the + GitProvider to change it. minLength: 1 type: string required: @@ -349,17 +337,14 @@ spec: - message: spec.url is immutable; delete and recreate the GitProvider to point at a different repository rule: self.url == oldSelf.url - - message: 'spec.receiver was replaced by a GitReceiver: create one with - this type and secretRef, and remove spec.receiver; deliveries wake - every GitProvider whose spec.url they name' - rule: '!has(self.receiver)' status: - description: status defines the observed state of GitProvider + description: status defines the observed state of GitProvider. properties: conditions: description: |- - Conditions report repository validation and commit configuration readiness: - the Ready summary plus the kstatus Reconciling/Stalled pair. + 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. @@ -420,14 +405,8 @@ spec: x-kubernetes-list-type: map lastVerifiedAt: description: |- - LastVerifiedAt is when the credential and the repository were last proved together: the - connectivity check listed the remote's refs with this provider's credential and the remote - answered. It is never cleared — a failure is already carried by Ready=False with a reason — - so the pair "not ready, last verified 11 minutes ago" says how long the connection has been - broken, which is the sentence an operator otherwise has to infer. - - 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. format: date-time type: string observedGeneration: @@ -437,9 +416,9 @@ spec: type: integer signingPublicKey: description: |- - SigningPublicKey is the operator's SSH signing public key in authorized_keys format. - Register this as a signing key on your git platform. - Only populated when commit.signing is configured and a signing 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 e07cd48e..1b1e9a2d 100644 --- a/config/crd/bases/configbutler.ai_gitreceivers.yaml +++ b/config/crd/bases/configbutler.ai_gitreceivers.yaml @@ -39,13 +39,12 @@ spec: schema: openAPIV3Schema: description: |- - GitReceiver is the endpoint a Git host notifies on a push. A delivery to it authenticates against - its token, names a repository and a branch, and wakes the branch worker of every GitProvider, in - any namespace, whose spec.url the delivery names, for that branch. A provider reaching its host - through an alias (another host name or port) is not matched, and finds pushes by its periodic - refresh. A delivery is only a hint: the branch worker reads the remote with its own provider's - credential to find out what happened, so nothing a delivery says can choose a URL, a credential - or a commit. GitBranch.status.lastPushNoticeAt shows that deliveries reach a branch. + 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: |- @@ -65,13 +64,13 @@ spec: metadata: type: object spec: - description: spec defines the desired state of GitReceiver + description: spec defines the desired state of GitReceiver. properties: secretRef: description: |- - SecretRef names a Secret in this GitReceiver's namespace whose "token" key is the webhook - secret. The token also derives 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. @@ -84,10 +83,10 @@ spec: rule: self.name != '' type: description: |- - Type is the payload and signature the Git host sends. "github" checks X-Hub-Signature-256, - "gitlab" checks X-Gitlab-Token, "gitea" checks X-Gitea-Signature, and "generic-hmac" checks - X-Signature: sha256= over a {"repository", "branch", "after"} body, - for a relay of your own. + 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 @@ -99,13 +98,13 @@ spec: - type type: object status: - description: status defines the observed state of GitReceiver + description: status defines the observed state of GitReceiver. properties: conditions: description: |- - Conditions report whether this GitReceiver serves deliveries: the Ready summary plus the - kstatus Reconciling/Stalled pair. Ready is True once the token Secret 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. @@ -171,10 +170,9 @@ spec: type: integer webhookPath: description: |- - WebhookPath is the path this GitReceiver is served on, under the receiver listener: - /git-push/ and an HMAC of this GitReceiver's namespace and name, keyed by the token. Point the - Git host's push webhook at the listener plus this path. Empty while the token Secret or its - "token" key 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 96f0082c..8310931d 100644 --- a/config/crd/bases/configbutler.ai_gittargets.yaml +++ b/config/crd/bases/configbutler.ai_gittargets.yaml @@ -82,7 +82,10 @@ spec: name: v1alpha3 schema: openAPIV3Schema: - description: GitTarget is the Schema for the gittargets API. + 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. properties: apiVersion: description: |- @@ -102,24 +105,20 @@ spec: metadata: type: object spec: - description: spec defines the desired state of GitTarget + description: spec defines the desired state of GitTarget. properties: applierClaimRef: description: |- - ApplierClaimRef names the applier claim, in this GitTarget's namespace, that holds the applier - of this folder: a FluxClaim (a Flux Kustomization) or an ArgoCDClaim (an Argo CD - Application). With it set, GitOps Reverser decides when that applier applies, always after - publishing the edits it holds: after a commit it did not push, after a refused edit - (spec.onRefusal: Reapply), after an apply reverted a newer API edit, after the API created an - object in the folder (so the applier owns it and prunes it later), and once when the operator - starts or the applier claim is first named, after the folder's initial snapshots are in Git. Objects - another applier owns are not mirrored into the folder. The applier claim's applier must apply this target's repository and - branch; the ApplierClaimReady condition says whether it does. Several GitTargets may name one - applier claim, and it is asked once for all of them. + 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 - ArgoCDClaim.' + description: 'Kind is the applier claim''s kind: "FluxClaim" or + "ArgoCDClaim".' enum: - FluxClaim - ArgoCDClaim @@ -132,74 +131,50 @@ spec: - kind - name type: object - branch: - description: |- - RemovedBranch is the old place of GitBranch.spec.branch. It is never read; setting it is - refused. - type: string - clusterProviderRef: - description: |- - RemovedClusterProviderRef is the replaced spelling of kubeProviderRef. It is never read; - setting it is refused. - properties: - name: - description: Name of the referent. - type: string - required: - - name - type: object commit: description: |- - Commit configures how this target's writes are batched into commits, and how those commits - are phrased. Omitted, writes coalesce over a 5s rolling silence window, a window closes at - most 1m after it opened, and commits use the built-in message templates. + 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 configures how this target's commit messages are formatted. Omitted, or with any - individual template left empty, the built-in templates are used. + description: Message sets the commit message templates. A template + left empty uses the built-in one. properties: eventTemplate: - description: EventTemplate is retired. Remove it and migrate - resource fields into liveTemplate's Resources. + description: |- + EventTemplate is retired and rejected. Move it to liveTemplate, reading resource fields from + Resources. type: string groupTemplate: - description: GroupTemplate is retired. Remove it and copy - its value to liveTemplate. + description: GroupTemplate is retired and rejected. Move its + value to liveTemplate. type: string liveTemplate: description: |- - LiveTemplate formats every live window, including one resource and a 0s window. - Fields: Author (raw username, possibly empty), GitTarget, Count, Operations, Resources. - Count is retained input entries after coalescing, before comparison with Git. - Resources exposes Operation, Group, Version, Resource, Namespace, Name, APIVersion, - ResourceVersion — the version of the state this commit wrote, which lags the object's - current one and is empty for a producer that observed none — and Generation, the desired - state's counter, which is 0 for a kind without a spec. Guard both with {{with}}. - A literal CommitRequest message overrides this template without changing authorship. + 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. type: string reconcileTemplate: description: |- - ReconcileTemplate formats atomic snapshots and resyncs. - Fields: Count, GitTarget, Group, Version, Resource, APIVersion, Namespace, ResourceVersion. - ResourceVersion is the snapshot LIST's version, not any one object's. Type and Namespace - fields are empty for whole-target snapshots, and ResourceVersion can be empty. - Guard optional fields so the message remains meaningful for every snapshot scope. - Revision was renamed to ResourceVersion in v0.48.0; a template still naming it is rejected. + 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 frames a CommitRequest's message instead of committing it verbatim. It - renders only for a commit window whose attached request supplied a message; every other - window uses liveTemplate or reconcileTemplate. - - It receives the same fields as liveTemplate, plus RequestMessage carrying the request's - message unaltered. That message is never parsed as a template, so template syntax inside it - stays literal. - - Omitted, a request's message is committed exactly as supplied. A template that never renders - RequestMessage is rejected. One that fails to render at commit time commits the message - verbatim instead, counted as message_source="commit_request_fallback". + 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 x-kubernetes-validations: @@ -209,18 +184,16 @@ spec: rule: '!has(self.groupTemplate)' window: description: |- - Window configures when this target's commit windows close. A commit window opens on an - author's first write to this target and collects that author's writes until it closes: after - idleTimeout of silence, or maxDuration after it opened, whichever comes first. Omitted, it is - {idleTimeout: 5s, maxDuration: 1m}. 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 after this much silence, as a Go duration string ("5s", - "750ms"). Every write the window collects restarts it. "0s" closes the window right after - each write, a 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: @@ -230,10 +203,9 @@ spec: maxDuration: default: 1m description: |- - MaxDuration closes the window this long after it opened, however much keeps arriving, as a - Go duration string ("1m", "90s"). It keeps continuous activity from postponing a commit - indefinitely. "0s" closes the window 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: @@ -247,35 +219,37 @@ spec: <= duration(self.maxDuration)' type: object encryption: - description: Encryption defines encryption settings for Secret resource - writes. + description: Encryption configures how Secrets are encrypted before + they are written to Git. properties: age: - description: Age configures age-specific encryption behavior for - SOPS. + description: Age configures age recipients for SOPS. properties: enabled: default: false - description: Enabled toggles age-based recipient resolution - and bootstrap behavior. + description: |- + Enabled turns on age encryption: resolving recipients, and creating a key when asked. + Defaults to false. type: boolean recipients: - description: Recipients defines how recipients are resolved. + description: Recipients says where the age recipients come + from. properties: extractFromSecret: default: false - description: ExtractFromSecret derives recipients from - all *.agekey entries in encryption.secretRef. + description: |- + 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 no *.agekey - exists. + description: |- + GenerateWhenMissing creates a date-named *.agekey entry in encryption.secretRef when it has + none. Defaults to false. type: boolean publicKeys: - description: PublicKeys is a static list of age recipients - (age1...). + description: PublicKeys is a fixed list of age recipients + ("age1..."). items: minLength: 1 type: string @@ -284,13 +258,14 @@ spec: type: object provider: default: sops - description: Provider selects the encryption provider. + description: Provider is the encryption tool. Only "sops" is supported. + Defaults to "sops". enum: - sops type: string secretRef: - description: SecretRef references namespace-local Secret data - used by the encryption provider. + description: SecretRef names the Secret, in this namespace, that + holds the provider's keys. properties: name: description: Name of the referent. @@ -306,11 +281,10 @@ spec: type: object gitBranchRef: description: |- - GitBranchRef names the GitBranch this target writes: the repository branch, and the credential - it is written with. Namespace omitted means this GitTarget's own namespace. The GitBranch is - used only while its spec.allowedGitTargets admit this GitTarget's namespace, its own included. - Many GitTargets may reference one GitBranch; they share its branch worker. - Immutable: delete and recreate the GitTarget to change its destination. + 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. @@ -325,28 +299,12 @@ spec: x-kubernetes-validations: - message: spec.gitBranchRef.name must not be empty rule: self.name != '' - gitProviderRef: - description: |- - RemovedGitProviderRef is the old place of GitBranch.spec.gitProviderRef. It is never read; - setting it is refused. - properties: - name: - description: Name of the referent. - type: string - namespace: - description: Namespace of the referent, when not specified it - acts as LocalObjectReference. - type: string - required: - - name - type: object kubeProviderRef: description: |- - KubeProviderRef names the KubeProvider this GitTarget mirrors FROM: the source cluster, and the - credential it is read with. Namespace omitted means this GitTarget's own namespace. The - KubeProvider is used only while its spec.allowedGitTargets admit this GitTarget's namespace, - its own included. Immutable: a folder's source cluster is part of what the folder means; - 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. @@ -367,141 +325,83 @@ spec: onRefusal: default: Ignore description: |- - OnRefusal decides what happens when a live edit is refused because it has no legal - destination in the Git folder. A refusal produces no commit, so nothing reverts the edit - until something applies Git over it. - - "Ignore", the default, records the refusal in conditions and events and does nothing else. + 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. - "Reapply" asks this target's applier (through spec.applierClaimRef, which it requires) to - apply the current tip after publishing the edits pending on the branch, which reverts the - refused edit and nothing that was waiting to be published. Only this - target's applier is asked. If its GitRepository has to fetch to reach the tip, the - Kustomizations a FluxClaim holds stay suspended and ignore the new artifact; only the objects - no applier claim holds, listed in the applier claim's status.source.alsoAppliedBy, apply it, as - they would for any push. + "Ignore" (the default) reports the refusal in conditions and events and does nothing else. - It fires only for an edit to a document the folder ALREADY holds, in a write that removes no - document anywhere: re-applying would otherwise prune, or put back, an object somebody else is - deleting. A folder-level refusal (unparseable YAML, a foreign file) never fires it, because - re-applying Git cannot repair a folder. A standing refusal is requested once, not once per - recheck; a different refused edit, or the same edit re-made after a write of that type was - accepted, is a new request. + "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 - parentBranch: - description: |- - RemovedParentBranch is the old place of GitBranch.spec.parentBranch. It is never read; - setting it is refused. - type: string path: description: |- - Path within the repository to write resources to, relative to the repository - root. Required and must be non-empty — there is no default, so a GitTarget can - never silently write to the repository root. To deliberately target the - repository root, set it to "." (the ArgoCD/Flux convention); an empty string is - rejected because it is too easy to leave blank by accident to be a deliberate - root choice. Any leading slash (absolute path) and ".." are rejected, and a - trailing slash is normalized away. - Immutable: delete and recreate the GitTarget to change its destination. + 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 declares where NEW resources are written. It has no effect on a - resource that already has a document in Git — that document is always - updated in place at its existing location, wherever that is. Mutable: a - change only affects resources created after the change. - - Its byType and default templates share one variable language (docs/configuration.md). - Besides the resource's identity, a template may read one of its labels: - "{label:app.kubernetes.io/instance}/configmaps.yaml". + 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. - Two variables can be absent for a resource that is otherwise placeable, and both are - still placed rather than skipped: "{label:key}" on a resource that does not set the - label (or sets it empty) renders the built-in "_unlabeled" bucket, and "{namespace}" on - a cluster-scoped resource renders "_cluster". Neither is a value its kind of source can - hold, so neither is ever confused with a real one. - - Append "|fallback" to either to name that bucket yourself — "{label:team|unassigned}", - "{namespace|_global}". A fallback is at most 63 characters of [A-Za-z0-9._-] and is - neither "." nor "..", so it can add no directory and escape no path; it may start with - "_" to stay collision-proof, or be empty ("{label:team|}", "{namespace|}") to render - nothing, collapsing the segment. A {namespace} fallback may name a real namespace - ("{namespace|team-a}"), which files cluster-scoped resources in that folder; begin it with - "_" instead when you want a bucket no namespace can reach. Only these two variables take a - fallback, and one written on any other is rejected rather than silently ignored — not - because the others always have a value ({groupPath} renders empty for a core resource) but - because an empty group segment collapses by design, which is the built-in path's intent. - - A label is never identity, so it does not contribute to the identity-completeness a - sensitive route requires (a {namespace} fallback does not cost it either). Because - placement runs only for a resource with no document yet, labeling one afterwards never - moves the file already written. + 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 an exact resource type key ("{group}/{version}/{resource}", e.g. - "v1/configmaps", "apps/v1/deployments", or "v1/secrets"; core resources omit - the group) to the path template used for a new resource of that type. A path - selected for a sensitive resource (Secrets, plus any operator-configured - sensitive type) must be identity-complete so it cannot collide two distinct - sensitive resources onto one 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 used for a new resource whose type has no ByType - entry. Omitted, it falls through to the folder's one supported kustomization - root, if it has exactly one, and then to the built-in path. - A bundling default (one that is not identity-complete, - such as "all.yaml") is only valid when a sensitive resource can never reach it - — give every sensitive type an explicit identity-complete 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 declares that this folder is a kustomize folder whose root the operator - maintains. It controls one thing: what happens when NO kustomization governs the path a new - document lands at. - - Omitted or false, the document is written and nothing else is touched. True, a - kustomization.yaml is created at spec.path and the new document is registered in it as part - of the same commit. - - A created root ADOPTS the folder: its resources: lists every managed document already there - as well as the new one. Turning a folder into a kustomize folder means the folder, and a - root naming one file would leave every other file in Git but out of every render. - - A created root carries NO namespace:. It holds an apiVersion, a kind and the resources: list - and nothing else, so the namespace still comes from the documents (serializeNamespace unset - or true) or from whatever installs the folder (serializeNamespace false). - - A folder that already has a kustomize render root never gains a second one, because two - render roots is an ambiguous folder that stops accepting new documents altogether. If a - byType or default template places a document outside the existing root, that placement is - REFUSED rather than committed as a file no kustomization would render. - - It has NO bearing on a folder that already has a root. A new file is always registered with - the nearest kustomization governing it, whatever chose its path, because a file no - kustomization lists is a file kustomize never builds. + 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 controls which deletion paths may remove documents from this target's folder: an - explicit source DELETE event, and the resync mark-and-sweep that infers a deletion from a - desired snapshot. Omitted, it is `mode: OnEvent` — observed deletes are mirrored, inferred - ones are not — for a stored GitTarget as well as a new one. + 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 selects which deletion paths are enabled. `Never` removes nothing; `OnEvent` mirrors an - observed source DELETE but never infers a deletion from a resync snapshot; `Always` enables - both, restoring full desired-state convergence. Omitted, it is `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 @@ -510,49 +410,29 @@ spec: type: object serializeNamespace: description: |- - SerializeNamespace declares whether a committed document carries its own - metadata.namespace. It governs every write this target makes, not just the first one, and it - applies to NAMESPACED resources only — a cluster-scoped document has no namespace, so the - field is ignored for it rather than being an error. + 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, the namespace is INFERRED per document, which is the behavior a target that says - nothing has always had: metadata.namespace is omitted only when the kustomization governing - the document's path sets namespace: to that resource's own namespace, and written explicitly - in every other case. Leave it unset for a folder that is legitimately non-uniform — a tree of - nested kustomize roots, each supplying its own namespace — because inference resolves each - document against the root governing its own path. + 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: the setting for a flat folder applied directly, where nothing - downstream supplies a namespace and a document without one is ambiguous. + true always writes it. Use it for a flat folder applied as is. - false never writes it, and is a claim about the whole folder: something outside this - repository — a Flux Kustomization's targetNamespace, an Argo Application's - destination.namespace, or a kustomization this target maintains itself — supplies the - namespace instead. Because a namespace-less document takes its namespace from a single - supplier, an explicit false admits exactly ONE source namespace: a second WatchRule bringing - another namespace to this target is refused, with GitPathAccepted=False and 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 target from writing to Git, without deleting it. It is the knob to turn - when something is wrong and the writes have to stop now: watches keep running, events keep - arriving, the folder keeps being scanned and status.placement keeps being maintained, and no - new commit is planned while it is set. - - It takes effect at the next planning boundary, not instantly. Work already committed - locally when suspend is set is still pushed, so a target suspended during a push cooldown - can publish one more commit seconds later. That is deliberate: a local commit that is never - pushed would sit in the worker's checkout indefinitely and surface later, out of order, on - resume. Suspend is a valve on new work, not an undo — it stops the next write, it does not - revert the last one. - - While it is set, status.retention is not published at all: nothing is swept, so nothing is - measured, and reporting zero retained documents would read as "converged" when nothing was - counted. - - Omitted, it is false. Clearing it resumes writing from the current cluster state on the next - resync; the writes suppressed while suspended are not replayed. + 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 @@ -566,21 +446,16 @@ spec: - message: spec.path is immutable; delete and recreate the GitTarget to change its destination rule: self.path == oldSelf.path - - message: spec.gitProviderRef, spec.branch and spec.parentBranch moved - to a GitBranch; set spec.gitBranchRef to it - rule: '!has(self.gitProviderRef) && !has(self.branch) && !has(self.parentBranch)' - - message: spec.clusterProviderRef was replaced by spec.kubeProviderRef, - which names a namespaced KubeProvider - rule: '!has(self.clusterProviderRef)' - message: 'spec.onRefusal: Reapply needs spec.applierClaimRef, the applier claim asked to reapply' rule: '!has(self.onRefusal) || self.onRefusal != ''Reapply'' || has(self.applierClaimRef)' status: - description: status defines the observed state of GitTarget + description: status defines the observed state of GitTarget. properties: conditions: - description: Conditions represent the latest available observations - of an object's state + 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. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -646,32 +521,28 @@ spec: type: integer placement: description: |- - Placement is what the last scan resolved about this folder's layout: whether a kustomize - render root governs new documents, which one, and whether the folder renders a base it may - not write to. It is an observation, like streams and retention, and the LayoutResolved - condition carries 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, and it is the field that predicts the surprises. + Mode is how this folder is written. - `Plain` — no kustomization governs the folder. A new document is written and nothing else - is touched; a delete removes it and nothing else is touched. + "Plain": no kustomization governs the folder. Documents are written and removed, and + nothing else is touched. - `KustomizeRoot` — exactly one kustomization governs the folder and the folder is - self-contained. A new document is also registered in that root's `resources:`; a delete - also drops its entry; and every write is proved by re-rendering, so kustomize itself 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 additionally renders a base outside - it (ReadOnlyBases). The base is read-only input: an edit to a field the base owns is - authored into the overlay when it is expressible there (`images:`, `replicas:`) and refused - with WriteBoundaryRefused otherwise, and DELETING an object the overlay inherits authors a - `$patch: delete` into the overlay rather than removing anything. + "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 covers more than one render root, which is LayoutResolved=Ambiguous: - there is no single answer, and no new document is placed until the target is pointed 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 @@ -679,60 +550,46 @@ spec: type: string readOnlyBases: description: |- - ReadOnlyBases are the kustomization directories this folder renders but may never write to, - relative to spec.path — so they lead with "../". Non-empty exactly when Mode is - KustomizeOverlay, and it is what makes a WriteBoundaryRefused message predictable instead of - surprising: an edit that lands on a document under one of these paths cannot be written - where it lives. + 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 in this folder, - relative to spec.path; "." is the folder itself. Empty for Plain (there is no root) and - under LayoutResolved=Ambiguous (there is no single one). + 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 resolution was computed. Like ResolvedAtCommit it dates the - resolution rather than the last scan, so a timestamp well in the past means the folder's - shape has been stable, not that scanning stopped. + 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 hash this resolution was first observed at. It is not - re-stamped on every scan: a resolution that has not changed is not republished, because - doing so would write status once per commit to the branch, whichever target caused the - commit. So it dates the RESOLUTION, not the last scan — a commit older than the branch - head means the folder's layout has not changed since, not that nothing has looked. - - It is empty when the branch had no commit at the time (a folder nothing has written to yet) - and is filled in by the first scan that finds one. + 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 reports documents a resync kept because this target's spec.prune.mode suppressed - the mark-and-sweep. It covers the INFERRED deletion path only: under `never`, a suppressed - source DELETE is not counted here, so a `never` target can report zero while still declining - to mirror deletes. It is informational either way — retention is the configured behavior, - never a fault, and no condition changes state because of it. + 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 records when the reported retention count or effective prune mode last - changed. It does NOT indicate when retention was last evaluated: a resync that re-reports the - same count leaves it untouched, so an old timestamp is equally consistent with stable - retention and with nothing having measured it since. + 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 EFFECTIVE spec.prune.mode this roll-up was produced under. It is reported here - rather than left to be read from the spec because a GitTarget that predates spec.prune has - no stored value at all, so the spec alone cannot explain why documents are being kept. + description: Mode is the spec.prune.mode these counts were taken + under, with the default applied. enum: - Never - OnEvent @@ -740,50 +597,46 @@ spec: type: string retainedDocuments: description: |- - RetainedDocuments is how many managed documents the policy kept that a converged mirror - would not hold. Zero means a resync ran and found nothing to retain — the mirror is - converged. An ABSENT retention block means something different: 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: - retainedDocuments type: object streams: - description: |- - Streams is the bounded data-plane roll-up over this GitTarget's tracked types. - Counts, never a per-type list, so it stays bounded however many types are watched. + description: Streams counts the types this GitTarget watches, by watch + state. properties: blocked: - description: Blocked is how many tracked types cannot currently - be watched. + description: Blocked is how many of them cannot be watched right + now. format: int32 type: integer pendingSample: - description: PendingSample is a bounded sample of types not yet - ready. + description: PendingSample names up to five types that are not + ready yet. items: type: string maxItems: 5 type: array ready: - description: Ready is how many tracked types are Streaming. + description: Ready is how many of them are streaming events. format: int32 type: integer replaying: - description: Replaying is how many tracked types are still replaying - their initial events. + description: Replaying is how many of them are still receiving + their initial list. format: int32 type: integer summary: description: |- - Summary is the display-only ready/total ratio, e.g. "3/4". - - 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. - Do not compute anything from it — read ready and total. + 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 tracked. + description: Total is how many types are watched. format: int32 type: integer required: diff --git a/config/crd/bases/configbutler.ai_kubeproviders.yaml b/config/crd/bases/configbutler.ai_kubeproviders.yaml index d9d157c5..1dadb5ca 100644 --- a/config/crd/bases/configbutler.ai_kubeproviders.yaml +++ b/config/crd/bases/configbutler.ai_kubeproviders.yaml @@ -43,11 +43,10 @@ spec: schema: openAPIV3Schema: description: |- - KubeProvider is a connection to a Kubernetes cluster and the identity used there. A GitTarget - mirrors from it; an applier claim reaches its applier through it. It holds the credential that - reaches it (a kubeconfig Secret in its own namespace, or the operator's own identity where the - installation allows it), who may use it and for what (spec.allowedGitTargets and - spec.allowedApplierClaims), and the audit route its attribution facts are partitioned under. + 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: |- @@ -76,35 +75,28 @@ spec: consumerNamespaces: from: Same description: |- - AllowedApplierClaims says which applier claims (FluxClaims and ArgoCDClaims) may hold an applier - through this provider, and in which namespaces that applier may live (applierNamespaces). - Admission to mirror does not admit to apply. Omitted, it defaults to one grant for this - KubeProvider's own namespace, holding appliers in the namespace of the same name - (consumerNamespaces: {from: Same}, applierNamespaces: ["{consumer.namespace}"]). A list you - write is the whole policy: nothing is added to it, and [] admits no applier claim. 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 applier claims in some control-cluster namespaces to hold an - applier through a KubeProvider, and bounds where that applier may live. + 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 namespaces in this provider's cluster in which an admitted applier claim's - Kustomization or Application may live: exact names, '{consumer.namespace}' for the namespace - named like the applier claim's, or '*' for every namespace. - - The effective scope is the union of applierNamespaces from all grants admitting the applier - claim, after expanding variables. This is a permission, not a selection: it is checked against - one namespace, that of the applier claim's kustomizationRef or applicationRef. The check runs - when the applier claim reconciles, and again when a grant changes, never at admission. An - applier outside the union is refused, in the applier claim's status - (ApplierNamespaceNotAllowed), and an applier 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 one namespace in a source or applier scope: a namespace name, - "{consumer.namespace}" for the namespace of the same name as 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: @@ -123,14 +115,14 @@ spec: x-kubernetes-list-type: set consumerNamespaces: description: |- - ConsumerNamespaces are the control-cluster namespaces whose applier claims 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 from: Same (the namespace of the object holding the grant, not - the consumer's), 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 @@ -138,11 +130,11 @@ spec: - All type: string names: - description: 'Names are the control-cluster namespaces admitted - with from: Names, by exact name.' + description: 'Names are the namespaces admitted with from: + "Names", by exact name.' items: - description: GrantNamespace is one control-cluster namespace - a grant admits by name. + description: GrantNamespace is a namespace a grant admits, + by exact name. A variable and "*" are refused. maxLength: 63 type: string x-kubernetes-validations: @@ -164,8 +156,8 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches labels on control-cluster Namespaces, with from: Selector. It must select - something: 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,35 +234,32 @@ spec: sourceNamespaces: - '{consumer.namespace}' description: |- - AllowedGitTargets says which GitTargets may mirror through this provider, and what: each grant - admits some control-cluster namespaces and bounds what the WatchRules feeding a GitTarget there - may watch (sourceNamespaces), and what a ClusterWatchRule may mirror through it - (clusterResources). Omitted, it defaults to one grant for this KubeProvider's own namespace, - mirroring the namespace of the same name (consumerNamespaces: {from: Same}, sourceNamespaces: - ["{consumer.namespace}"]); it grants no cluster-scoped resource. A list you write is the whole - policy: nothing is added to it, and [] admits no GitTarget. 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 GitTargets in some control-cluster namespaces to mirror through a - KubeProvider, and bounds what they may mirror. + 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 resource types a ClusterWatchRule may mirror through an - admitted GitTarget: resource.group (clusterroles.rbac.authorization.k8s.io), a bare resource for - the core group only (nodes), or '*' for every one, including CRDs installed later. An identity - 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 effective scope is the union of clusterResources from all grants admitting the GitTarget. - This is a permission, not a selection: listing a type mirrors nothing until a ClusterWatchRule - asks for it. A ClusterWatchRule item with '*' in apiGroups or resources needs '*' in at least - one admitting grant; otherwise it is refused, never narrowed to the types listed. The check runs - when the ClusterWatchRule reconciles, never at admission, and a rule asking for any type outside - the union is refused as a whole, in its status (ClusterResourceNotAllowed). + 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 names a cluster-scoped resource type: resource.group, a bare resource for - the core group, or "*" for every one. + 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 @@ -280,14 +269,14 @@ spec: x-kubernetes-list-type: set consumerNamespaces: description: |- - ConsumerNamespaces are the control-cluster 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 from: Same (the namespace of the object holding the grant, not - the consumer's), 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 @@ -295,11 +284,11 @@ spec: - All type: string names: - description: 'Names are the control-cluster namespaces admitted - with from: Names, by exact name.' + description: 'Names are the namespaces admitted with from: + "Names", by exact name.' items: - description: GrantNamespace is one control-cluster namespace - a grant admits by name. + description: GrantNamespace is a namespace a grant admits, + by exact name. A variable and "*" are refused. maxLength: 63 type: string x-kubernetes-validations: @@ -321,8 +310,8 @@ spec: x-kubernetes-list-type: set selector: description: |- - Selector matches labels on control-cluster Namespaces, with from: Selector. It must select - something: 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 @@ -387,29 +376,19 @@ spec: && size(self.selector.matchExpressions) > 0)' sourceNamespaces: description: |- - SourceNamespaces are source-cluster namespaces that a WatchRule feeding an admitted GitTarget may - name in rules[].sourceNamespace. Entries are exact names, '{consumer.namespace}' for the - namespace named like the GitTarget's, or '*'. Omitted, this grant gives no namespace. - - The effective scope is the union of sourceNamespaces from all grants admitting the GitTarget, - after expanding variables. Every WatchRule item must be allowed by that union, an omitted - sourceNamespace included (it names the WatchRule's own namespace). This is a permission, not a - selection: listing a namespace watches nothing until a WatchRule names it. + 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. - It is not a filter on "*". A rule item with sourceNamespace "*" needs '*' in at least one - admitting grant; otherwise it is refused, never narrowed to the namespaces listed. With '*' - granted, such an item is one cluster-wide watch, bounded only by this KubeProvider credential's - RBAC. - - The check runs when the WatchRule reconciles, and again when a grant changes, never at - admission. A WatchRule with any item outside the union is refused as a whole, in its status - (SourceNamespaceNotAllowed), and watches nothing. The scope bounds what is read, never where - objects are written in Git, and the credential's RBAC still applies inside it. + 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 one namespace in a source or applier scope: a namespace name, - "{consumer.namespace}" for the namespace of the same name as 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: @@ -437,67 +416,59 @@ spec: type: array x-kubernetes-list-type: atomic attribution: - description: |- - Attribution groups this cluster's author-attribution settings. The block is spelled - "attribution" rather than "authorAttribution" even though the operator flags are - --author-attribution-*: the prefix groups a flat flag namespace, and a block on a - source-cluster object already supplies that scope. + 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 sender is the API server's - webhook backend (https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/#webhook-backend), - and this is the segment its configured URL ends in: /audit-webhook/. When - several logical clusters share one backend it is instead the value of the audit-event - annotation named by --author-attribution-audit-route-annotation-key. + 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. - It partitions the attribution facts, so two KubeProviders carrying the same route read one - cluster's facts, and two carrying different routes can never cross-credit an author. - - Empty means ., which no provider in another namespace can resolve to. Set it - when several KubeProviders in this namespace name one cluster, since an API server has one - webhook backend and so posts under one route. A route is held by one namespace: providers in - another namespace that carry it report Stalled=True with reason AuditRouteConflict and are not - used. The namespace a route starts with (.) holds it when a provider there carries - it; otherwise the namespace of the oldest provider carrying it does. + 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 overrides the operator's outgoing kube-client throttles for this cluster. Omitted, - the built-in defaults apply (20 QPS, burst 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 overrides the operator's outgoing kube-client burst for this cluster. Omitted, the - built-in default of 30 applies. + 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 overrides the operator's outgoing kube-client query-per-second throttle for this - cluster's watches and discovery. Omitted, the built-in default of 20 applies. + 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 provider 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 with reason InClusterNotAuthorized and is never used. Exactly one - of secretRef and inCluster is set. Immutable. + 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 a Secret in this KubeProvider's namespace holding a kubeconfig for the source - cluster. When key is empty the kubeconfig is read from "value", then "value.yaml" (Flux's - order). Unsafe kubeconfigs (exec auth, insecure-skip-tls-verify, credentials named by file - path) are refused with Validated=False unless the operator opts in through its flags. - Exactly one of secretRef and inCluster is set. Immutable. + 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 @@ -528,26 +499,23 @@ spec: properties: auditRoute: description: |- - AuditRoute is the route this provider's attribution facts are partitioned under, as resolved: - spec.attribution.auditRoute, or . when that is empty. It is the route the - AuditFactsReceived condition describes, so when it changes that condition starts over — - proof that one route delivered says nothing about another. + 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 report the provider's readiness: Validated (the credential is safe and - resolvable, asserted without a network dial) plus the aggregated Ready and the kstatus - Reconciling/Stalled pair. Stalled=True with reason InClusterNotAuthorized or - AuditRouteConflict means the provider is not used at all. + 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 reports whether an attribution fact has ever been accepted and recorded on - this provider's audit route. It is True (Received) from the first one on and never goes back: - later silence is a quiet cluster, not a fault. Until then it is Unknown, with the reason - naming which silence it is and therefore where to look: TransportUnavailable (the fact - transport is refusing writes, so no route can be judged), NoAuditDelivery (nothing is posting - audit to this operator at all), or RouteUnused (audit is arriving and has never carried this - route). It is absent when the operator runs with author attribution disabled. It is not part - of Ready. + 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 cda73c96..7d435aad 100644 --- a/config/crd/bases/configbutler.ai_watchrules.yaml +++ b/config/crd/bases/configbutler.ai_watchrules.yaml @@ -50,14 +50,9 @@ spec: schema: openAPIV3Schema: description: |- - WatchRule selects NAMESPACED resources on the source cluster its GitTarget mirrors from, with - filtering by API group, version, source namespace, and object labels. Scope is carried by the rule - KIND: a WatchRule never selects cluster-scoped types — use a ClusterWatchRule for those. - - Each spec.rules[] item watches its own source namespace: this WatchRule's OWN namespace when - omitted, an explicit name, or "*" for every namespace the GitTarget admits. Anything other than - its own namespace passes the gate described on rules[].sourceNamespace. RBAC controls who may - create or modify WatchRules per namespace. + 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: |- @@ -77,11 +72,12 @@ spec: metadata: type: object spec: - description: spec defines the desired state of WatchRule + description: spec defines the desired state of WatchRule. properties: gitTargetRef: - description: GitTargetRef names the GitTarget this rule feeds, in - this WatchRule's own namespace. + description: |- + GitTargetRef names the GitTarget, in this namespace, that mirrors what this WatchRule + selects. properties: name: description: Name of the referent. @@ -94,62 +90,40 @@ spec: rule: self.name != '' rules: description: |- - Rules define which resources to watch, and in which source namespaces. - Multiple rules create a logical OR - a resource matching ANY rule is watched. - Each rule can specify API groups, versions, resource types, and a source namespace. - A selected resource collection is observed through its whole lifecycle; the GitTarget's - spec.prune.mode decides whether an observed removal 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 defines a set of namespaced resources to watch. - Omitted API groups and versions are resolved from the served Kubernetes API surface. - All fields except Resources are optional. + ResourceRule selects namespaced resources by type, and optionally by API group, version, source + namespace and label. properties: apiGroups: description: |- - APIGroups to match. Empty string ("") matches the core API group. - If omitted, GitOps Reverser resolves the resource name across all served API groups. - Wildcards supported: "*" matches all groups. - Examples: - - [""] matches core API (pods, services, configmaps) - - ["apps"] matches apps API group (deployments, statefulsets) - - ["", "apps"] matches both core and apps groups - - ["*"] matches all groups - - [] resolves a named resource only when it is served by one API group + 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 to match. If empty, uses the preferred served version for each group/resource. - Wildcards supported: "*" matches all versions. - Examples: - - ["v1"] matches only v1 version - - ["v1", "v1beta1"] matches both versions - - ["*"] matches all served versions - - [] matches the preferred served version - - Multi-version note: the built-in cold-start Git path is versionless, so two - objects that differ only by API version resolve to the same file. To watch - several versions of a group/resource and keep them in separate files, give the - GitTarget a placement template that includes {version} (see GitTargetPlacementSpec). + 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 objectSelector: description: |- - ObjectSelector selects which objects of the matched types are mirrored, by label. The API - server applies it to every LIST and WATCH this item opens, with list/watch semantics: an - object that stops matching leaves the collection exactly as a deleted one does, and the - GitTarget's spec.prune.mode decides what that does to its Git document. - It is a standard Kubernetes label selector: matchLabels and matchExpressions, ANDed. - Omitted or {} selects every object. A selector that does not parse refuses the whole rule + 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). - - A selected mirror owns every document of its type in its namespace scope, including documents - the selector never matched: under prune.mode Always, a complete snapshot removes them. - Two rules feeding one GitTarget whose collections overlap (same type, and the same namespace or - one of them all namespaces) must use the same selector; the newer rule is refused. properties: matchExpressions: description: matchExpressions is a list of label selector @@ -196,20 +170,10 @@ spec: x-kubernetes-map-type: atomic resources: description: |- - Resources to match (plural names like "pods", "configmaps"). - This field is required and determines which resource types trigger this rule. - Wildcard semantics follow Kubernetes admission webhook patterns: - - "*" matches all resources - - "pods" matches exactly pods (case-insensitive) - - For custom resources, use the exact plural resource name and set apiGroups - when more than one served API group exposes that name. - - Note: Subresources cannot be added here. Values containing "/" (for example - "pods/log" or "pods/*") are rejected by the API because subresources are - not supported for list/watch snapshot planning. Prefix/suffix wildcards - like "pod*" or "*.example.com" are NOT supported. Use exact matches or the - "*" wildcard for broad matching. + 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 pattern: ^[^/]*$ @@ -218,22 +182,15 @@ spec: type: array sourceNamespace: description: |- - SourceNamespace is the namespace this item watches IN THE SOURCE CLUSTER its GitTarget - mirrors from: omitted for this WatchRule's own namespace, an exact name for one other, or - "*" for every namespace the source credential can read. - - "*" is one cluster-wide list and watch rather than a set of per-namespace ones, so its bound - is the credential's own RBAC and nothing else. - - Every value, this rule's own namespace included, must be in the union of sourceNamespaces from - all grants in the GitTarget's KubeProvider spec.allowedGitTargets that admit the GitTarget - ("{consumer.namespace}" there is the target's own namespace). "*" needs "*" in at least one of - them; a union without it refuses the rule, it does not narrow the watch. One item outside the - union refuses the whole WatchRule (SourceNamespaceNotAllowed). The check runs when the rule - reconciles, never at admission. + 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. - This changes only which namespace is WATCHED, never where objects are written: Git placement - follows each mirrored object's own namespace. + 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 @@ -247,11 +204,13 @@ spec: - rules type: object status: - description: status defines the observed state of WatchRule + description: status defines the observed state of WatchRule. properties: conditions: - description: Conditions represent the latest available observations - of an object's state + 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. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -316,40 +275,37 @@ spec: format: int64 type: integer streams: - description: Streams is the bounded stream-readiness roll-up for the - types this rule resolves. + description: Streams counts the types this WatchRule resolves, by + watch state. properties: blocked: - description: Blocked is how many tracked types cannot currently - be watched. + description: Blocked is how many of them cannot be watched right + now. format: int32 type: integer pendingSample: - description: PendingSample is a bounded sample of types not yet - ready. + description: PendingSample names up to five types that are not + ready yet. items: type: string maxItems: 5 type: array ready: - description: Ready is how many tracked types are Streaming. + description: Ready is how many of them are streaming events. format: int32 type: integer replaying: - description: Replaying is how many tracked types are still replaying - their initial events. + description: Replaying is how many of them are still receiving + their initial list. format: int32 type: integer summary: description: |- - Summary is the display-only ready/total ratio, e.g. "3/4". - - 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. - Do not compute anything from it — read ready and total. + 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 tracked. + description: Total is how many types are watched. format: int32 type: integer required: diff --git a/docs/TODO.md b/docs/TODO.md index 761e989f..9c817f43 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -112,10 +112,6 @@ Their argument for each is in [gitops-api-open-asks.md](future/gitops-api-open-a has no production observer, so nothing sweeps the manifests of an uninstalled CRD, and since #419 the operator asks Flux or Argo CD to apply that folder. Its consumer is the `stop` classification in [target-watch-plan.md](design/target-watch-plan.md), "What a collection leaving means". -- [ ] **Drop the "TEMPORARY at Info" logs to V(1)** (`gittarget_controller.go`, - `watch/render_fidelity_gate.go`, `watch/event_router.go`), now that Failure A of - [watch-plane-status-convergence-failures.md](design/watch-plane-status-convergence-failures.md) - is fixed. - [ ] Pin with a test that an unforced retention pass keeps a scope's revision, so only `Invalidate` re-issues one (`internal/git/render_fidelity_gate.go`), and close the retention revision-mismatch question in @@ -179,16 +175,6 @@ Their argument for each is in [gitops-api-open-asks.md](future/gitops-api-open-a ## Docs and vocabulary -- [ ] **Cleanup: drop the "loud rejection" fields.** `RemovedReceiver` (`gitprovider_types.go`), - `RemovedClusterProviderRef` (`gittarget_types.go`, `fluxclaim_types.go`, `argocdclaim_types.go`), - `RemovedGitProviderRef`, `RemovedBranch` and `RemovedParentBranch` (`gittarget_types.go`) refuse an - old spelling at admission with a message naming its replacement. Keeping that message is not a - release defect; with no users yet they can go, with their CEL rules and the matching sentences in - [UPGRADING.md](UPGRADING.md), whenever convenient. -- [ ] **Cleanup, F-i: CRD text beyond the contract.** `RepairPending` says "an API edit made after - its flush", and `kubectl explain` never defines "flush" (`fluxclaim_types.go`, - `argocdclaim_types.go`). Check `applierClaimRef`'s description for rationale too (AGENTS.md, API - changes). - [ ] **Stale design-doc headers**, one docs PR: [open-asks-priority.md](design/open-asks-priority.md) (#339 closed, #23 fixed), [gittarget-api-wave.md](design/gittarget-api-wave.md) (riders shipped), [git-write-preflight.md](design/git-write-preflight.md) (superseded by the red-status plan), diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 795b81ba..4c264e62 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -70,8 +70,8 @@ The source cluster is a namespaced `KubeProvider`. The cluster-scoped `ClusterPr gone, and nothing converts one: a CRD cannot change its scope. `GitTarget`, `FluxClaim` and `ArgoCDClaim` name their provider with the required, immutable `spec.kubeProviderRef`, whose omitted namespace means the referrer's own. There is no provider named `default` that an omitted -reference falls back to. `spec.clusterProviderRef` stays in the schema only so that setting it is -refused at admission, with a message naming `spec.kubeProviderRef`. +reference falls back to. `spec.clusterProviderRef` is gone from the schema: `kubectl apply` refuses +it as an unknown field, and a client that does not validate fields strictly has it pruned. ```yaml kind: KubeProvider @@ -131,7 +131,7 @@ name, so two providers with one name in two namespaces are two series. A repository branch is its own object, `GitBranch`, claiming the branch through the `GitProvider` whose credential writes it. A `GitTarget` names one with the required, immutable `spec.gitBranchRef`, and `spec.gitProviderRef`, `spec.branch` and `spec.parentBranch` are gone from -`GitTarget`: setting one is refused at admission, with a message naming `spec.gitBranchRef`. +`GitTarget`'s schema: `kubectl apply` refuses them as unknown fields. `GitTarget.status.remote` is now `GitBranch.status.remote`, and `GitProvider.status.branches` is gone; `kubectl get gitbranch -A` answers what it did. @@ -372,10 +372,9 @@ no change. A receiver under another name publishes a new `status.webhookPath`; r `kubectl get gitreceiver` shows `Ready=True` once its token is readable and the listener is on, and after the next push the `GitBranch`'s `status.lastPushNoticeAt` shows the delivery reached it. -`GitProvider.spec.receiver` stays in the schema only so that setting it is refused at admission, -with a message naming the replacement. A `GitProvider` that still carries it keeps working as a -provider, and its status keeps updating, but any write that changes its `spec` is refused until that -same write removes the field. Nothing is served on its old path. +`GitProvider.spec.receiver` is gone from the schema: `kubectl apply` refuses it as an unknown field, +and a client that does not validate fields strictly has it pruned. Nothing is served on its old +path. **A delivery is routed by the repository it names.** The repository URLs in the payload (`repository.clone_url`, `ssh_url` and `html_url` from GitHub and Gitea, `project.git_http_url` and diff --git a/hack/crdfields/fields.yaml b/hack/crdfields/fields.yaml index de2573cf..57d8422b 100644 --- a/hack/crdfields/fields.yaml +++ b/hack/crdfields/fields.yaml @@ -54,8 +54,6 @@ kinds: - path: commit.signing default: "off" doc: SSH commit signing. See [commit signing](#commit-signing) - - path: receiver - skip: replaced by `GitReceiver`, kept in the schema only so admission refuses it, naming the replacement - kind: GitBranch title: "`GitBranch` (namespaced): one branch of the repository" @@ -139,20 +137,12 @@ kinds: doc: >- The `GitBranch` this target writes: its own namespace, or `namespace` names another. Its `allowedGitTargets` must admit this namespace - - path: gitProviderRef - skip: moved to `GitBranch`, kept in the schema only so admission refuses it, naming `gitBranchRef` - - path: branch - skip: moved to `GitBranch`, kept in the schema only so admission refuses it, naming `gitBranchRef` - - path: parentBranch - skip: moved to `GitBranch`, kept in the schema only so admission refuses it, naming `gitBranchRef` - path: path doc: Folder within the repository. `.` targets the root; empty is rejected - path: kubeProviderRef doc: >- The `KubeProvider` to mirror from: its own namespace, or `namespace` names another. Its `allowedGitTargets` must admit this namespace - - path: clusterProviderRef - skip: replaced by `kubeProviderRef`, kept in the schema only so admission refuses it, naming `kubeProviderRef` - path: commit.window skip: a block; its two timers are listed below - path: commit.window.idleTimeout @@ -264,8 +254,6 @@ kinds: doc: >- The `KubeProvider` whose cluster holds the `Kustomization`: the applier claim's own namespace, or `namespace` names another. Its `allowedApplierClaims` must admit this namespace - - path: clusterProviderRef - skip: replaced by `kubeProviderRef`, kept in the schema only so admission refuses it, naming `kubeProviderRef` - path: kustomizationRef doc: >- The `Kustomization` that applies the folders of the `GitTarget`s naming this applier claim. See @@ -279,8 +267,6 @@ kinds: doc: >- The `KubeProvider` whose cluster holds the `Application`: the applier claim's own namespace, or `namespace` names another. Its `allowedApplierClaims` must admit this namespace - - path: clusterProviderRef - skip: replaced by `kubeProviderRef`, kept in the schema only so admission refuses it, naming `kubeProviderRef` - path: applicationRef doc: >- The `Application`, with automated sync off, that syncs the folders of the `GitTarget`s diff --git a/internal/controller/superseded_fields_admission_test.go b/internal/controller/superseded_fields_admission_test.go index d620cfd2..1aca28ed 100644 --- a/internal/controller/superseded_fields_admission_test.go +++ b/internal/controller/superseded_fields_admission_test.go @@ -233,11 +233,9 @@ var _ = Describe("Reference names must not be empty", func() { }) }) -// spec.clusterProviderRef was replaced by spec.kubeProviderRef on every kind that named a source -// cluster. A pruned field would apply cleanly and leave a manifest written for the old API looking -// accepted, so the old spelling stays in each schema and a CEL rule refuses it, naming -// kubeProviderRef. -var _ = Describe("Retained and refused clusterProviderRef", func() { +// spec.kubeProviderRef has no default on any kind that names a source cluster: an omitted reference +// must be refused, not resolved to some provider. +var _ = Describe("Required kubeProviderRef", func() { referrer := func(kind, name string, spec map[string]any) *unstructured.Unstructured { switch kind { case "GitTarget": @@ -257,17 +255,6 @@ var _ = Describe("Retained and refused clusterProviderRef", func() { } for _, kind := range []string{"GitTarget", "FluxClaim", "ArgoCDClaim"} { - It("refuses spec.clusterProviderRef on a "+kind+", naming spec.kubeProviderRef", func() { - ctx := context.Background() - obj := referrer(kind, "legacy-cluster-provider", map[string]any{ - "kubeProviderRef": map[string]any{"name": "local"}, - "clusterProviderRef": map[string]any{"name": "default"}, - }) - err := k8sClient.Create(ctx, obj) - Expect(err).To(HaveOccurred(), "the old field must not apply cleanly") - Expect(err.Error()).To(ContainSubstring("spec.clusterProviderRef was replaced by spec.kubeProviderRef")) - }) - It("requires spec.kubeProviderRef on a "+kind+", with no default", func() { ctx := context.Background() err := k8sClient.Create(ctx, referrer(kind, "no-kube-provider-ref", map[string]any{})) @@ -277,11 +264,9 @@ var _ = Describe("Retained and refused clusterProviderRef", func() { } }) -// GitTarget.spec.gitProviderRef, spec.branch and spec.parentBranch moved to a GitBranch. A pruned -// field would apply cleanly and leave the GitTarget naming no branch, so the old spellings stay in -// the schema and a CEL rule refuses them, naming gitBranchRef. -var _ = Describe("Retained and refused GitTarget branch fields", func() { - legacyTarget := func(name string, spec map[string]any) *unstructured.Unstructured { +// GitTarget.spec.gitBranchRef is the only way a GitTarget names its branch, so it is required. +var _ = Describe("Required GitTarget gitBranchRef", func() { + unstructuredTarget := func(name string, spec map[string]any) *unstructured.Unstructured { spec["gitBranchRef"] = map[string]any{"name": "main"} spec["kubeProviderRef"] = map[string]any{"name": "local"} spec["path"] = "apps" @@ -293,24 +278,10 @@ var _ = Describe("Retained and refused GitTarget branch fields", func() { }} } - It("refuses spec.gitProviderRef, spec.branch and spec.parentBranch, naming spec.gitBranchRef", func() { - ctx := context.Background() - - for name, spec := range map[string]map[string]any{ - "legacy-provider": {"gitProviderRef": map[string]any{"name": "platform"}}, - "legacy-branch": {"branch": "main"}, - "legacy-parent": {"parentBranch": "develop"}, - } { - err := k8sClient.Create(ctx, legacyTarget(name, spec)) - Expect(err).To(HaveOccurred(), "%s: the old field must not apply cleanly", name) - Expect(err.Error()).To(ContainSubstring("moved to a GitBranch; set spec.gitBranchRef"), name) - } - }) - It("requires spec.gitBranchRef", func() { ctx := context.Background() - target := legacyTarget("no-branch-ref", map[string]any{}) + target := unstructuredTarget("no-branch-ref", map[string]any{}) Expect(unstructured.SetNestedField(target.Object, nil, "spec", "gitBranchRef")).To(Succeed()) delete(target.Object["spec"].(map[string]any), "gitBranchRef") err := k8sClient.Create(ctx, target) @@ -319,35 +290,8 @@ var _ = Describe("Retained and refused GitTarget branch fields", func() { }) }) -// GitProvider.spec.receiver became the GitReceiver kind. A pruned field would apply cleanly and leave -// a provider that used to be notified silently unnotified, so the old spelling stays in the schema -// and a CEL rule refuses it, naming the replacement. -var _ = Describe("Retained and refused GitProvider receiver field", func() { - legacyProvider := func(name string, spec map[string]any) *unstructured.Unstructured { - spec["url"] = "https://github.com/acme/platform.git" - spec["allowedBranches"] = []any{"main"} - return &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": "configbutler.ai/v1alpha3", - "kind": "GitProvider", - "metadata": map[string]any{"name": name, "namespace": "default"}, - "spec": spec, - }} - } - - It("refuses spec.receiver in its old shape, or any other, naming the GitReceiver that replaces it", func() { - ctx := context.Background() - - for name, receiver := range map[string]any{ - "legacy-receiver": map[string]any{"type": "github", "secretRef": map[string]any{"name": "token"}}, - "empty-receiver": map[string]any{}, - } { - err := k8sClient.Create(ctx, legacyProvider(name, map[string]any{"receiver": receiver})) - Expect(err).To(HaveOccurred(), "%s: the old field must not apply cleanly", name) - Expect(err.Error()).To(ContainSubstring("spec.receiver was replaced by a GitReceiver"), name) - Expect(err.Error()).To(ContainSubstring("whose spec.url they name"), name) - } - }) - +// A GitReceiver's type and secretRef are both required, and type is a closed set. +var _ = Describe("GitReceiver admission", func() { It("requires a GitReceiver's type and secretRef, and refuses an unknown type", func() { ctx := context.Background() From 0b8334eb2070d6f855541caaf72324342e6913f6 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Sun, 25 Oct 2026 06:11:29 +0000 Subject: [PATCH 3/3] refactor(api): remove the retired eventTemplate and groupTemplate liveTemplate replaced both, and since then they have only been refused, by CEL at admission and by the GitTarget controller's Validated check. The migration release they were kept for has shipped, so the fields, both refusals, and the test that pinned the migration contract go. kubectl apply now refuses them as unknown fields; UPGRADING.md says so. Co-Authored-By: Claude Opus 5.5 (1M context) --- api/v1alpha3/gitprovider_types.go | 11 - .../crd/bases/configbutler.ai_gittargets.yaml | 14 -- docs/UPGRADING.md | 9 + docs/commit-messages.md | 2 +- .../controller/gittarget_commit_validation.go | 6 - .../gittarget_commit_validation_test.go | 16 -- .../controller/live_message_migration_test.go | 191 ------------------ 7 files changed, 10 insertions(+), 239 deletions(-) delete mode 100644 internal/controller/live_message_migration_test.go diff --git a/api/v1alpha3/gitprovider_types.go b/api/v1alpha3/gitprovider_types.go index 42a64857..6c05bd58 100644 --- a/api/v1alpha3/gitprovider_types.go +++ b/api/v1alpha3/gitprovider_types.go @@ -184,8 +184,6 @@ type CommitterSpec struct { } // CommitMessageSpec configures commit message formatting, on GitTarget.spec.commit.message. -// +kubebuilder:validation:XValidation:rule="!has(self.eventTemplate)",message="eventTemplate is retired; migrate to liveTemplate" -// +kubebuilder:validation:XValidation:rule="!has(self.groupTemplate)",message="groupTemplate is retired; migrate to liveTemplate" 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 @@ -220,15 +218,6 @@ type CommitMessageSpec struct { // renamed to ResourceVersion, is rejected. // +optional ReconcileTemplate string `json:"reconcileTemplate,omitempty"` - - // EventTemplate is retired and rejected. Move it to liveTemplate, reading resource fields from - // Resources. - // +optional - EventTemplate string `json:"eventTemplate,omitempty"` - - // GroupTemplate is retired and rejected. Move its value to liveTemplate. - // +optional - GroupTemplate string `json:"groupTemplate,omitempty"` } // CommitSigningSpec configures commit signing. diff --git a/config/crd/bases/configbutler.ai_gittargets.yaml b/config/crd/bases/configbutler.ai_gittargets.yaml index 8310931d..25187d0c 100644 --- a/config/crd/bases/configbutler.ai_gittargets.yaml +++ b/config/crd/bases/configbutler.ai_gittargets.yaml @@ -141,15 +141,6 @@ spec: description: Message sets the commit message templates. A template left empty uses the built-in one. properties: - eventTemplate: - description: |- - EventTemplate is retired and rejected. Move it to liveTemplate, reading resource fields from - Resources. - type: string - groupTemplate: - description: GroupTemplate is retired and rejected. Move its - value to liveTemplate. - type: string liveTemplate: description: |- LiveTemplate is the Go template for the message of a commit made from live edits, whatever @@ -177,11 +168,6 @@ spec: is rejected, and one that fails at commit time commits the message as written. type: string type: object - x-kubernetes-validations: - - message: eventTemplate is retired; migrate to liveTemplate - rule: '!has(self.eventTemplate)' - - message: groupTemplate is retired; migrate to liveTemplate - rule: '!has(self.groupTemplate)' window: description: |- Window sets when this GitTarget's commit windows close. A window opens on an author's first diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 4c264e62..ffd651c5 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -405,6 +405,15 @@ without `--receiver-bind-address` (chart value `servers.receiver.enabled=false`) (one has, but no branch worker writes that branch). A query that read `outcome="unmatched"` reads 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. + ## `allowAnySourceNamespace` is gone; `allowCrossNamespace.sources` replaces it **Breaking.** `ClusterProvider.spec.allowAnySourceNamespace` is no longer read. diff --git a/docs/commit-messages.md b/docs/commit-messages.md index 1164a408..540c1ca3 100644 --- a/docs/commit-messages.md +++ b/docs/commit-messages.md @@ -248,5 +248,5 @@ name for it. Per run, empty covers two different facts: an all-namespaces sweep type, and a cluster-scoped type that has no namespaces. No single word is true of both, so the honest rendering is to say nothing. -`eventTemplate` and `groupTemplate` are retired and rejected. Follow the +`eventTemplate` and `groupTemplate` are gone. Follow the [upgrade instructions](UPGRADING.md#one-live-commit-message-template) to migrate existing templates. diff --git a/internal/controller/gittarget_commit_validation.go b/internal/controller/gittarget_commit_validation.go index 01536042..0c5fdde7 100644 --- a/internal/controller/gittarget_commit_validation.go +++ b/internal/controller/gittarget_commit_validation.go @@ -26,12 +26,6 @@ func validateCommitConfig(target *configbutleraiv1alpha3.GitTarget) (bool, strin return true, "" } - if message := target.Spec.Commit.Message; message != nil { - if message.EventTemplate != "" || message.GroupTemplate != "" { - return false, "spec.commit.message.eventTemplate and groupTemplate are retired; migrate to liveTemplate" - } - } - config := gitpkg.ResolveCommitConfig(nil).WithTargetMessage(target.Spec.Commit.Message) if err := gitpkg.ValidateCommitConfig(config); err != nil { return false, fmt.Sprintf("invalid spec.commit.message: %v", err) diff --git a/internal/controller/gittarget_commit_validation_test.go b/internal/controller/gittarget_commit_validation_test.go index c1b5b7b6..db90dd66 100644 --- a/internal/controller/gittarget_commit_validation_test.go +++ b/internal/controller/gittarget_commit_validation_test.go @@ -48,22 +48,6 @@ func TestValidateCommitConfig(t *testing.T) { }, ok: true, }, - { - // A retired field is refused before any template is parsed, so a stored legacy value - // reports the migration instruction rather than a parse error about a dead field. - name: "a retired event template", - spec: &configbutleraiv1alpha3.GitTargetCommitSpec{ - Message: &configbutleraiv1alpha3.CommitMessageSpec{EventTemplate: "{{.Operation"}, - }, - saysA: "retired", - }, - { - name: "a retired group template", - spec: &configbutleraiv1alpha3.GitTargetCommitSpec{ - Message: &configbutleraiv1alpha3.CommitMessageSpec{GroupTemplate: "grouped: {{.Author}}"}, - }, - saysA: "retired", - }, { name: "an unparseable live template", spec: &configbutleraiv1alpha3.GitTargetCommitSpec{ diff --git a/internal/controller/live_message_migration_test.go b/internal/controller/live_message_migration_test.go deleted file mode 100644 index aeac466f..00000000 --- a/internal/controller/live_message_migration_test.go +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package controller - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/yaml" - - api "github.com/ConfigButler/gitops-reverser/api/v1alpha3" -) - -// applyIgnoringUnknownFields server-side applies obj as a client that ignores unknown fields. -// client.Client.Apply cannot express fieldValidation: Ignore, so the apply patch type is sent -// directly; the migration contract has to hold for exactly these lenient clients. -func applyIgnoringUnknownFields(ctx context.Context, c client.Client, obj *unstructured.Unstructured) error { - data, err := obj.MarshalJSON() - if err != nil { - return err - } - - return c.Patch(ctx, obj, client.RawPatch(types.ApplyPatchType, data), client.FieldOwner("migration-test"), - &client.PatchOptions{Raw: &metav1.PatchOptions{FieldValidation: "Ignore"}}) -} - -const crdBaseDir = "../../config/crd/bases" - -// legacyGitTarget is the shape a GitTarget had before liveTemplate replaced the two -// per-event templates: the documents this migration has to keep working for. -func legacyGitTarget(name string) *unstructured.Unstructured { - return &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": "configbutler.ai/v1alpha3", "kind": "GitTarget", - "metadata": map[string]any{"name": name, "namespace": "default"}, - "spec": map[string]any{ - "gitBranchRef": map[string]any{"name": "provider-main"}, - "kubeProviderRef": map[string]any{"name": "local"}, - "path": name, - "commit": map[string]any{ - "message": map[string]any{"eventTemplate": "old event", "groupTemplate": "old group"}, - }, - }, - }} -} - -// startEnv brings up an envtest apiserver holding exactly the given CRDs and returns a client. -// Passing the CRDs as objects rather than a directory is what lets a caller install a MODIFIED -// schema, without ever editing one that is already being served. -func startEnv(t *testing.T, crds ...*apiextv1.CustomResourceDefinition) client.Client { - t.Helper() - env := &envtest.Environment{} - if len(crds) == 0 { - env.CRDDirectoryPaths = []string{crdBaseDir} - env.ErrorIfCRDPathMissing = true - } else { - env.CRDs = crds - } - cfg, err := env.Start() - require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, env.Stop()) }) - sch := runtime.NewScheme() - require.NoError(t, api.AddToScheme(sch)) - require.NoError(t, apiextv1.AddToScheme(sch)) - c, err := client.New(cfg, client.Options{Scheme: sch}) - require.NoError(t, err) - - return c -} - -// gitTargetCRDWithoutMessageValidation reads the shipped GitTarget CRD and returns it with the -// retirement rules on spec.commit.message removed. -// -// The point is WHEN this happens: before the apiserver has ever seen the CRD, so the relaxed -// schema is the only one it ever compiles. Editing a live CRD instead means asking it to rebuild -// a serving handler, and under CPU pressure that rebuild is sometimes missed outright -- the new -// schema reads back correctly while the previously compiled one goes on validating, with nothing -// to retrigger it. Doing the edit here removes the rebuild from the test rather than waiting on -// one, which is not something a timeout can fix: measured under load, every run that converged -// did so within 6.3s and every run that failed used its entire budget, whether that was 10 -// seconds or 60. -func gitTargetCRDWithoutMessageValidation(t *testing.T) *apiextv1.CustomResourceDefinition { - t.Helper() - raw, err := os.ReadFile(filepath.Join(crdBaseDir, "configbutler.ai_gittargets.yaml")) - require.NoError(t, err) - var crd apiextv1.CustomResourceDefinition - require.NoError(t, yaml.Unmarshal(raw, &crd)) - for i := range crd.Spec.Versions { - root := crd.Spec.Versions[i].Schema.OpenAPIV3Schema - spec := root.Properties["spec"] - commit := spec.Properties["commit"] - msg := commit.Properties["message"] - require.NotEmpty(t, msg.XValidations, "shipped CRD no longer carries the rules this test relaxes") - msg.XValidations = nil - commit.Properties["message"] = msg - spec.Properties["commit"] = commit - root.Properties["spec"] = spec - } - - return &crd -} - -// TestLiveMessageValidation_RejectsLegacyAcceptsMigrated pins what the SHIPPED schema does: -// it refuses the pre-migration shape and accepts the migrated one. -// -// It runs against the CRD exactly as installed, and never edits it. That is the point. This -// assertion used to live at the end of the migration test below, which got there by stripping -// the validation and then putting it back -- and waiting for the restore to take effect is a -// race the apiserver loses often enough to matter. The apiextensions apiserver rebuilds a CRD's -// serving handler asynchronously after a spec write, and under CPU pressure that rebuild is -// sometimes missed outright: the new schema reads back correctly while the previously compiled -// one goes on validating, and nothing retriggers it. Measured under load: 571 polls across 60s -// with the restored rules present in the live CRD and still unenforced. A longer timeout does -// not help, because the wait is not slow, it is stuck -- every run that passed did so within -// 6.3s, and every run that failed used its whole budget. Asserting against an unmodified CRD -// removes the rebuild from the picture rather than waiting on it. -func TestLiveMessageValidation_RejectsLegacyAcceptsMigrated(t *testing.T) { - ctx := context.Background() - c := startEnv(t) - - err := applyIgnoringUnknownFields(ctx, c, legacyGitTarget("reject-legacy")) - require.Error(t, err) - require.Contains(t, err.Error(), "liveTemplate") - - migrated := legacyGitTarget("accept-migrated") - require.NoError(t, unstructured.SetNestedMap(migrated.Object, - map[string]any{"liveTemplate": "chore: sync {{.Count}}"}, "spec", "commit", "message")) - require.NoError(t, applyIgnoringUnknownFields(ctx, c, migrated)) -} - -func TestLiveMessageMigration_StoredValuesAndRemoval(t *testing.T) { - ctx := context.Background() - legacy := legacyGitTarget - - // Storing the pre-migration shape means getting it past the validation that forbids it. That - // relaxation is applied to the CRD BEFORE it is installed, so the apiserver compiles it once - // and nothing here ever waits on a schema change. Proving the SHIPPED schema still rejects - // this shape is TestLiveMessageValidation_RejectsLegacyAcceptsMigrated's job, against a CRD - // that is never modified at all. - c := startEnv(t, gitTargetCRDWithoutMessageValidation(t)) - for _, name := range []string{"migrate-apply", "migrate-patch"} { - require.NoError(t, applyIgnoringUnknownFields(ctx, c, legacy(name))) - } - for _, name := range []string{"migrate-apply", "migrate-patch"} { - var target api.GitTarget - key := client.ObjectKey{Name: name, Namespace: "default"} - require.NoError(t, c.Get(ctx, key, &target)) - ok, msg := validateCommitConfig(&target) - require.False(t, ok) - require.Contains(t, msg, "liveTemplate") - target.Status.Conditions = []metav1.Condition{ - { - Type: "Validated", - Status: metav1.ConditionFalse, - Reason: "InvalidConfig", - Message: msg, - ObservedGeneration: target.Generation, - LastTransitionTime: metav1.Now(), - }, - } - require.NoError(t, c.Status().Update(ctx, &target)) - if name == "migrate-apply" { - obj := legacy(name) - require.NoError( - t, - unstructured.SetNestedMap(obj.Object, map[string]any{"liveTemplate": "chore: sync {{.Count}}"}, - "spec", "commit", "message"), - ) - require.NoError(t, applyIgnoringUnknownFields(ctx, c, obj)) - } else { - patch := []byte(`{"spec":{"commit":{"message":{` + - `"eventTemplate":null,"groupTemplate":null,` + - `"liveTemplate":"chore: sync {{.Count}}"}}}}`) - require.NoError(t, c.Patch(ctx, &target, client.RawPatch(types.MergePatchType, patch), - &client.PatchOptions{Raw: &metav1.PatchOptions{FieldValidation: "Ignore"}})) - } - require.NoError(t, c.Get(ctx, key, &target)) - require.Equal(t, "chore: sync {{.Count}}", target.Spec.Commit.Message.LiveTemplate) - ok, msg = validateCommitConfig(&target) - require.True(t, ok, msg) - } -}