diff --git a/api/v1beta2/gitrepository_types.go b/api/v1beta2/gitrepository_types.go index 0f6a0a23..e85127d6 100644 --- a/api/v1beta2/gitrepository_types.go +++ b/api/v1beta2/gitrepository_types.go @@ -224,9 +224,27 @@ type GitRepositoryStatus struct { // be used to determine if the content of the included repository has // changed. // It has the format of `:`, for example: `sha256:`. + // + // Deprecated: Replaced with explicit fields for observed artifact content + // config in the status. // +optional ContentConfigChecksum string `json:"contentConfigChecksum,omitempty"` + // ObservedIgnore is the observed exclusion patterns used for constructing + // the source artifact. + // +optional + ObservedIgnore *string `json:"observedIgnore,omitempty"` + + // ObservedRecurseSubmodules is the observed resource submodules + // configuration used to produce the current Artifact. + // +optional + ObservedRecurseSubmodules bool `json:"observedRecurseSubmodules,omitempty"` + + // ObservedInclude is the observed list of GitRepository resources used to + // to produce the current Artifact. + // +optional + ObservedInclude []GitRepositoryInclude `json:"observedInclude,omitempty"` + meta.ReconcileRequestStatus `json:",inline"` } diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index f75ab315..82c09347 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -346,6 +346,16 @@ func (in *GitRepositoryStatus) DeepCopyInto(out *GitRepositoryStatus) { } } } + if in.ObservedIgnore != nil { + in, out := &in.ObservedIgnore, &out.ObservedIgnore + *out = new(string) + **out = **in + } + if in.ObservedInclude != nil { + in, out := &in.ObservedInclude, &out.ObservedInclude + *out = make([]GitRepositoryInclude, len(*in)) + copy(*out, *in) + } out.ReconcileRequestStatus = in.ReconcileRequestStatus } diff --git a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml index 9380f20c..032cfe48 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml @@ -658,13 +658,14 @@ spec: type: object type: array contentConfigChecksum: - description: 'ContentConfigChecksum is a checksum of all the configurations + description: "ContentConfigChecksum is a checksum of all the configurations related to the content of the source artifact: - .spec.ignore - .spec.recurseSubmodules - .spec.included and the checksum of the included artifacts observed in .status.observedGeneration version of the object. This can be used to determine if the content of the included repository has changed. It has the format of `:`, - for example: `sha256:`.' + for example: `sha256:`. \n Deprecated: Replaced with explicit + fields for observed artifact content config in the status." type: string includedArtifacts: description: IncludedArtifacts contains a list of the last successfully @@ -723,6 +724,44 @@ spec: the GitRepository object. format: int64 type: integer + observedIgnore: + description: ObservedIgnore is the observed exclusion patterns used + for constructing the source artifact. + type: string + observedInclude: + description: ObservedInclude is the observed list of GitRepository + resources used to to produce the current Artifact. + items: + description: GitRepositoryInclude specifies a local reference to + a GitRepository which Artifact (sub-)contents must be included, + and where they should be placed. + properties: + fromPath: + description: FromPath specifies the path to copy contents from, + defaults to the root of the Artifact. + type: string + repository: + description: GitRepositoryRef specifies the GitRepository which + Artifact contents must be included. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + toPath: + description: ToPath specifies the path to copy contents to, + defaults to the name of the GitRepositoryRef. + type: string + required: + - repository + type: object + type: array + observedRecurseSubmodules: + description: ObservedRecurseSubmodules is the observed resource submodules + configuration used to produce the current Artifact. + type: boolean url: description: URL is the dynamic fetch link for the latest Artifact. It is provided on a "best effort" basis, and using the precise GitRepositoryStatus.Artifact diff --git a/controllers/gitrepository_controller.go b/controllers/gitrepository_controller.go index a0a5cee9..8ea55aae 100644 --- a/controllers/gitrepository_controller.go +++ b/controllers/gitrepository_controller.go @@ -18,12 +18,10 @@ package controllers import ( "context" - "crypto/sha256" "errors" "fmt" "os" "path/filepath" - "strconv" "strings" "time" @@ -33,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" kuberecorder "k8s.io/client-go/tools/record" + "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -507,8 +506,8 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, // If it's a partial commit obtained from an existing artifact, check if the // reconciliation can be skipped if other configurations have not changed. if !git.IsConcreteCommit(*commit) { - // Calculate content configuration checksum. - if r.calculateContentConfigChecksum(obj, includes) == obj.Status.ContentConfigChecksum { + // Check if the content config contributing to the artifact has changed. + if !gitContentConfigChanged(obj, includes) { ge := serror.NewGeneric( fmt.Errorf("no changes since last reconcilation: observed revision '%s'", commit.String()), sourcev1.GitOperationSucceedReason, @@ -559,27 +558,24 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, // // The inspection of the given data to the object is differed, ensuring any // stale observations like v1beta2.ArtifactOutdatedCondition are removed. -// If the given Artifact and/or artifactSet (includes) and the content config -// checksum do not differ from the object's current, it returns early. +// If the given Artifact and/or artifactSet (includes) and observed artifact +// content config do not differ from the object's current, it returns early. // Source ignore patterns are loaded, and the given directory is archived while // taking these patterns into account. -// On a successful archive, the Artifact, Includes and new content config -// checksum in the Status of the object are set, and the symlink in the Storage -// is updated to its path. +// On a successful archive, the Artifact, Includes, observed ignore, recurse +// submodules and observed include in the Status of the object are set, and the +// symlink in the Storage is updated to its path. func (r *GitRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *sourcev1.GitRepository, commit *git.Commit, includes *artifactSet, dir string) (sreconcile.Result, error) { // Create potential new artifact with current available metadata artifact := r.Storage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), commit.String(), fmt.Sprintf("%s.tar.gz", commit.Hash.String())) - // Calculate the content config checksum. - ccc := r.calculateContentConfigChecksum(obj, includes) - // Set the ArtifactInStorageCondition if there's no drift. defer func() { if obj.GetArtifact().HasRevision(artifact.Revision) && !includes.Diff(obj.Status.IncludedArtifacts) && - obj.Status.ContentConfigChecksum == ccc { + !gitContentConfigChanged(obj, includes) { conditions.Delete(obj, sourcev1.ArtifactOutdatedCondition) conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision '%s'", artifact.Revision) @@ -589,7 +585,7 @@ func (r *GitRepositoryReconciler) reconcileArtifact(ctx context.Context, // The artifact is up-to-date if obj.GetArtifact().HasRevision(artifact.Revision) && !includes.Diff(obj.Status.IncludedArtifacts) && - obj.Status.ContentConfigChecksum == ccc { + !gitContentConfigChanged(obj, includes) { r.eventLogf(ctx, obj, events.EventTypeTrace, sourcev1.ArtifactUpToDateReason, "artifact up-to-date with remote revision: '%s'", artifact.Revision) return sreconcile.ResultSuccess, nil } @@ -652,10 +648,13 @@ func (r *GitRepositoryReconciler) reconcileArtifact(ctx context.Context, return sreconcile.ResultEmpty, e } - // Record it on the object + // Record the observations on the object. obj.Status.Artifact = artifact.DeepCopy() obj.Status.IncludedArtifacts = *includes - obj.Status.ContentConfigChecksum = ccc + obj.Status.ContentConfigChecksum = "" // To be removed in the next API version. + obj.Status.ObservedIgnore = obj.Spec.Ignore + obj.Status.ObservedRecurseSubmodules = obj.Spec.RecurseSubmodules + obj.Status.ObservedInclude = obj.Spec.Include // Update symlink on a "best effort" basis url, err := r.Storage.Symlink(artifact, "latest.tar.gz") @@ -825,39 +824,6 @@ func (r *GitRepositoryReconciler) fetchIncludes(ctx context.Context, obj *source return &artifacts, nil } -// calculateContentConfigChecksum calculates a checksum of all the -// configurations that result in a change in the source artifact. It can be used -// to decide if further reconciliation is needed when an artifact already exists -// for a set of configurations. -func (r *GitRepositoryReconciler) calculateContentConfigChecksum(obj *sourcev1.GitRepository, includes *artifactSet) string { - c := []byte{} - // Consider the ignore rules and recurse submodules. - if obj.Spec.Ignore != nil { - c = append(c, []byte(*obj.Spec.Ignore)...) - } - c = append(c, []byte(strconv.FormatBool(obj.Spec.RecurseSubmodules))...) - - // Consider the included repository attributes. - for _, incl := range obj.Spec.Include { - c = append(c, []byte(incl.GitRepositoryRef.Name+incl.FromPath+incl.ToPath)...) - } - - // Consider the checksum and revision of all the included remote artifact. - // This ensures that if the included repos get updated, this checksum changes. - // NOTE: The content of an artifact may change at the same revision if the - // ignore rules change. Hence, consider both checksum and revision to - // capture changes in artifact checksum as well. - // TODO: Fix artifactSet.Diff() to consider checksum as well. - if includes != nil { - for _, incl := range *includes { - c = append(c, []byte(incl.Checksum)...) - c = append(c, []byte(incl.Revision)...) - } - } - - return fmt.Sprintf("sha256:%x", sha256.Sum256(c)) -} - // verifyCommitSignature verifies the signature of the given Git commit, if a // verification mode is specified on the object. // If the signature can not be verified or the verification fails, it records @@ -978,3 +944,64 @@ func (r *GitRepositoryReconciler) eventLogf(ctx context.Context, obj runtime.Obj } r.Eventf(obj, eventType, reason, msg) } + +// gitContentConfigChanged evaluates the current spec with the observations of +// the artifact in the status to determine if artifact content configuration has +// changed and requires rebuilding the artifact. +func gitContentConfigChanged(obj *sourcev1.GitRepository, includes *artifactSet) bool { + if !pointer.StringEqual(obj.Spec.Ignore, obj.Status.ObservedIgnore) { + return true + } + if obj.Spec.RecurseSubmodules != obj.Status.ObservedRecurseSubmodules { + return true + } + if len(obj.Spec.Include) != len(obj.Status.ObservedInclude) { + return true + } + + // Convert artifactSet to index addressable artifacts and ensure that it and + // the included artifacts include all the include from the spec. + artifacts := []*sourcev1.Artifact(*includes) + if len(obj.Spec.Include) != len(artifacts) { + return true + } + if len(obj.Spec.Include) != len(obj.Status.IncludedArtifacts) { + return true + } + + // The order of spec.include, status.IncludeArtifacts and + // status.observedInclude are the same. Compare the values by index. + for index, incl := range obj.Spec.Include { + observedIncl := obj.Status.ObservedInclude[index] + observedInclArtifact := obj.Status.IncludedArtifacts[index] + currentIncl := artifacts[index] + + // Check if the include are the same in spec and status. + if !gitRepositoryIncludeEqual(incl, observedIncl) { + return true + } + + // Check if the included repositories are still the same. + if observedInclArtifact.Revision != currentIncl.Revision { + return true + } + if observedInclArtifact.Checksum != currentIncl.Checksum { + return true + } + } + return false +} + +// Returns true if both GitRepositoryIncludes are equal. +func gitRepositoryIncludeEqual(a, b sourcev1.GitRepositoryInclude) bool { + if a.GitRepositoryRef != b.GitRepositoryRef { + return false + } + if a.FromPath != b.FromPath { + return false + } + if a.ToPath != b.ToPath { + return false + } + return true +} diff --git a/controllers/gitrepository_controller_test.go b/controllers/gitrepository_controller_test.go index bfb857df..2817fda9 100644 --- a/controllers/gitrepository_controller_test.go +++ b/controllers/gitrepository_controller_test.go @@ -143,7 +143,6 @@ Oomb3gD/TRf/nAdVED+k81GdLzciYdUGtI71/qI47G0nMBluLRE= =/4e+ -----END PGP PUBLIC KEY BLOCK----- ` - emptyContentConfigChecksum = "sha256:fcbcf165908dd18a9e49f7ff27810176db8e9f63b4352213741664245224f8aa" ) var ( @@ -685,8 +684,6 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T) Revision: "staging/" + latestRev, Path: randStringRunes(10), }, - // Checksum with all the relevant fields unset. - ContentConfigChecksum: emptyContentConfigChecksum, } conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo") }, @@ -709,8 +706,6 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T) Revision: "staging/" + latestRev, Path: randStringRunes(10), }, - // Checksum with all the relevant fields unset. - ContentConfigChecksum: emptyContentConfigChecksum, } conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo") }, @@ -835,6 +830,9 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) { includes: artifactSet{&sourcev1.Artifact{Revision: "main/revision"}}, beforeFunc: func(obj *sourcev1.GitRepository) { obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Include = []sourcev1.GitRepositoryInclude{ + {GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}}, + } }, afterFunc: func(t *WithT, obj *sourcev1.GitRepository) { t.Expect(obj.GetArtifact()).ToNot(BeNil()) @@ -850,12 +848,15 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) { { name: "Up-to-date artifact should not update status", dir: "testdata/git/repository", - includes: artifactSet{&sourcev1.Artifact{Revision: "main/revision"}}, + includes: artifactSet{&sourcev1.Artifact{Revision: "main/revision", Checksum: "some-checksum"}}, beforeFunc: func(obj *sourcev1.GitRepository) { obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Include = []sourcev1.GitRepositoryInclude{ + {GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}}, + } obj.Status.Artifact = &sourcev1.Artifact{Revision: "main/revision"} obj.Status.IncludedArtifacts = []*sourcev1.Artifact{{Revision: "main/revision", Checksum: "some-checksum"}} - obj.Status.ContentConfigChecksum = "sha256:f825d11a1c5987e033d2cb36449a3b0435a6abc9b2bfdbcdcc7c49bf40e9285d" + obj.Status.ObservedInclude = obj.Spec.Include }, afterFunc: func(t *WithT, obj *sourcev1.GitRepository) { t.Expect(obj.Status.URL).To(BeEmpty()) @@ -2145,53 +2146,6 @@ func TestGitRepositoryReconciler_fetchIncludes(t *testing.T) { } } -func TestGitRepositoryReconciler_calculateContentConfigChecksum(t *testing.T) { - g := NewWithT(t) - obj := &sourcev1.GitRepository{} - r := &GitRepositoryReconciler{} - - emptyChecksum := r.calculateContentConfigChecksum(obj, nil) - g.Expect(emptyChecksum).To(Equal(emptyContentConfigChecksum)) - - // Ignore modified. - obj.Spec.Ignore = pointer.String("some-rule") - ignoreModChecksum := r.calculateContentConfigChecksum(obj, nil) - g.Expect(emptyChecksum).ToNot(Equal(ignoreModChecksum)) - - // Recurse submodules modified. - obj.Spec.RecurseSubmodules = true - submodModChecksum := r.calculateContentConfigChecksum(obj, nil) - g.Expect(ignoreModChecksum).ToNot(Equal(submodModChecksum)) - - // Include modified. - obj.Spec.Include = []sourcev1.GitRepositoryInclude{ - { - GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, - FromPath: "aaa", - ToPath: "bbb", - }, - } - artifacts := &artifactSet{ - &sourcev1.Artifact{Revision: "some-revision-1", Checksum: "some-checksum-1"}, - } - includeModChecksum := r.calculateContentConfigChecksum(obj, artifacts) - g.Expect(submodModChecksum).ToNot(Equal(includeModChecksum)) - - // Artifact modified revision. - artifacts = &artifactSet{ - &sourcev1.Artifact{Revision: "some-revision-2", Checksum: "some-checksum-1"}, - } - artifactModChecksum := r.calculateContentConfigChecksum(obj, artifacts) - g.Expect(includeModChecksum).ToNot(Equal(artifactModChecksum)) - - // Artifact modified checksum. - artifacts = &artifactSet{ - &sourcev1.Artifact{Revision: "some-revision-2", Checksum: "some-checksum-2"}, - } - artifactCsumModChecksum := r.calculateContentConfigChecksum(obj, artifacts) - g.Expect(artifactModChecksum).ToNot(Equal(artifactCsumModChecksum)) -} - func resetChmod(path string, dirMode os.FileMode, fileMode os.FileMode) error { err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { @@ -2212,3 +2166,371 @@ func resetChmod(path string, dirMode os.FileMode, fileMode os.FileMode) error { return nil } + +func TestGitRepositoryIncludeEqual(t *testing.T) { + tests := []struct { + name string + a sourcev1.GitRepositoryInclude + b sourcev1.GitRepositoryInclude + want bool + }{ + { + name: "empty", + want: true, + }, + { + name: "different refs", + a: sourcev1.GitRepositoryInclude{ + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + }, + b: sourcev1.GitRepositoryInclude{ + GitRepositoryRef: meta.LocalObjectReference{Name: "bar"}, + }, + want: false, + }, + { + name: "same refs", + a: sourcev1.GitRepositoryInclude{ + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + }, + b: sourcev1.GitRepositoryInclude{ + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + }, + want: true, + }, + { + name: "different from paths", + a: sourcev1.GitRepositoryInclude{FromPath: "foo"}, + b: sourcev1.GitRepositoryInclude{FromPath: "bar"}, + want: false, + }, + { + name: "same from paths", + a: sourcev1.GitRepositoryInclude{FromPath: "foo"}, + b: sourcev1.GitRepositoryInclude{FromPath: "foo"}, + want: true, + }, + { + name: "different to paths", + a: sourcev1.GitRepositoryInclude{ToPath: "foo"}, + b: sourcev1.GitRepositoryInclude{ToPath: "bar"}, + want: false, + }, + { + name: "same to paths", + a: sourcev1.GitRepositoryInclude{ToPath: "foo"}, + b: sourcev1.GitRepositoryInclude{ToPath: "foo"}, + want: true, + }, + { + name: "same all", + a: sourcev1.GitRepositoryInclude{ + GitRepositoryRef: meta.LocalObjectReference{Name: "foo-ref"}, + FromPath: "foo-path", + ToPath: "bar-path", + }, + b: sourcev1.GitRepositoryInclude{ + GitRepositoryRef: meta.LocalObjectReference{Name: "foo-ref"}, + FromPath: "foo-path", + ToPath: "bar-path", + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + g.Expect(gitRepositoryIncludeEqual(tt.a, tt.b)).To(Equal(tt.want)) + }) + } +} + +func TestGitContentConfigChanged(t *testing.T) { + tests := []struct { + name string + obj sourcev1.GitRepository + artifacts []*sourcev1.Artifact + want bool + }{ + { + name: "no content config", + want: false, + }, + { + name: "unobserved ignore", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{Ignore: pointer.String("foo")}, + }, + want: true, + }, + { + name: "observed ignore", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{Ignore: pointer.String("foo")}, + Status: sourcev1.GitRepositoryStatus{ObservedIgnore: pointer.String("foo")}, + }, + want: false, + }, + { + name: "unobserved recurse submodules", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{RecurseSubmodules: true}, + }, + want: true, + }, + { + name: "observed recurse submodules", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{RecurseSubmodules: true}, + Status: sourcev1.GitRepositoryStatus{ObservedRecurseSubmodules: true}, + }, + want: false, + }, + { + name: "unobserved include", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + Include: []sourcev1.GitRepositoryInclude{ + {GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, FromPath: "bar", ToPath: "baz"}, + }, + }, + }, + want: true, + }, + { + name: "observed include", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + Include: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedInclude: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + IncludedArtifacts: []*sourcev1.Artifact{{Revision: "aaa", Checksum: "bbb"}}, + }, + }, + artifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "bbb"}, + }, + want: false, + }, + { + name: "observed include but different artifact revision", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + Include: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedInclude: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + IncludedArtifacts: []*sourcev1.Artifact{{Revision: "aaa", Checksum: "bbb"}}, + }, + }, + artifacts: []*sourcev1.Artifact{ + {Revision: "ccc", Checksum: "bbb"}, + }, + want: true, + }, + { + name: "observed include but different artifact checksum", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + Include: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedInclude: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + IncludedArtifacts: []*sourcev1.Artifact{{Revision: "aaa", Checksum: "bbb"}}, + }, + }, + artifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "ddd"}, + }, + want: true, + }, + { + name: "observed include but updated spec", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + Include: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedInclude: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + IncludedArtifacts: []*sourcev1.Artifact{{Revision: "aaa", Checksum: "bbb"}}, + }, + }, + artifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "bbb"}, + }, + want: true, + }, + { + name: "different number of include and observed include", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + Include: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + }, + Status: sourcev1.GitRepositoryStatus{ + IncludedArtifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "bbb"}, + {Revision: "ccc", Checksum: "ccc"}, + }, + }, + }, + artifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "bbb"}, + {Revision: "ccc", Checksum: "ddd"}, + }, + want: true, + }, + { + name: "different number of include and artifactset", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + Include: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedInclude: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + IncludedArtifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "bbb"}, + {Revision: "ccc", Checksum: "ccc"}, + }, + }, + }, + artifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "bbb"}, + }, + want: true, + }, + { + name: "different number of include and included artifacts", + obj: sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + Include: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedInclude: []sourcev1.GitRepositoryInclude{ + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, + FromPath: "bar", + ToPath: "baz", + }, + { + GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"}, + FromPath: "bar", + ToPath: "baz", + }, + }, + IncludedArtifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "bbb"}, + }, + }, + }, + artifacts: []*sourcev1.Artifact{ + {Revision: "aaa", Checksum: "bbb"}, + {Revision: "ccc", Checksum: "ccc"}, + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + includes := artifactSet(tt.artifacts) + g.Expect(gitContentConfigChanged(&tt.obj, &includes)).To(Equal(tt.want)) + }) + } +} diff --git a/docs/api/source.md b/docs/api/source.md index ac6eef61..ee3a6ad9 100644 --- a/docs/api/source.md +++ b/docs/api/source.md @@ -1539,7 +1539,8 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus

(Appears on: -GitRepositorySpec) +GitRepositorySpec, +GitRepositoryStatus)

GitRepositoryInclude specifies a local reference to a GitRepository which Artifact (sub-)contents must be included, and where they should be placed.

@@ -1969,6 +1970,49 @@ observed in .status.observedGeneration version of the object. This can be used to determine if the content of the included repository has changed. It has the format of <algo>:<checksum>, for example: sha256:<checksum>.

+

Deprecated: Replaced with explicit fields for observed artifact content +config in the status.

+ + + + +observedIgnore
+ +string + + + +(Optional) +

ObservedIgnore is the observed exclusion patterns used for constructing +the source artifact.

+ + + + +observedRecurseSubmodules
+ +bool + + + +(Optional) +

ObservedRecurseSubmodules is the observed resource submodules +configuration used to produce the current Artifact.

+ + + + +observedInclude
+ + +[]GitRepositoryInclude + + + + +(Optional) +

ObservedInclude is the observed list of GitRepository resources used to +to produce the current Artifact.

diff --git a/docs/spec/v1beta2/gitrepositories.md b/docs/spec/v1beta2/gitrepositories.md index a2556942..7cfbfd18 100644 --- a/docs/spec/v1beta2/gitrepositories.md +++ b/docs/spec/v1beta2/gitrepositories.md @@ -854,6 +854,79 @@ configurations of the GitRepository that indicate a change in source and records it in `.status.contentConfigChecksum`. This field is used to determine if the source artifact needs to be rebuilt. +**Deprecation Note:** `contentConfigChecksum` is no longer used and will be +removed in the next API version. The individual components used for generating +content configuration checksum now have explicit fields in the status. This +makes the observations used by the controller for making artifact rebuild +decisions more transparent and easier to debug. + +### Observed Ignore + +The source-controller reports an observed ignore in the GitRepository's +`.status.observedIgnore`. The observed ignore is the latest `.spec.ignore` value +which resulted in a [ready state](#ready-gitrepository), or stalled due to error +it can not recover from without human intervention. +The value is the same as the [ignore in spec](#ignore). +It indicates the ignore rules used in building the current artifact in storage. +It is also used by the controller to determine if an artifact needs to be +rebuilt. + +Example: +```yaml +status: + ... + observedIgnore: | + cue + pkg + ... +``` + +### Observed Recurse Submodules + +The source-controller reports an observed recurse submodule in the +GitRepository's `.status.observedRecurseSubmodules`. The observed recurse +submodules is the latest `.spec.recurseSubmodules` value which resulted in a +[ready state](#ready-gitrepository), or stalled due to error it can not recover +from without human intervention. The value is the same as the +[recurse submodules in spec](#recurse-submodules). It indicates the recurse +submodules configuration used in building the current artifact in storage. It is +also used by the controller to determine if an artifact needs to be rebuilt. + +Example: +```yaml +status: + ... + observedRecurseSubmodules: true + ... +``` + +### Observed Include + +The source-controller reports observed include in the GitRepository's +`.status.observedInclude`. The observed include is the latest +`.spec.recurseSubmodules` value which resulted in a +[ready state](#ready-gitrepository), or stalled due to error it can not recover +from without human intervention. The value is the same as the +[include in spec](#include). It indicates the include configuration used in +building the current artifact in storage. It is also used by the controller to +determine if an artifact needs to be rebuilt. + +Example: +```yaml +status: + ... + observedInclude: + - fromPath: deploy/webapp + repository: + name: repo1 + toPath: foo + - fromPath: deploy/secure + repository: + name: repo2 + toPath: bar + ... +``` + ### Observed Generation The source-controller reports an [observed generation][typical-status-properties]