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..6c05bd58 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,38 @@ 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. -// +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" +// CommitMessageSpec configures commit message formatting, on GitTarget.spec.commit.message. 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 +203,31 @@ 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. - // +optional - EventTemplate string `json:"eventTemplate,omitempty"` - - // GroupTemplate is retired. Remove it and copy 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 +242,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 +252,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..25187d0c 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,95 +131,55 @@ 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. - type: string - groupTemplate: - description: GroupTemplate is retired. Remove it and copy - 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: - - 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 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 +189,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 +205,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 +244,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 +267,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 +285,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 +311,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 +396,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 +432,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 +507,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 +536,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 +583,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..ffd651c5 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 @@ -406,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/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/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/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/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) - } -} 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() 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