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
2 changes: 1 addition & 1 deletion .coverage-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
79.0
79.1
17 changes: 17 additions & 0 deletions api/v1alpha3/gittarget_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ type GitTargetSpec struct {
// +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 is mutable because it only says where a branch
// starts; changing it abandons nothing already written, unlike branch and path. It can never
// hold a commit or tag: a pinned starting point would be a sibling field.

// ParentBranch is the branch the write branch (spec.branch) is created from, and the branch the
// target compares against while the write 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 the write branch to be
// created; otherwise the GitTarget reports ParentBranchNotFound and writes nothing. Once the
// write 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. All GitTargets on one GitProvider
// and branch must name the same parent branch (omitted counts as its own value).
// +optional
// +kubebuilder:validation:MinLength=1
ParentBranch 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
Expand Down
12 changes: 12 additions & 0 deletions config/crd/bases/configbutler.ai_gittargets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,18 @@ spec:
- Ignore
- PushEmptyCommit
type: string
parentBranch:
description: |-
ParentBranch is the branch the write branch (spec.branch) is created from, and the branch the
target compares against while the write 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 the write branch to be
created; otherwise the GitTarget reports ParentBranchNotFound and writes nothing. Once the
write 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. All GitTargets on one GitProvider
and branch must name the same parent branch (omitted counts as its own value).
minLength: 1
type: string
path:
description: |-
Path within the repository to write resources to, relative to the repository
Expand Down
14 changes: 10 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ Key fields:
- `spec.gitProviderRef`: a `GitProvider` in the same namespace, by name.
- `spec.clusterProviderRef`: a cluster-scoped source `ClusterProvider`; it defaults to `{name: default}`.
- `spec.branch`: immutable branch, validated against `GitProvider.spec.allowedBranches`.
- `spec.parentBranch`: optional, mutable branch the write branch is created from and compared
against while it does not exist. Omitted means the remote's default branch. An explicit parent
must exist when creating the write branch. It does not need to be in `allowedBranches` because
it is only read.
- `spec.path`: immutable, required path under the repo (`MinLength=1`; `.` means repo root and must be
chosen explicitly).
- `spec.encryption`: optional SOPS/age encryption settings for sensitive resources.
Expand All @@ -372,7 +376,8 @@ resolves the window per open window, since a window is bound to exactly one targ

`gitProviderRef`, `clusterProviderRef`, `branch`, and `path` are immutable so a target cannot silently
orphan an old materialization or change its source cluster. The controller also rejects path overlaps
between GitTargets sharing a provider and branch.
between GitTargets sharing a provider and branch, and rejects two GitTargets on one branch that name
different parent branches.

Status has a kstatus-compatible summary layer plus domain conditions:

Expand Down Expand Up @@ -1240,9 +1245,10 @@ A branch whose remote has been **deleted** takes the same path. The advertisemen
branch at all, which is reported as a moved remote with a zero hash, so the replay re-roots on the remote's
default branch and the retry re-creates the branch with the retained writes on top.

**A branch that does not exist yet** is created from the remote's default branch (its parent) only
when a write has something to commit, so an idle or in-sync target never creates it. Until then the
worker compares the folder against the parent. Periodic refresh, ten minutes by default, follows the
**A branch that does not exist yet** is created from its parent (`spec.parentBranch`, or the
remote's default branch when that is omitted) only when a write has something to commit, so an
idle or in-sync target never creates it. Until then the worker compares the folder against the
parent. Periodic refresh, ten minutes by default, follows the
parent from the same advertisement and updates the idle view independently of the publication
check. The push records the parent and the commit it started from as its root, and treats a moved
parent like a moved branch: fetch, reset, replay, check again. A branch somebody else created in the
Expand Down
31 changes: 31 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ The destination fields are immutable: to move a target, delete it and create a n
|---|---|---|
| `gitProviderRef` | **required** | The `GitProvider` backing this target, in the same namespace |
| `branch` | **required** | Branch to write. Must be in the provider's `allowedBranches` |
| `parentBranch` | the remote's default branch | Branch a `branch` the remote does not have yet is created from. Mutable, and only read, so it need not be allowed. See [starting the write branch from another branch](#starting-the-write-branch-from-another-branch-specparentbranch) |
| `path` | **required** | Folder within the repository. `.` targets the root; empty is rejected |
| `clusterProviderRef` | `{"name":"default"}` | Source cluster to mirror from. The default names a `ClusterProvider` called `default` |
| `commit.window.idleTimeout` | `5s` | Close a commit window after this much silence. `0s` commits every write on its own. See [the commit window](#the-commit-window-speccommitwindow) |
Expand Down Expand Up @@ -551,6 +552,8 @@ The important fields are:
- `spec.clusterProviderRef`: which `ClusterProvider` supplies resources; omit it to reference the
user-created `default` provider
- `spec.branch`: which allowed branch to write to
- `spec.parentBranch`: which branch `spec.branch` is created from while it does not exist yet; omit it
for the remote's default branch
- `spec.path`: required relative path inside the repository; use `.` only when you deliberately
want the repository root
- `spec.encryption`: how `Secret` resources should be encrypted before commit
Expand Down Expand Up @@ -747,6 +750,34 @@ two are always separate objects, and deleting the preview cannot disturb the rea

For inspecting a repository without a cluster at all, use the `manifest-analyzer` CLI.

### Starting the write branch from another branch (`spec.parentBranch`)

A target whose `spec.branch` does not exist on the remote waits on standby: it compares the cluster
against the **parent branch** and creates nothing while they agree. The first write that has
something to commit creates the branch from the parent's current tip, checked when it pushes, so a
target that waited a week does not publish onto a week-old checkout. A write that finds nothing to
change creates no branch at all.

The parent is the remote's default branch unless you name one:

```yaml
spec:
gitProviderRef: {name: homelab}
branch: reverser/prod-edits # the write branch: created on the first commit
parentBranch: env/prod # what it is created from
path: apps/checkout
```

- An explicit parent must exist when the write branch is created. Otherwise the target reports
`Ready=False` with reason `ParentBranchNotFound`, writes nothing, and recovers once the branch is
pushed. An omitted parent never fails this way, and in an empty repository the first commit
starts a branch with no history.
- Once the write branch exists, it is used as it is: new commits on the parent do not reach it.
Delete the write branch after its changes are merged, and the next edit starts a fresh one.
- `parentBranch` is mutable, and it is only read, so it does not need to be in `allowedBranches`.
- All `GitTarget` objects on one `GitProvider` and branch must name the same parent branch; omitted
counts as its own value. The later-created one reports `TargetConflict`.

`spec.suspend` is **not** this. It is the next section.

### Stopping a target from writing (`spec.suspend`)
Expand Down
9 changes: 9 additions & 0 deletions docs/definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ Observation failure supplies no evidence of absence. The code calls this write-s
branch)` tuple. Every write to that branch goes through it. Two words, lowercase in prose,
`BranchWorker` as the Go type.

**Write branch.** The branch a `GitTarget` writes to: `spec.branch`. It must be in the
`GitProvider`'s `allowedBranches`.

**Parent branch.** The branch a write branch is created from, and compared against while the write
branch does not exist on the remote: `spec.parentBranch`, or the remote's default branch when that
is omitted. Always with its noun: "parent" alone already names a parent resource. Not "base", which
names a kustomize read-only base and base trust, and not "source", which names the source cluster.
Once the write branch exists, the parent is not followed.

**Commit window.** The interval a branch worker batches one author's writes to one target over before
it commits. Only a write opens one; nothing else does. It closes on its `idleTimeout` of silence or
its `maxDuration` after opening, whichever comes first. Windows do not merge across authors, which
Expand Down
6 changes: 6 additions & 0 deletions hack/crdfields/fields.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ kinds:
doc: The `GitProvider` backing this target, in the same namespace
- path: branch
doc: Branch to write. Must be in the provider's `allowedBranches`
- path: parentBranch
default: the remote's default branch
doc: >-
Branch a `branch` the remote does not have yet is created from. Mutable, and only read, so
it need not be allowed. See
[starting the write branch from another branch](#starting-the-write-branch-from-another-branch-specparentbranch)
- path: path
doc: Folder within the repository. `.` targets the root; empty is rejected
- path: clusterProviderRef
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/condition_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ func conditionIsTrue(conditions []metav1.Condition, conditionType string) bool {
func conditionIsFalse(conditions []metav1.Condition, conditionType string) bool {
return apimeta.IsStatusConditionFalse(conditions, conditionType)
}

// readyReasonIs reports whether the Ready condition carries the given reason.
func readyReasonIs(conditions []metav1.Condition, reason string) bool {
ready := apimeta.FindStatusCondition(conditions, ConditionTypeReady)
return ready != nil && ready.Reason == reason
}
Loading
Loading