|
| 1 | +/* |
| 2 | +Copyright 2026 The Flux authors |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package controller |
| 18 | + |
| 19 | +import ( |
| 20 | + "os" |
| 21 | + "path/filepath" |
| 22 | + "testing" |
| 23 | + "time" |
| 24 | + |
| 25 | + gogit "github.com/go-git/go-git/v5" |
| 26 | + . "github.com/onsi/gomega" |
| 27 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 28 | + "sigs.k8s.io/controller-runtime/pkg/client" |
| 29 | + |
| 30 | + "github.com/fluxcd/pkg/apis/meta" |
| 31 | + "github.com/fluxcd/pkg/git" |
| 32 | + "github.com/fluxcd/pkg/gittestserver" |
| 33 | + "github.com/fluxcd/pkg/runtime/conditions" |
| 34 | + sourcev1 "github.com/fluxcd/source-controller/api/v1" |
| 35 | +) |
| 36 | + |
| 37 | +// TestGitRepositoryIncludePropagation exercises the manager watch, checkout, |
| 38 | +// composition and artifact publication through two include edges. |
| 39 | +func TestGitRepositoryIncludePropagation(t *testing.T) { |
| 40 | + g := NewWithT(t) |
| 41 | + server, err := gittestserver.NewTempGitServer() |
| 42 | + g.Expect(err).NotTo(HaveOccurred()) |
| 43 | + t.Cleanup(func() { g.Expect(os.RemoveAll(server.Root())).To(Succeed()) }) |
| 44 | + server.AutoCreate() |
| 45 | + g.Expect(server.StartHTTP()).To(Succeed()) |
| 46 | + t.Cleanup(server.StopHTTP) |
| 47 | + |
| 48 | + leafDir, mainDir := t.TempDir(), t.TempDir() |
| 49 | + g.Expect(os.WriteFile(filepath.Join(leafDir, "value.txt"), []byte("before"), 0o644)).To(Succeed()) |
| 50 | + g.Expect(os.WriteFile(filepath.Join(mainDir, "root.txt"), []byte("unchanged"), 0o644)).To(Succeed()) |
| 51 | + leafGit, err := initGitRepo(server, leafDir, git.DefaultBranch, "/leaf.git") |
| 52 | + g.Expect(err).NotTo(HaveOccurred()) |
| 53 | + _, err = initGitRepo(server, mainDir, git.DefaultBranch, "/main.git") |
| 54 | + g.Expect(err).NotTo(HaveOccurred()) |
| 55 | + |
| 56 | + newSource := func(path string, include *sourcev1.GitRepository) *sourcev1.GitRepository { |
| 57 | + repo := &sourcev1.GitRepository{ |
| 58 | + ObjectMeta: metav1.ObjectMeta{GenerateName: "include-chain-", Namespace: "default"}, |
| 59 | + Spec: sourcev1.GitRepositorySpec{ |
| 60 | + URL: server.HTTPAddress() + path, |
| 61 | + Interval: metav1.Duration{Duration: time.Hour}, |
| 62 | + }, |
| 63 | + } |
| 64 | + if include != nil { |
| 65 | + repo.Spec.Include = []sourcev1.GitRepositoryInclude{{GitRepositoryRef: meta.LocalObjectReference{Name: include.Name}, ToPath: "included"}} |
| 66 | + } |
| 67 | + g.Expect(k8sClient.Create(ctx, repo)).To(Succeed()) |
| 68 | + t.Cleanup(func() { g.Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, repo))).To(Succeed()) }) |
| 69 | + g.Eventually(func() bool { |
| 70 | + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(repo), repo); err != nil { |
| 71 | + return false |
| 72 | + } |
| 73 | + return conditions.IsReady(repo) && repo.GetArtifact() != nil |
| 74 | + }, 30*time.Second, 100*time.Millisecond).Should(BeTrue()) |
| 75 | + return repo |
| 76 | + } |
| 77 | + leaf := newSource("/leaf.git", nil) |
| 78 | + middle := newSource("/main.git", leaf) |
| 79 | + outer := newSource("/main.git", middle) |
| 80 | + middleBefore, outerBefore := middle.Status.Artifact.DeepCopy(), outer.Status.Artifact.DeepCopy() |
| 81 | + |
| 82 | + // Only the leaf's Git revision changes. The other objects have hour-long |
| 83 | + // intervals, so their updates must arrive via the include watches. |
| 84 | + g.Expect(os.WriteFile(filepath.Join(leafDir, "value.txt"), []byte("after"), 0o644)).To(Succeed()) |
| 85 | + g.Expect(commitFromFixture(leafGit, leafDir)).To(Succeed()) |
| 86 | + g.Expect(leafGit.Push(&gogit.PushOptions{})).To(Succeed()) |
| 87 | + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(leaf), leaf)).To(Succeed()) |
| 88 | + leaf.Annotations = map[string]string{meta.ReconcileRequestAnnotation: "leaf-update"} |
| 89 | + g.Expect(k8sClient.Update(ctx, leaf)).To(Succeed()) |
| 90 | + for _, obj := range []*sourcev1.GitRepository{middle, outer} { |
| 91 | + before := middleBefore |
| 92 | + if obj == outer { |
| 93 | + before = outerBefore |
| 94 | + } |
| 95 | + g.Eventually(func() bool { |
| 96 | + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil { |
| 97 | + return false |
| 98 | + } |
| 99 | + return conditions.IsReady(obj) && obj.GetArtifact() != nil && obj.GetArtifact().Digest != before.Digest |
| 100 | + }, 30*time.Second, 100*time.Millisecond).Should(BeTrue()) |
| 101 | + g.Expect(obj.Status.Artifact.Revision).To(Equal(before.Revision)) |
| 102 | + } |
| 103 | + g.Expect(middle.Status.IncludedArtifacts[0].Digest).NotTo(BeEmpty()) |
| 104 | + g.Expect(outer.Status.IncludedArtifacts[0].Digest).To(Equal(middle.Status.Artifact.Digest)) |
| 105 | + extracted := filepath.Join(t.TempDir(), "artifact") |
| 106 | + g.Expect(testStorage.CopyToPath(outer.Status.Artifact, ".", extracted)).To(Succeed()) |
| 107 | + content, err := os.ReadFile(filepath.Join(extracted, "included", "included", "value.txt")) |
| 108 | + g.Expect(err).NotTo(HaveOccurred()) |
| 109 | + g.Expect(string(content)).To(Equal("after")) |
| 110 | + |
| 111 | + // A forced no-op reconciliation keeps the artifact stable. |
| 112 | + before := outer.Status.Artifact.DeepCopy() |
| 113 | + outer.Annotations = map[string]string{meta.ReconcileRequestAnnotation: "no-op"} |
| 114 | + g.Expect(k8sClient.Update(ctx, outer)).To(Succeed()) |
| 115 | + g.Eventually(func() string { |
| 116 | + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(outer), outer)).To(Succeed()) |
| 117 | + return outer.Status.LastHandledReconcileAt |
| 118 | + }, 30*time.Second, 100*time.Millisecond).Should(Equal("no-op")) |
| 119 | + g.Expect(outer.Status.Artifact).To(Equal(before)) |
| 120 | + |
| 121 | + // Timestamp-only source events do not requeue either dependent. |
| 122 | + middleVersion, outerVersion := middle.ResourceVersion, outer.ResourceVersion |
| 123 | + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(leaf), leaf)).To(Succeed()) |
| 124 | + leaf.Status.Artifact.LastUpdateTime = metav1.Now() |
| 125 | + g.Expect(k8sClient.Status().Update(ctx, leaf)).To(Succeed()) |
| 126 | + g.Consistently(func() []string { |
| 127 | + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(middle), middle)).To(Succeed()) |
| 128 | + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(outer), outer)).To(Succeed()) |
| 129 | + return []string{middle.ResourceVersion, outer.ResourceVersion} |
| 130 | + }, time.Second, 100*time.Millisecond).Should(Equal([]string{middleVersion, outerVersion})) |
| 131 | +} |
| 132 | + |
| 133 | +func TestGitRepositoryIncludeArchiveStability(t *testing.T) { |
| 134 | + g := NewWithT(t) |
| 135 | + dir := t.TempDir() |
| 136 | + file := filepath.Join(dir, "value") |
| 137 | + g.Expect(os.WriteFile(file, []byte("unchanged"), 0o644)).To(Succeed()) |
| 138 | + obj := &sourcev1.GitRepository{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "archive-stability"}} |
| 139 | + artifact := testStorage.NewArtifactFor(sourcev1.GitRepositoryKind, obj, "same", "test.tar.gz") |
| 140 | + g.Expect(testStorage.MkdirAll(artifact)).To(Succeed()) |
| 141 | + t.Cleanup(func() { _, err := testStorage.RemoveAll(artifact); g.Expect(err).NotTo(HaveOccurred()) }) |
| 142 | + g.Expect(testStorage.Archive(&artifact, dir, nil)).To(Succeed()) |
| 143 | + digest := artifact.Digest |
| 144 | + g.Expect(os.Chtimes(file, time.Unix(1000, 0), time.Unix(1000, 0))).To(Succeed()) |
| 145 | + g.Expect(testStorage.Archive(&artifact, dir, nil)).To(Succeed()) |
| 146 | + g.Expect(artifact.Digest).To(Equal(digest)) |
| 147 | +} |
0 commit comments