Repository navigation
Expand file tree
/
Copy pathtypes.go
More file actions
1102 lines (1009 loc) · 53.5 KB
/
Copy pathtypes.go
File metadata and controls
1102 lines (1009 loc) · 53.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: Apache-2.0
package git
import (
"errors"
"fmt"
"net/url"
"sort"
"strings"
"time"
gogit "github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
k8stypes "k8s.io/apimachinery/pkg/types"
v1alpha3 "github.com/ConfigButler/gitops-reverser/api/v1alpha3"
"github.com/ConfigButler/gitops-reverser/internal/git/manifestedit"
"github.com/ConfigButler/gitops-reverser/internal/manifestanalyzer"
"github.com/ConfigButler/gitops-reverser/internal/repourl"
"github.com/ConfigButler/gitops-reverser/internal/types"
)
// AttributionOutcome records what happened when the operator tried to name the actor behind a
// change. It is carried EXPLICITLY rather than inferred from the author identity, because the
// author string is load-bearing in several places (window grouping, commit-message templates,
// the author_kind metric) and overloading it to also mean "attribution failed" made a silent
// failure indistinguishable from correct configured-author behaviour. See
// docs/architecture.md#author-and-committer-identity-in-git.
type AttributionOutcome string
const (
// AttributionNotAttempted is configured-author mode: attribution is switched off, so the
// committer legitimately IS the author and no actor was ever sought.
//
// Deliberately the EMPTY string so it is also the ZERO VALUE: the many paths that never assign
// Attribution (reconcile, resync, bootstrap) all mean exactly this. Any other value makes the
// zero value a silent fourth state, which is the bug that stopped every CommitRequest
// attaching. TestAttributionZeroValueIsNotAttempted pins it.
AttributionNotAttempted AttributionOutcome = ""
// AttributionResolved means an audit fact named the actor.
AttributionResolved AttributionOutcome = "resolved"
// AttributionUnresolved means attribution ran and did not arrive at an actor.
//
// "Unresolved", not "failed": no fact produced (correct; not every change has a human actor),
// a cancelled wait, a read error and a malformed value all return the same not-found, so
// calling it a failure would assert a fault the operator cannot prove.
AttributionUnresolved AttributionOutcome = "unresolved"
)
// NamesActor reports whether the outcome carries an actor to compare against.
//
// The ONLY distinction that survives across subsystem boundaries: --author-attribution and
// --admission-webhook are configured independently, so two producers can disagree about the enum
// while agreeing about whether there is an actor. Comparing enums across that boundary couples the
// two flags; comparing NamesActor does not. Within one subsystem the enum IS compared directly.
func (o AttributionOutcome) NamesActor() bool {
return o == AttributionResolved
}
// UnresolvedAuthor is the identity written to the Git AUTHOR HEADER when attribution ran and
// did not resolve an actor. It exists so an unresolved attribution is visible in `git log`
// instead of being indistinguishable from a configured-author commit.
//
// Scope: the git author header and nothing else. DERIVED at the write path from the carried
// outcome, never stamped onto an Event, so it does NOT reach window grouping, message bodies, or
// {{.Username}} templates — pushing a magic token there would change commit text in every existing
// deployment and force user templates to handle a value they never had.
//
// Three strings because the header needs all three: Username is the greppable machine token,
// DisplayName is what a human reads, Email uses the RFC 2606 .invalid TLD so it never routes mail.
func UnresolvedAuthor() UserInfo {
return UserInfo{
Username: UnresolvedAuthorUsername,
DisplayName: UnresolvedAuthorDisplayName,
Email: UnresolvedAuthorEmail,
}
}
const (
// UnresolvedAuthorUsername is the stable machine token for an unresolved attribution.
UnresolvedAuthorUsername = "attribution-unresolved"
// UnresolvedAuthorDisplayName is the human-facing git author name.
UnresolvedAuthorDisplayName = "unknown (attribution unresolved)"
// UnresolvedAuthorEmail is a reserved-invalid address (RFC 2606).
UnresolvedAuthorEmail = "attribution-unresolved@gitops-reverser.invalid"
)
const (
// DefaultCommitterName matches the default operator identity in Git history.
DefaultCommitterName = "GitOps Reverser"
// DefaultCommitterEmail matches the default operator email in Git history.
DefaultCommitterEmail = "noreply@configbutler.ai"
// DefaultReconcileCommitMessageTemplate names the synced type AND, when the run covered one,
// the namespace, so the otherwise indistinguishable per-collection reconciles one GitTarget
// produces are self-describing: a target watching configmaps in team-a and in team-b would
// otherwise write two byte-identical subjects. Plural resource alone for readability; add
// {{.APIVersion}} when plural collisions matter.
//
// The {{if}} guards fall back to "chore: reconcile N resources" for a whole-target reconcile,
// so the subject never degrades to an identity-less "chore: reconcile N ". The namespace guard
// in particular must stay an {{if}} rather than a sentinel: an empty Namespace here means the
// run was not namespace-scoped, which covers BOTH an all-namespaces sweep of a namespaced type
// and a cluster-scoped type having no namespaces at all. No single word is true of both, so
// the honest rendering of "no namespace to name" is to say nothing. (Contrast ResourceRef,
// which describes ONE resource, where empty has exactly one meaning and carries the
// types.ClusterScopeSegment sentinel.)
DefaultReconcileCommitMessageTemplate = "chore: reconcile {{.Count}} " +
"{{if .Resource}}{{.Resource}}{{else}}resources{{end}}" +
"{{if .Namespace}} in {{.Namespace}}{{end}}" +
"{{if .ResourceVersion}} (last resourceVersion: {{.ResourceVersion}}){{end}}"
// DefaultLiveCommitMessageTemplate describes retained input resources.
DefaultLiveCommitMessageTemplate = "chore: sync {{.Count}} resource{{if ne .Count 1}}s{{end}}\n\n" +
"{{range .Resources -}}" +
"- [{{.Operation}}] {{.APIVersion}}/{{.Resource}}/{{.Namespace}}/{{.Name}}\n" +
"{{end -}}"
resourceRefStringPartCap = 5
)
// CommitFile represents a single file to be committed.
type CommitFile struct {
Path string
Content []byte
}
// BranchInfo contains information about a Git branch.
type BranchInfo struct {
ShortName string // e.g., "main"
Sha string // commit hash, normally the tip of the default branch. But will be empty ("") for an unborn branch that is going to be orphaned branch (if the default branch does not exist)
Unborn bool // Is true for branches that don't have commits yet: only HEAD is configured to it
}
// RepoInfo represents high-level repository information.
type RepoInfo struct {
DefaultBranch *BranchInfo
RemoteBranchCount int
}
// PullReport provides detailed pull operation results.
type PullReport struct {
ExistsOnRemote bool // Branch exists on remote
HEAD BranchInfo
IncomingChanges bool // SHA changed, requiring resource-level reconcile
// ParentBranch is the branch checked out instead, when the target branch is absent: the parent
// it would be created from. HEAD.Sha is then the parent's commit. Empty otherwise.
ParentBranch string
}
// BranchKey is the branch a worker owns: one branch of one repository, the repository compared as
// repourl does, so two spellings of one URL are one key. It is the unit of worker ownership: every
// write to a repository branch goes through the one worker under its key, which pushes with the
// credential of the one GitProvider that owns the branch (branch_owner.go). Many GitTargets can
// share it, writing different paths.
type BranchKey struct {
// Repository is the repository's URL, normalized by repourl.
Repository string
// Branch is the Git branch name.
Branch string
}
// BranchKeyFor is the key of a branch of the repository at url. An empty url names no repository,
// and its zero key matches nothing.
func BranchKeyFor(url, branch string) BranchKey {
normalized := repourl.Normalize(url)
if normalized == "" {
return BranchKey{}
}
return BranchKey{Repository: normalized, Branch: branch}
}
// String returns a string representation for logging and debugging.
// Format: "repository@branch".
func (k BranchKey) String() string {
return k.Repository + "@" + k.Branch
}
// OlderGitTarget reports whether a was created before b, ties broken by namespace and name, so every
// caller orders two GitTargets the same way.
func OlderGitTarget(a, b *v1alpha3.GitTarget) bool {
at, bt := a.CreationTimestamp.Time, b.CreationTimestamp.Time
if !at.Equal(bt) {
return at.Before(bt)
}
return a.Namespace+"/"+a.Name < b.Namespace+"/"+b.Name
}
// WritesThrough reports whether target writes through gitBranch: the GitBranch a branch worker
// serves.
func WritesThrough(target *v1alpha3.GitTarget, gitBranch k8stypes.NamespacedName) bool {
return gitBranch.Name != "" && target.GitBranchKey() == gitBranch
}
// RepoIdentity is WHICH REPOSITORY a branch worker's clone, base trust and observations are
// about, down to the GitProvider object it was made for. BranchKey names the repository by URL,
// but not that object: a GitProvider recreated under its name, for the same repository, is the same
// key and another identity, and the worker is replaced.
//
// The UID carries the identity because spec.url is immutable, so within the lifetime of one
// GitProvider object the repository cannot change: a new object is the only way to reach a new
// one, and that mints a new UID. The URL rides along as a safety net for an immutability rule
// that was bypassed — a CRD reinstalled without the CEL rule, or a write straight to etcd. It is
// compared EXACTLY, with no normalization, because a re-spelled URL can only arrive with a
// recreate, which the UID has already caught.
//
// It is deliberately NOT part of BranchKey. The event router resolves a worker from a GitTarget by
// its GitProvider and branch (WritesThrough), with no GitProvider read on the per-event path, and a
// key carrying the UID would make a recreated GitProvider a second live worker on one branch.
type RepoIdentity struct {
// ProviderUID is the GitProvider object's metadata.uid.
ProviderUID k8stypes.UID
// URL is the GitProvider's spec.url, exactly as it is written there.
URL string
}
// IsZero reports that nothing is known about the repository. The CLI and tests that never touch a
// remote build workers this way, and a comparison against an unknown identity proves nothing.
func (r RepoIdentity) IsZero() bool { return r.ProviderUID == "" && r.URL == "" }
// String is for logs: enough to tell two repositories apart without printing a credential.
//
// spec.url is validated for length and nothing else, so it can carry userinfo
// (https://user:token@host/repo.git), and a replacement logs both identities at default
// verbosity. The userinfo is stripped rather than the whole URL withheld, because telling two
// repositories apart is the entire job of this line. A URL that will not parse is printed as
// written: it reached no remote, so it holds no credential a remote accepted.
func (r RepoIdentity) String() string {
shown := r.URL
if parsed, err := url.Parse(r.URL); err == nil && parsed.User != nil {
parsed.User = nil
shown = parsed.String()
}
return fmt.Sprintf("%s (uid %s)", shown, r.ProviderUID)
}
// UserInfo contains relevant user information for commit messages.
type UserInfo struct {
Username string
UID string
// DisplayName is the human-readable name from the OIDC "name" claim, when
// the audit event carries it. Empty means "fall back to Username".
DisplayName string
// Email is the address from the OIDC "email" claim, when the audit event
// carries it. Empty means "fall back to ConstructSafeEmail(Username)".
Email string
}
// WriteRequest is the unit of work queued and written by the BranchWorker: live events, which
// stream through the commit window. Each event names its own GitTarget.
type WriteRequest struct {
Events []Event
}
// gitTarget is the GitTarget the request's first event writes for, as namespace/name, for logs.
func (r *WriteRequest) gitTarget() string {
if r == nil || len(r.Events) == 0 {
return ""
}
return r.Events[0].GitTargetNamespace + "/" + r.Events[0].GitTargetName
}
// PendingWriteKind distinguishes the durable write shapes retained until push.
type PendingWriteKind string
const (
// PendingWriteCommit is one finalized commit-shaped live-event window.
PendingWriteCommit PendingWriteKind = "grouped_window"
// PendingWriteResync is a streaming-snapshot resync (M8): it carries the COMPLETE
// desired resource set for one GitTarget, and the worker materialises it with a
// content-derived mark-and-sweep against the worktree (upsert every desired
// resource, drop every watched managed document the snapshot did not contain).
PendingWriteResync PendingWriteKind = "resync"
// PendingWriteRequestRecord carries no content. It records a CommitRequest's message
// in an empty commit when the request ran out of time to attach, and asked for that with
// whenNothingToCommit: CommitEmpty. It is not a window: only writes open windows.
PendingWriteRequestRecord PendingWriteKind = "request_record"
)
type pendingTargetKey struct {
Name string
Namespace string
}
// ResolvedTargetMetadata is the target-scoped planning data retained with a
// pending write so replay does not re-fetch mutable GitTarget state.
type ResolvedTargetMetadata struct {
Name string
Namespace string
Path string
BootstrapOptions pathBootstrapOptions
EncryptionConfig *ResolvedEncryptionConfig
// Placement is the GitTarget's declared new-file placement policy, resolved
// from spec.placement. Nil when the GitTarget declares none, in which case new
// resources are placed beside the folder's one kustomize root, if it has exactly one,
// and otherwise at the built-in path.
Placement *manifestanalyzer.PlacementPolicy
// Namespaces is the GitTarget's declared namespace behavior — spec.serializeNamespace and the
// source namespaces reaching the target — which decides whether the documents this target
// writes carry metadata.namespace, and which namespace a namespace-free one belongs to. It is
// resolved with the rest of the metadata so a replayed write honours the policy it was planned
// under, exactly as PruneMode and Suspend are.
Namespaces namespacePolicy
// CommitMessage is the GitTarget's spec.commit.message, verbatim and possibly nil. It is
// overlaid onto the provider-resolved CommitConfig so a commit is phrased by the folder it
// writes to rather than by the connection it travels over. Resolved with the rest of the
// target's mutable state, so a write replayed after a rebase is phrased by the policy it was
// planned under.
CommitMessage *v1alpha3.CommitMessageSpec
// PruneMode is the GitTarget's EFFECTIVE spec.prune.mode — always a concrete value,
// because it is resolved through EffectivePruneMode and an omitted policy is onEvent.
// It gates both deletion paths: the resync mark-and-sweep (through the planner's
// SweepMode) and the steady-state DELETE-event writer.
//
// Retained on the pending write so a replay after a rebase is not re-planned under a LOOSER
// policy than it was planned against. Not frozen, though: tightenPendingPruneModes lowers it
// when the current policy is stricter, because tightening exists to stop deletions that have
// not landed yet.
PruneMode v1alpha3.PruneMode
// SourceCluster is the source cluster the GitTarget mirrors from —
// (api/v1alpha3).GitTarget.SourceCluster(), the referenced KubeProvider's namespace/name.
// The resync mark-and-sweep resolves this subtree's
// documents' GVK->GVR against that cluster's registry, so a folder mirroring a remote is swept
// against the right cluster's mapping.
SourceCluster string
// Applied is whether the GitTarget names an applier claim: an applier applies its folder, which
// must then exist before it is asked to. A resync into a folder that does not exist yet commits
// the folder's bootstrap files even when it holds no object (executeResyncPendingWrite).
Applied bool
// Suspend suppresses the WRITE only; the scan still runs, which keeps a suspended target's
// status fresh. Being CAPTURED defines the cutover: a suspension arriving after this write was
// planned does not retract it, and a commit already made locally is still pushed. Reading the
// live GitTarget at push time would strand that commit, to resurface out of order on resume.
Suspend bool
}
// PendingWrite is the unit retained until a push succeeds.
type PendingWrite struct {
Kind PendingWriteKind
Events []Event
CommitMessage string
CommitConfig CommitConfig
Signer gogit.Signer
GitTargetName string
GitTargetNamespace string
Targets map[pendingTargetKey]ResolvedTargetMetadata
ByteSize int64
// Desired is the complete desired resource snapshot, set only for a
// PendingWriteResync. The worker folds it over the worktree's content-derived
// store to produce the resync plan (upserts + mark-and-sweep drops).
Desired []manifestanalyzer.DesiredResource
// Scope, when set, restricts the resync's mark-and-sweep to one type's
// (group, resource) and optionally to one namespace: the M12 per-type
// reconcile/sweep. Desired then carries only that scope's objects (empty for a pure
// sweep), and no sibling type's — nor, for a namespace-scoped resync, any sibling
// namespace's — document is ever dropped. Nil is the whole-GitTarget resync.
Scope *ResyncScope
// ResourceVersion is the cluster snapshot's resourceVersion the desired set is pinned to
// (the joined streaming-watch bookmark) — the COLLECTION's version, not any one object's.
// Carried for diagnostics, logging, and the reconcile commit message.
//
// Not "Revision": in this codebase a revision is a Git commit (see LayoutReport.Revision).
ResourceVersion string
// ResyncStats, when non-nil, is populated during apply with the plan's
// create/update/delete/skip counts so a synchronous caller can report them.
ResyncStats *ResyncStats
// Committed, when non-nil, is set true during apply iff the resync produced a
// commit. A no-op resync (e.g. an empty initial snapshot) must not be retained or
// pushed: doing so would advance the push cooldown and delay the next real
// snapshot's push past its window.
Committed *bool
// AllowEmpty makes a grouped window commit even when its events changed nothing, because the
// CommitRequest attached to it asked for whenNothingToCommit: CommitEmpty. A suspended target
// still writes nothing.
AllowEmpty bool
// RequestAuthor and RequestAttribution author a PendingWriteRequestRecord, which has no
// events to take an author from: they are the request submitter's, matched the same way a
// window's author is.
RequestAuthor UserInfo
RequestAttribution AttributionOutcome
// emptyCommitted is stamped by executePendingWrites, the way CommitSHA is: the commit this
// write made changed no file. A request riding it resolves with the cause (AlreadyPresent or
// NoOpenWindow) and the commit, so a caller can tell "saved your changes" from "recorded your
// message". Re-stamped on a rebase-replay, where the same write may now change files.
emptyCommitted bool
// CommitRequest, when set, is the CommitRequest attached to this write: it is
// resolved Committed (with CommitSHA) once this write is pushed. It rides the write through the
// push cooldown and the conflict rebase-replay, so the result follows the data.
CommitRequest *commitRequestID
// committedMessageSource is the message source this write actually committed under, stamped by
// executePendingWrites the way CommitSHA is and for the same reason: publishCommitsForPush runs
// after the push, and recomputing the source from the write cannot know that a requestTemplate
// render failed at commit time. Zero means "not stamped", so messageSource() recomputes.
committedMessageSource messageResolution
// actionsDecided is set once the write was planned on the tree it was first committed on. A
// write carries the actions that plan decided, never a picture of the cluster to plan again
// (docs/design/applier-echoes.md, F2): from then on Events holds only the objects whose plan
// changed that tree, except in a save (CommitRequest), which keeps every object it captured,
// and a resync carries resyncActions instead of its snapshot. A replay applies those actions to
// the newer tip, and plans nothing else again. Loop-goroutine only.
actionsDecided bool
// resyncActions is what a resync decided on its tree: the upserts that changed it and the
// documents its sweep removed. Nil until decided. See actionsDecided.
resyncActions *resyncActions
// ownedElsewhere names, by objectKey, the snapshot objects another applier owns, which a resync
// into a coordinated folder neither upserts nor sweeps (apply_ownership.go).
ownedElsewhere map[string]bool
// committedOnce is set once the loop has committed this write at least once. It stays set after
// a reset discards that commit: whether the checkout holds it now is checkoutApplied's to say. A
// decided write the loop could not commit yet (the remote was unreachable) stays pending with it
// unset; such writes always form the tail of the pending writes. Loop-goroutine only.
committedOnce bool
// replayRefusal is the refusal the last completed replay met for this write: the tree the write
// was replayed onto refuses it, so the replay skipped it and the checkout does not hold it. The
// loop settles it out of the pending writes (settleReplayRefusals). Stamped by replayPendingWrites.
replayRefusal error
// charge is what keeping this write was charged against the retained-byte budget when it was
// decided, and is refunded when it stops being pending. See retainedCharge. Loop-goroutine only.
charge int64
// decidedAt is when the write was decided, which dates the oldest kept write. Loop-goroutine
// only.
decidedAt time.Time
// decision numbers the write in the order the loop decided it: the flush an apply waits for is the
// writes up to a mark (serviceAppliers). Loop-goroutine only.
decision uint64
// origin is what the write's outcome is settled against. See writeOrigin.
origin writeOrigin
// CommitSHA is the hash of the commit this write created, captured in
// executePendingWrite and refreshed when the write is re-executed on a
// rebase-replay (so it is never a stale pre-rebase hash). Zero when the write
// produced no commit (no diff).
CommitSHA plumbing.Hash
}
// WorkItem is the unit of work in the BranchWorker queue. Exactly one of
// Request, Attach, Resync, or Refresh is set.
type WorkItem struct {
// Request is a resource-write request.
Request *WriteRequest
// Attach is a CommitRequest attach: bind a message to the author's window and
// let its timers close that window.
Attach *AttachCommitRequest
// Resync is the FIFO position of a streaming-snapshot resync request (M8): a synchronous
// request/reply that materialises a GitTarget's complete desired set.
Resync *resyncMarker
// Refresh asks the worker to re-prove where its branch is on the remote. It is the only
// work item that never writes anything: see RefreshRequest.
Refresh *RefreshRequest
// Withdraw cancels a CommitRequest the worker has not acted on yet. It rides the same FIFO as
// the attach, so it is always handled after every attach sent before it.
Withdraw *AttachCommitRequest
// ApplyBarrier is one answer to the barrier an apply window took (apply_barrier.go). A stream's
// answer rides the FIFO behind every event that stream routed before it.
ApplyBarrier *ApplyBarrier
// charge is what a write or an attach was charged against the intake budget at enqueue, and is
// released once the loop has handled it. A resync's is on its marker, because coalescing can
// change it while the marker waits. See intake.go.
charge int64
}
// ResyncScope restricts a resync's mark-and-sweep to the slice of the mirror the desired
// snapshot was actually gathered over: one collection, and the served version that collection was
// gathered at.
//
// The invariant: THE SWEEP SCOPE MUST BE EXACTLY THE SCOPE THE DESIRED SET WAS GATHERED OVER.
// Narrower deletes documents that were never in scope; wider silently leaves documents unmanaged.
// The namespace lives inside the collection so a per-namespace replay cannot reach the sweep carrying
// only its type — a replay of one namespace once swept every other namespace's documents.
type ResyncScope struct {
// Collection is the sweep boundary and the scope's identity: group, resource, namespace.
Collection types.CollectionKey
// Version is the served version the desired set was gathered at. It is DATA, not
// identity: it renders the reconcile commit message's {{.APIVersion}} and names the
// version a snapshot came from in logs, and it is deliberately absent from the collection
// key, so a scope always round-trips to the boundary it sweeps (types.CollectionKey).
Version string
}
// ResyncScopeFor builds a scope from the served GVR a snapshot was gathered with and the
// namespace it was gathered in. It is the only constructor: going through it is what keeps
// the version on the data side of the type and out of the identity.
func ResyncScopeFor(gvr schema.GroupVersionResource, namespace string) ResyncScope {
return ResyncScope{Collection: types.CollectionKeyFor(gvr, namespace), Version: gvr.Version}
}
// GVR reconstructs the served GroupVersionResource this scope was gathered with, for the
// callers that must talk to the API machinery in its own terms.
func (s *ResyncScope) GVR() schema.GroupVersionResource {
if s == nil {
return schema.GroupVersionResource{}
}
return schema.GroupVersionResource{Group: s.Collection.Group, Version: s.Version, Resource: s.Collection.Resource}
}
// String renders the scope for logs and status messages. It is nil-safe: a nil
// scope is the whole-GitTarget resync and renders empty.
func (s *ResyncScope) String() string {
if s == nil {
return ""
}
return s.Collection.String()
}
// Matches reports whether a resolved resource identity falls inside this scope. A nil scope
// matches everything (whole-GitTarget resync). An empty namespace matches every namespace
// for the type.
func (s *ResyncScope) Matches(ri types.ResourceIdentifier) bool {
if s == nil {
return true
}
return s.Collection.Matches(ri)
}
// Overlaps reports whether two scopes' sweep boundaries can hold the same document. A nil scope is
// the whole GitTarget and overlaps every scope.
func (s *ResyncScope) Overlaps(other *ResyncScope) bool {
if s == nil || other == nil {
return true
}
return s.Collection.Overlaps(other.Collection)
}
// ApplyState is where a branch worker's appliers stand, as a snapshot's collection reads it when it
// starts (docs/design/applier-echoes.md, F3).
type ApplyState struct {
// Epoch is raised at every send of an apply attempt and at every end of one.
Epoch uint64
// GitAhead is whether Git holds a commit the cluster may not run yet: an applier on the branch
// has a ForeignCommit, Repair or Refusal pending, a converge due, or an attempt out.
GitAhead bool
}
// ResyncRequest is a synchronous resync of one GitTarget against a complete,
// version-pinned desired snapshot (M8). It rides the worker queue so the single
// git-mutating goroutine applies it in order with live events, and replies on
// Result once the local commit is created. The desired set is the whole watched
// resource state at ResourceVersion; the worker's content-derived mark-and-sweep drops
// any managed document the snapshot did not contain.
type ResyncRequest struct {
Desired []manifestanalyzer.DesiredResource
ResourceVersion string
GitTargetName string
GitTargetNamespace string
// Scope, when set, makes this a per-type (M12) reconcile/sweep: the mark-and-sweep is
// restricted to the named type — and, when the scope names a namespace, to that
// namespace — while Desired carries only that scope's objects (empty = pure sweep of a
// removed type). Nil is a whole-GitTarget resync. See ResyncScope for the invariant
// binding this to Desired.
Scope *ResyncScope
// SourceCollection names the target-watch collection that gathered this snapshot. Zero for a
// whole-GitTarget resync, which speaks for no single collection. Diagnostic only: nothing
// filters the queue on it. See source_collection.go.
SourceCollection types.CollectionKey
// RenderFidelityRevision is the render-fidelity revision of the target-watch stream whose
// replay this is, for the scope Scope.Collection names. Zero for a resync no stream replayed.
// The worker records that scope clean once the resync applies (recordReplayClean).
RenderFidelityRevision uint64
// Owners is the applier that owns each snapshot object some applier owns, read from the raw
// object (AppliedByOf). An object another applier owns is neither upserted into a claimed
// folder nor swept from it (apply_ownership.go).
Owners map[types.ResourceIdentifier]AppliedBy
// Initial marks the first snapshot of its collection for this GitTarget: none was applied for it
// before. It is the baseline the worker decides whatever Git holds, never held for an apply (F3
// in docs/design/applier-echoes.md).
Initial bool
// CollectionStart is the worker's ApplyState read when the snapshot's collection started. A
// snapshot collected while Git was ahead of the cluster, or across an apply, holds a picture
// the apply is about to make stale, and is discarded (docs/design/applier-echoes.md, F3). Nil
// when the producer read none: only the worker's state on arrival is then checked.
CollectionStart *ApplyState
// Result receives exactly one reply. It is buffered (cap 1) by the emitter so
// the worker never blocks delivering it.
Result chan ResyncResult
// payloadBytes is the snapshot's estimated serialized size once sized is set. See payloadSize.
payloadBytes int64
sized bool
// heldUpTo is the last live event the loop had held at a pending render-fidelity gate when it
// took this request off the queue: those were queued before the snapshot was taken, so once it
// applies it supersedes the ones of its collection (render_fidelity_hold.go).
heldUpTo uint64
}
// refusalCollection is the watched collection this request speaks for: its scope's collection for a per-type
// reconcile, and the ZERO collection for a whole-GitTarget resync, which speaks for every collection the
// target holds rather than for one of them. It is what keys a refusal's dedupe memory and its
// queued commit, so that one watched type's success neither clears nor re-arms another's.
func (r *ResyncRequest) refusalCollection() types.CollectionKey {
if r == nil || r.Scope == nil {
return types.CollectionKey{}
}
return r.Scope.Collection
}
// resyncKey identifies the slice of a mirror a resync reconciles: one GitTarget,
// and the scope within it. Two requests sharing a key are interchangeable in the
// sense that matters — the newer one's desired set wholly supersedes the older's —
// which is what makes coalescing them safe.
type resyncKey struct {
namespace string
name string
scope string
}
// resyncMarker is a queued resync's place in the FIFO, and the request it runs when it comes up. The
// FIFO holds the marker, never a request, so a request coalescing replaced is not kept alive by the
// queue: only the current one is reachable, and only the current one is charged.
//
// While its key is open (BranchWorker.pendingResyncs), a newer request for the same key replaces
// request. Once anything for its scope is queued behind it (tailPassed) the marker's position is no
// longer a safe place to run a newer snapshot, and the key is released: the marker keeps the request
// it holds, and the next request for the key takes a marker of its own. See "Queue ordering and
// coalescing" in docs/design/target-watch-plan.md.
type resyncMarker struct {
// request, charge and tailPassed are guarded by pendingResyncsMu.
request *ResyncRequest
// charge is what the intake budget was charged for the marker: its current request's, adjusted
// whenever coalescing replaces it. See intake.go.
charge int64
tailPassed bool
}
func resyncKeyFor(request *ResyncRequest) resyncKey {
key := resyncKey{namespace: request.GitTargetNamespace, name: request.GitTargetName}
if request.Scope != nil {
key.scope = request.Scope.Collection.String()
}
return key
}
// ResyncResult is the reply to a ResyncRequest: the plan's change counts, or an
// error if the resync could not be applied (in which case nothing was committed).
type ResyncResult struct {
Stats ResyncStats
Err error
}
// ResyncStats summarises what a resync changed. Skipped is documents present but not safely
// editable; PlacementSkipped is new resources the writer refused to place fail-safe. Both are
// counted and logged per-resource rather than swallowed, so a not-mirrored resource is visible in
// the summary. Neither has a dedicated status condition yet.
type ResyncStats struct {
Created int
Updated int
Deleted int
Skipped int
PlacementSkipped int
// Retained is how many managed documents this resync's prune policy kept that a converged
// mirror would have dropped. It is the ONE count here that does not describe something the
// resync did: a suppressed drop produces no action, no commit, and no other stat, so without
// it nothing downstream can tell a converged mirror from a deliberately retaining one. It
// rides the reply channel to the drain, which rolls it up onto GitTarget status.
Retained int
// PruneMode stamps Retained with the effective policy that produced it, so the count and the
// reason for it travel together. Reading the mode from the spec at projection time instead
// would let a target that has just been switched publish a new mode beside a count the old
// one produced.
PruneMode v1alpha3.PruneMode
}
// reply delivers a result on the request's buffered channel without blocking, so a
// caller that already gave up (timeout/ctx cancel) never wedges the worker loop.
func (r *ResyncRequest) reply(result ResyncResult) {
if r.Result == nil {
return
}
select {
case r.Result <- result:
default:
}
}
// Event represents a resource change event to be processed by a branch worker.
// Branch comes from the worker context (not stored in event).
// Path comes from the GitTarget that created this event.
type Event struct {
// Object is the sanitized Kubernetes object. Exactly one of Object or
// FieldPatch is set for a resource mutation; a control or DELETE event may
// carry neither.
Object *unstructured.Unstructured
// FieldPatch, when set, replaces Object with a bounded in-place edit of an
// existing parent manifest (subresource audit resolution). It is mutually
// exclusive with Object.
FieldPatch *FieldPatch
// Identifier contains resource identification information.
Identifier types.ResourceIdentifier
// ResourceVersion is the metadata.resourceVersion of the observed state this event
// describes. It is PROVENANCE, never content: sanitize strips resourceVersion from Object
// on purpose (a version in a committed manifest makes every observation a byte change), so
// this field carries the fact BESIDE the object rather than inside it. Never write it back
// onto Object.
//
// Stamped where the object is observed — the live watch holds the unsanitized object there
// and nowhere downstream does. Producers with no observed object (reconcile, bootstrap)
// leave it empty.
//
// Unlike the object-derived fields, a DELETE CAN carry one: the watch Deleted frame
// delivers the final object, so this names the last version that existed.
ResourceVersion string
// Generation is the metadata.generation of the observed state — the DESIRED state's counter,
// where ResourceVersion counts every write. Same provenance-not-content rule: sanitize strips
// generation from Object one line after resourceVersion, and for the same reason.
//
// 0 means "no generation", which covers both "this producer observed nothing" and "this kind
// has none": a ConfigMap or a Secret never carries one, only types with a spec do.
Generation int64
// Operation is the admission operation (CREATE, UPDATE, DELETE).
Operation string
// LastFieldManagers names who wrote the object last: the managers of its managedFields
// entries for the object itself (not a subresource such as status) that carry the latest
// time. More than one when they tie, since those times resolve to the second. It is PROVENANCE
// like ResourceVersion, stamped where the live watch holds the unsanitized object (sanitize
// removes managedFields); empty for a DELETE and for producers that observed no object. The
// apply window reads it to tell an applier's write from an API edit (apply_window.go).
LastFieldManagers []string
// AppliedBy is the applier that owns the object, read from its raw labels and annotations
// (AppliedByOf). PROVENANCE like LastFieldManagers, stamped by the live watch; zero when no
// applier owns it. An object another applier owns is not mirrored into a coordinated folder
// (apply_ownership.go).
AppliedBy AppliedBy
// LastFieldManagedAt is when those entries were written, to the second; zero with no
// LastFieldManagers. The apply window compares it with the time of an API edit: a late apply
// moves the applier's entry forward, and an undo that deletes the editor's entry does not.
LastFieldManagedAt time.Time
// UserInfo contains user information for commit messages.
UserInfo UserInfo
// Attribution is the authority for author rendering, the author_kind metric, and
// CommitRequest window matching. None may infer the outcome from UserInfo: an empty username
// cannot distinguish "attribution is off" from "it ran and found nothing". attachAuthor is
// the only assignment outside tests.
Attribution AttributionOutcome
// Path is the POSIX-like relative path prefix for this event's files.
// This comes from the GitTarget that claimed this event.
// Empty string means write to repository root.
Path string
// GitTargetName is the target owning this event.
GitTargetName string
// GitTargetNamespace is the namespace of the target owning this event.
GitTargetNamespace string
// SourceCluster is the source cluster this object was watched on —
// (api/v1alpha3).GitTarget.SourceCluster(), the referenced KubeProvider's namespace/name.
// The writer resolves this document's GVK->GVR
// against that cluster's type registry, so a folder mirroring a remote is never indexed
// against the local cluster's mapping.
SourceCluster string
// BootstrapOptions controls path-scoped bootstrap file staging for this event.
BootstrapOptions pathBootstrapOptions
// SourceCollection names the target-watch collection that produced this event. Zero for every
// non-stream producer (reconcile, bootstrap, the admission path). Nothing filters the queue on
// it; a snapshot of that collection supersedes the event while it is held at a pending
// render-fidelity gate (render_fidelity_hold.go). See source_collection.go.
SourceCollection types.CollectionKey
// payloadBytes is the object's estimated serialized size once sized is set. See payloadSize.
payloadBytes int64
sized bool
}
// IsFieldPatch reports whether the event carries a bounded field patch instead of
// a full object. It is the single predicate the pipeline branches on to route a
// patch to the in-place writer rather than the object writer.
func (e Event) IsFieldPatch() bool {
return e.FieldPatch != nil
}
// FieldPatch is a bounded set of field assignments to an existing parent manifest,
// carried in place of a full Object. It is how an author-preserving subresource
// mutation (e.g. deployments/scale) reaches Git: set exactly the audited field
// paths on the already committed parent, never reconstructing the whole object.
// See docs/spec/scale-subresource-audit-rehydration.md.
type FieldPatch struct {
// Assignments are the (path, value) pairs to set on the parent manifest. Paths
// are disjoint; each owns only its own subtree, so the patch is additive and
// leaves every unmentioned field in Git untouched.
Assignments []manifestedit.FieldAssignment
// Source is a bounded origin label for commit messages and metrics, e.g.
// "deployments/scale". Never the request URI.
//
// The parent Kind is NOT carried: the audit objectRef gives only the GVR, and the subresource
// body's own Kind ("Scale") is not the parent's. The writer resolves the parent through the
// same resource-identity inventory the GVR-only delete uses.
Source string
}
// CommitConfig is the resolved commit behavior used by the git writer.
type CommitConfig struct {
Committer CommitterConfig
Message CommitMessageConfig
}
// CommitterConfig defines the operator identity used as the git committer.
type CommitterConfig struct {
Name string
Email string
}
// CommitMessageConfig contains the resolved live and reconcile templates.
type CommitMessageConfig struct {
LiveTemplate string
ReconcileTemplate string
// RequestTemplate frames a CommitRequest's message. Empty — the default — commits that
// message verbatim, which is the behaviour every target had before the field existed.
RequestTemplate string
}
// ReconcileCommitMessageData is the template context for reconcile commit messages.
//
// Group, Version, Resource and APIVersion describe the reconciled type, and are
// populated only for a per-type reconcile. ResourceVersion is the version the desired set was
// pinned to. Any template referencing these must render cleanly when absent; the default guards
// both with {{if}}.
type ReconcileCommitMessageData struct {
Count int
GitTarget string
Group string
Version string
Resource string
APIVersion string
// ResourceVersion is THE COLLECTION'S metadata.resourceVersion — the LIST the desired set
// was folded from, not any one object's. A reconcile describes a type, so the version it
// can name is the snapshot's. (The per-object counterpart is
// LiveCommitMessageData.Resources[i].ResourceVersion, which a live commit carries because
// it names n resources.)
//
// Empty for a pure sweep. It was called Revision until v0.48.0; see the Revision tombstone
// below for why the word moved.
ResourceVersion string
// Namespace is the single source namespace a namespace-scoped reconcile covered, and
// is empty for a whole-target or all-namespaces reconcile.
Namespace string
}
// Revision is the retired name of ResourceVersion, kept ONLY so a stored template that still
// says {{.Revision}} is refused with a sentence instead of a text/template internal
// ("can't evaluate field Revision in type git.ReconcileCommitMessageData").
//
// It is a method rather than a scan of the template source because a method catches every
// spelling — {{with .Revision}}, {{.Revision | printf "%s"}}, assignment to a variable — which a
// search for the literal string would not. Same argument validateRequestTemplate makes for
// probing behaviour rather than matching template text.
//
// It deliberately does NOT return the value. Silently honouring the old name would keep the
// wrong word alive indefinitely, and "accepted, and quietly carried on" is the upgrade failure
// mode docs/UPGRADING.md is written against. Reject, do not prune.
//
// Delete one minor release after v0.48.0. By then {{.Revision}} can go back to failing as a
// plain unknown field, which is all an unknown field deserves.
func (ReconcileCommitMessageData) Revision() (string, error) {
return "", errors.New(
"reconcileTemplate: {{.Revision}} was renamed to {{.ResourceVersion}} in v0.48.0 " +
"(it is the snapshot's resourceVersion; \"revision\" now only ever means a Git commit)")
}
// ResourceRef is the lightweight resource identifier emitted to grouped commit
// templates via LiveCommitMessageData.Resources.
//
// Its fields are deliberately the placement template language's variables under Go template
// casing: {namespace} is .Namespace, {kind} is .Kind, and so on.
// The two renderers are separate on purpose (a path must be statically checkable, a message
// must be able to range over n resources), but a reader should not have to learn two
// vocabularies to describe the same resource. See docs/configuration.md.
type ResourceRef struct {
Operation string
APIVersion string
Group string
Version string
Resource string
// Kind is the manifest kind, the commit-message spelling of the {kind} placement
// variable. It is read off the event's object, so it is EMPTY for a DELETE: the watcher
// carries no object for one, because by then the object is gone from the cluster.
Kind string
// Namespace is the resource's namespace, or the literal "_cluster" when it is
// cluster-scoped — types.ClusterScopeSegment, the same word the built-in Git path and the
// {namespace} placement variable use. A cluster-scoped resource HAS a scope name; it is
// simply not a namespace, and "_cluster" is a name no real namespace can collide with
// (DNS-1123 forbids "_"). A template therefore never has to guard this field.
Namespace string
Name string
// Labels is the resource's metadata.labels as the writer commits them: the SANITIZED
// object's labels, so one internal/sanitize strips is already absent, exactly as for the
// "{label:key}" placement variable. It is nil for a DELETE (no object) and for a resource
// carrying none.
//
// Read it with .Label, not with .Labels.key — see Label.
Labels map[string]string
// ResourceVersion is the metadata.resourceVersion of the observed state this commit wrote,
// and "" for a producer that observed none (reconcile, bootstrap). Guard it with
// {{with .ResourceVersion}} — empty here has one meaning, "this producer observed no
// version", and no sentinel states that better than silence.
//
// It is NOT the object's current version, and the gap is deliberate: an update whose
// git-writable content is unchanged (a /status-only write) is never routed, so this value
// lags the cluster's. Compare it for EQUALITY — equal means nothing is pending — never by
// subtraction: resourceVersion is opaque by contract and is the global store revision in
// practice, so a gap measures other objects' writes, not our own drops. The census
// counts those: watch_events_total{outcome="unchanged"}.
//
// Unlike Kind and Labels, a DELETE DOES carry one — see Event.ResourceVersion.
ResourceVersion string
// Generation is the metadata.generation of the state this commit wrote, and 0 when there is
// none. It moves only when the DESIRED state changes, so unlike ResourceVersion it is worth
// comparing: it is per object, starts at 1, and advances once per spec write, so a gap
// between two commits really does mean spec changes that were not committed separately.
//
// Two blind spots keep it from replacing ResourceVersion. A ConfigMap, a Secret, and any
// other kind without a spec never carry one, so this stays 0 for much of what a target
// mirrors. And a label- or annotation-only edit changes what gets committed WITHOUT moving
// it, so an unchanged Generation does not mean an unchanged commit.
//
// Guard it with {{with .Generation}}, which renders nothing for 0.
Generation int64
}
// Label is the value of one label on this resource, and "" when it does not carry the label.
//
// Use it rather than indexing Labels directly. These templates render with
// missingkey=error, so "{{.Labels.team}}" does not render empty for a resource that has no
// "team" label: it fails the render, and a failed render fails the whole commit (the window's
// events are lost until the next resync). "{{.Label \"team\"}}" renders empty instead, which
// is this language's counterpart of the "_unlabeled" bucket a path falls back to.
func (r ResourceRef) Label(key string) string { return r.Labels[key] }
// String renders the ref as group/version/resource/namespace/name, where the namespace segment
// is "_cluster" for a cluster-scoped resource (Namespace never renders blank). Templates that
// {{range}} over Resources and print each entry get that form.
func (r ResourceRef) String() string {
parts := make([]string, 0, resourceRefStringPartCap)
if r.Group != "" {
parts = append(parts, r.Group)
}
if r.Version != "" {
parts = append(parts, r.Version)
}
if r.Resource != "" {
parts = append(parts, r.Resource)
}
if r.Namespace != "" {
parts = append(parts, r.Namespace)
}
if r.Name != "" {
parts = append(parts, r.Name)
}
return strings.Join(parts, "/")
}
// LiveCommitMessageData is the template context for every live commit message, whatever the
// window retained: one resource, many, or a 0s window. Each live commit covers exactly one
// (author, gitTarget) tuple (see docs/spec/commit-window-refactor.md).
type LiveCommitMessageData struct {
// Author is the verbatim event.UserInfo.Username for the group.
Author string
// GitTarget is the single target this commit is bound to.
GitTarget string
// Count is the number of retained resource entries before comparison with Git.
Count int
// Operations counts retained entries by operation kind (CREATE/UPDATE/DELETE).
Operations map[string]int
// Resources is the per-resource list, deduplicated by file path so the
// final state is what's being committed.
Resources []ResourceRef
// RequestMessage is the message an attached CommitRequest supplied, unaltered. It is empty for
// every window no request attached to, which is the ordinary case and why liveTemplate may
// reference it freely.
//
// It lives on this struct rather than on a parallel request-only context for the reason
// ResourceRef's own comment gives about the placement vocabulary: a reader should not have to
// learn two vocabularies to describe one commit. One sample builder then serves both renders.
RequestMessage string
}
// LabelValues is the sorted, distinct set of values this commit's resources carry for one
// label key, skipping every resource that does not set it (so the result is empty, never a
// list with a blank in it).
//