Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ resources:
kind: GitTarget
path: github.com/ConfigButler/gitops-reverser/api/v1alpha3
version: v1alpha3
- api:
crdVersion: v1
namespaced: true
controller: true
domain: configbutler.ai
kind: GitBranch
path: github.com/ConfigButler/gitops-reverser/api/v1alpha3
version: v1alpha3
- api:
crdVersion: v1
namespaced: true
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ agent over MCP.
2. **Sanitize** the change and compare it against what Git already holds.
3. **Write** stable YAML to the target folder and push, grouping a burst of changes into one commit.

Custom resources configure it: `GitProvider` for repository and credentials, `GitTarget` for branch
and folder, `WatchRule` or `ClusterWatchRule` for what to capture, and `ClusterProvider` for the
source cluster, created as `default` on install. A rule selects whole resource collections, including
their deletions; the `GitTarget`'s prune mode decides whether a deletion removes the file from Git.
Custom resources configure it: `GitProvider` for repository and credentials, `GitBranch` for one
branch of that repository, `GitTarget` for a folder on it, `WatchRule` or `ClusterWatchRule` for what
to capture, and `ClusterProvider` for the source cluster, created as `default` on install. A rule
selects whole resource collections, including their deletions; the `GitTarget`'s prune mode decides
whether a deletion removes the file from Git.
See [Configuration](docs/configuration.md).

By default it watches its own cluster. Add more of each as you need them: one operator can serve
Expand Down
2 changes: 2 additions & 0 deletions api/v1alpha3/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ func newExampleObject(kind string) any {
switch kind {
case "GitProvider":
return &GitProvider{}
case "GitBranch":
return &GitBranch{}
case "GitTarget":
return &GitTarget{}
case "ClusterProvider":
Expand Down
211 changes: 211 additions & 0 deletions api/v1alpha3/gitbranch_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// SPDX-License-Identifier: Apache-2.0

package v1alpha3

import (
meta "github.com/fluxcd/pkg/apis/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

// GitBranchSpec defines the desired state of GitBranch.
//
// Every field is immutable: together they are the repository branch this object claims, and a
// claim that could be repointed would move a branch worker's checkout, credential and pending
// writes to another branch under the GitTargets that reference it. Delete and recreate instead.
// +kubebuilder:validation:XValidation:rule="self.gitProviderRef == oldSelf.gitProviderRef",message="spec.gitProviderRef is immutable; delete and recreate the GitBranch to change it"
// +kubebuilder:validation:XValidation:rule="self.branch == oldSelf.branch",message="spec.branch is immutable; delete and recreate the GitBranch to change it"
// +kubebuilder:validation:XValidation:rule="has(self.parentBranch) == has(oldSelf.parentBranch) && (!has(self.parentBranch) || self.parentBranch == oldSelf.parentBranch)",message="spec.parentBranch is immutable; delete and recreate the GitBranch to change it"
type GitBranchSpec struct {
// A LocalObjectReference on purpose: creating a GitBranch then needs the right to create objects
// in the namespace that holds the credential, which is what makes a branch claim an act of the
// credential's owner. Other namespaces reach the branch through the GitProvider's accessFrom.

// GitProviderRef names the GitProvider, in this GitBranch's namespace, whose repository and
// credential this branch is written with. GitTargets in other namespaces may reference this
// GitBranch while that GitProvider's spec.accessFrom admits their namespace.
// 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.LocalObjectReference `json:"gitProviderRef"`

// Branch is the branch on the remote that this GitBranch writes. It must match one of the
// GitProvider's allowedBranches. Only one GitBranch writes a repository branch: the oldest 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.
// +required
// +kubebuilder:validation:MinLength=1
Branch string `json:"branch"`

// Why "parent" and not base/source/revision: docs/definitions.md reserves those words, and the
// field is flat because it is one fact. It decides what history a new write branch gets, and a
// 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 it does not need to be in the GitProvider's
// allowedBranches.
// Immutable, including adding or removing it: delete and recreate the GitBranch to change it.
// +optional
// +kubebuilder:validation:MinLength=1
ParentBranch string `json:"parentBranch,omitempty"`
}

// GitBranchStatus defines the observed state of GitBranch.
type GitBranchStatus struct {
// ObservedGeneration is the latest generation observed by the controller.
// +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.
// +optional
// +listType=map
// +listMapKey=type
// +patchMergeKey=type
// +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.
// +optional
Repository string `json:"repository,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.
// +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.
// +optional
GitTargetCount int32 `json:"gitTargetCount"`
}

// GitBranchRemoteStatus is the answer to "where is my branch, and when did we last prove it".
//
// It is sampled when the GitBranch reconciles, from what its branch worker last proved about the
// branch, and written at most once a minute. The first observation, and a correction after the
// repository changed under the branch, are written at once.
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.
// +optional
Commit string `json:"commit,omitempty"`

// LastVerifiedAt is when the remote was last observed.
// +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.
// +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.
// +optional
Parent *GitBranchParentStatus `json:"parent,omitempty"`
}

// GitBranchParentState is what a branch that does not exist yet would be created from.
// +kubebuilder:validation:Enum=Found;Missing;Unborn
type GitBranchParentState string

const (
// GitBranchParentFound means the parent branch exists; the first commit builds on its tip.
GitBranchParentFound GitBranchParentState = "Found"
// GitBranchParentMissing means the parent is not on the remote either, so the branch cannot be
// created; the GitBranch reports ParentBranchNotFound. The parent is spec.parentBranch, or, when
// that is omitted, the default branch of a repository that is not empty but whose HEAD names no
// branch it carries.
GitBranchParentMissing GitBranchParentState = "Missing"
// GitBranchParentUnborn means the repository is empty, with no refs at all (tags included), and
// no parent is configured, so the first commit starts the branch with no history.
GitBranchParentUnborn GitBranchParentState = "Unborn"
)

// 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).
// +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.
// +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.
// +optional
Commit string `json:"commit,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.gitProviderRef.name`
// +kubebuilder:printcolumn:name="Branch",type=string,JSONPath=`.spec.branch`
// +kubebuilder:printcolumn:name="Commit",type=string,JSONPath=`.status.remote.commit`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`,priority=1
// +kubebuilder:printcolumn:name="Repository",type=string,JSONPath=`.status.repository`,priority=1
// +kubebuilder:printcolumn:name="Targets",type=integer,JSONPath=`.status.gitTargetCount`,priority=1
// +kubebuilder:printcolumn:name="Verified",type=date,JSONPath=`.status.remote.lastVerifiedAt`,priority=1
// +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.
type GitBranch struct {
metav1.TypeMeta `json:",inline"`

// metadata is a standard object metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty,omitzero"`

// spec defines the desired state of GitBranch
// +required
Spec GitBranchSpec `json:"spec"`

// status defines the observed state of GitBranch
// +optional
Status GitBranchStatus `json:"status,omitempty,omitzero"`
}

// GitProviderKey is the GitProvider this GitBranch writes through: spec.gitProviderRef, in this
// GitBranch's own namespace.
func (b *GitBranch) GitProviderKey() types.NamespacedName {
return types.NamespacedName{Namespace: b.Namespace, Name: b.Spec.GitProviderRef.Name}
}

// +kubebuilder:object:root=true

// GitBranchList contains a list of GitBranch.
type GitBranchList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []GitBranch `json:"items"`
}

func init() {
SchemeBuilder.Register(&GitBranch{}, &GitBranchList{})
}
32 changes: 0 additions & 32 deletions api/v1alpha3/gitprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,22 +213,6 @@ type GitProviderStatus struct {
// Only populated when commit.signing is configured and a signing key is available.
// +optional
SigningPublicKey string `json:"signingPublicKey,omitempty"`

// Branches lists the branches this repository's GitTargets are configured to write, with how
// many reference each. It answers what the repository is being used for, so it includes a
// branch whose targets are blocked or suspended and excludes a branch nothing references any
// more.
//
// It is not the branches that exist on the remote, and not the workers currently running: it
// moves only when somebody edits a GitTarget. An EMPTY list beside Ready=True is the state
// this exists to make legible — configured and unused, which looks nothing like broken and
// previously took a GitTarget listing to tell apart. An ABSENT list is the third state and a
// different one: no inventory has been read yet, so nothing here has been measured.
// +optional
// +listType=map
// +listMapKey=name
Branches []GitProviderBranchStatus `json:"branches"`

// 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 —
Expand All @@ -247,22 +231,6 @@ type GitProviderStatus struct {
WebhookPath string `json:"webhookPath,omitempty"`
}

// GitProviderBranchStatus is one branch of this repository that GitTargets are configured to
// write, and how many of them write it.
type GitProviderBranchStatus struct {
// Name is the branch.
// +required
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`

// GitTargetCount is how many GitTargets are configured to write this branch. More than one means
// they share a branch worker, and each still reports its own folder's health on its own
// conditions.
// +required
// +kubebuilder:validation:Minimum=0
GitTargetCount int32 `json:"gitTargetCount"`
}

// CommitSpec configures the commit identity and signing a GitProvider uses. Message formatting
// lives on the GitTarget (spec.commit.message), because it describes the folder rather than the
// connection.
Expand Down
Loading
Loading