Skip to content

Commit 05a6e55

Browse files
authored
Merge pull request #780 from fluxcd/feat-778
Store checksum of ImageRepository tags and trigger ImagePolicy watch only when it changes
2 parents d980267 + 352897b commit 05a6e55

File tree

11 files changed

+338
-111
lines changed

11 files changed

+338
-111
lines changed

api/v1beta2/imagerepository_types.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
const ImageRepositoryKind = "ImageRepository"
2929

3030
// Deprecated: Use ImageFinalizer.
31+
// TODO: Remove in v1.
3132
const ImageRepositoryFinalizer = ImageFinalizer
3233

3334
// ImageRepositorySpec defines the parameters for scanning an image
@@ -115,10 +116,26 @@ type ImageRepositorySpec struct {
115116
Insecure bool `json:"insecure,omitempty"`
116117
}
117118

119+
// ScanResult contains information about the last scan of the image repository.
120+
// TODO: Make all fields except for LatestTags required in v1.
118121
type ScanResult struct {
119-
TagCount int `json:"tagCount"`
120-
ScanTime metav1.Time `json:"scanTime,omitempty"`
121-
LatestTags []string `json:"latestTags,omitempty"`
122+
// Revision is a stable hash of the scanned tags.
123+
// +optional
124+
Revision string `json:"revision"`
125+
126+
// TagCount is the number of tags found in the last scan.
127+
// +required
128+
TagCount int `json:"tagCount"`
129+
130+
// ScanTime is the time when the last scan was performed.
131+
// +optional
132+
ScanTime metav1.Time `json:"scanTime"`
133+
134+
// LatestTags is a small sample of the tags found in the last scan.
135+
// It's the first 10 tags when sorting all the tags in descending
136+
// alphabetical order.
137+
// +optional
138+
LatestTags []string `json:"latestTags,omitempty"`
122139
}
123140

124141
// ImageRepositoryStatus defines the observed state of ImageRepository

config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,13 +483,23 @@ spec:
483483
description: LastScanResult contains the number of fetched tags.
484484
properties:
485485
latestTags:
486+
description: |-
487+
LatestTags is a small sample of the tags found in the last scan.
488+
It's the first 10 tags when sorting all the tags in descending
489+
alphabetical order.
486490
items:
487491
type: string
488492
type: array
493+
revision:
494+
description: Revision is a stable hash of the scanned tags.
495+
type: string
489496
scanTime:
497+
description: ScanTime is the time when the last scan was performed.
490498
format: date-time
491499
type: string
492500
tagCount:
501+
description: TagCount is the number of tags found in the last
502+
scan.
493503
type: integer
494504
required:
495505
- tagCount

docs/api/v1beta2/image-reflector.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,8 @@ would select 0.</p>
10981098
(<em>Appears on:</em>
10991099
<a href="#image.toolkit.fluxcd.io/v1beta2.ImageRepositoryStatus">ImageRepositoryStatus</a>)
11001100
</p>
1101+
<p>ScanResult contains information about the last scan of the image repository.
1102+
TODO: Make all fields except for LatestTags required in v1.</p>
11011103
<div class="md-typeset__scrollwrap">
11021104
<div class="md-typeset__table">
11031105
<table>
@@ -1110,12 +1112,25 @@ would select 0.</p>
11101112
<tbody>
11111113
<tr>
11121114
<td>
1115+
<code>revision</code><br>
1116+
<em>
1117+
string
1118+
</em>
1119+
</td>
1120+
<td>
1121+
<em>(Optional)</em>
1122+
<p>Revision is a stable hash of the scanned tags.</p>
1123+
</td>
1124+
</tr>
1125+
<tr>
1126+
<td>
11131127
<code>tagCount</code><br>
11141128
<em>
11151129
int
11161130
</em>
11171131
</td>
11181132
<td>
1133+
<p>TagCount is the number of tags found in the last scan.</p>
11191134
</td>
11201135
</tr>
11211136
<tr>
@@ -1128,6 +1143,8 @@ Kubernetes meta/v1.Time
11281143
</em>
11291144
</td>
11301145
<td>
1146+
<em>(Optional)</em>
1147+
<p>ScanTime is the time when the last scan was performed.</p>
11311148
</td>
11321149
</tr>
11331150
<tr>
@@ -1138,6 +1155,10 @@ Kubernetes meta/v1.Time
11381155
</em>
11391156
</td>
11401157
<td>
1158+
<em>(Optional)</em>
1159+
<p>LatestTags is a small sample of the tags found in the last scan.
1160+
It&rsquo;s the first 10 tags when sorting all the tags in descending
1161+
alphabetical order.</p>
11411162
</td>
11421163
</tr>
11431164
</tbody>

internal/controller/database.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package controller
1818

1919
// DatabaseWriter implementations record the tags for an image repository.
2020
type DatabaseWriter interface {
21-
SetTags(repo string, tags []string) error
21+
SetTags(repo string, tags []string) (string, error)
2222
}
2323

2424
// DatabaseReader implementations get the stored set of tags for an image

internal/controller/imagepolicy_controller.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,18 @@ func (imageRepositoryPredicate) Update(e event.UpdateEvent) bool {
164164
return false
165165
}
166166

167-
// This is a temporary workaround to avoid reconciling ImagePolicy
168-
// when the ImageRepository is not ready. In the near future, we
169-
// will implement a digest for the scanned tags in the ImageRepository
170-
// and will only return true here if the digest has changed, which
171-
// covers not only skipping the reconciliation when the ImageRepository
172-
// is not ready, but also when the tags have not changed.
173-
repo := e.ObjectNew.(*imagev1.ImageRepository)
174-
return conditions.IsReady(repo) &&
175-
conditions.GetObservedGeneration(repo, meta.ReadyCondition) == repo.Generation
167+
newRepo := e.ObjectNew.(*imagev1.ImageRepository)
168+
if newRepo.Status.LastScanResult == nil {
169+
return false
170+
}
171+
172+
oldRepo := e.ObjectOld.(*imagev1.ImageRepository)
173+
if oldRepo.Status.LastScanResult == nil ||
174+
oldRepo.Status.LastScanResult.Revision != newRepo.Status.LastScanResult.Revision {
175+
return true
176+
}
177+
178+
return false
176179
}
177180

178181
func (r *ImagePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {

internal/controller/imagepolicy_controller_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,147 @@ func TestImagePolicyReconciler_ignoresImageRepoNotReadyEvent(t *testing.T) {
185185
}).Should(BeTrue())
186186
}
187187

188+
func TestImagePolicyReconciler_imageRepoRevisionLifeCycle(t *testing.T) {
189+
g := NewWithT(t)
190+
191+
namespaceName := "imagepolicy-" + randStringRunes(5)
192+
namespace := &corev1.Namespace{
193+
ObjectMeta: metav1.ObjectMeta{Name: namespaceName},
194+
}
195+
g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred())
196+
t.Cleanup(func() {
197+
g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred())
198+
})
199+
200+
imageRepo := &imagev1.ImageRepository{
201+
ObjectMeta: metav1.ObjectMeta{
202+
Namespace: namespaceName,
203+
Name: "repo",
204+
},
205+
Spec: imagev1.ImageRepositorySpec{
206+
Image: "ghcr.io/stefanprodan/podinfo",
207+
},
208+
}
209+
g.Expect(k8sClient.Create(ctx, imageRepo)).NotTo(HaveOccurred())
210+
t.Cleanup(func() {
211+
g.Expect(k8sClient.Delete(ctx, imageRepo)).NotTo(HaveOccurred())
212+
})
213+
214+
g.Eventually(func() bool {
215+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imageRepo), imageRepo)
216+
return err == nil && conditions.IsReady(imageRepo) &&
217+
imageRepo.Generation == conditions.GetObservedGeneration(imageRepo, meta.ReadyCondition)
218+
}, timeout).Should(BeTrue())
219+
220+
imagePolicy := &imagev1.ImagePolicy{
221+
ObjectMeta: metav1.ObjectMeta{
222+
Namespace: namespaceName,
223+
Name: "test-imagepolicy",
224+
},
225+
Spec: imagev1.ImagePolicySpec{
226+
ImageRepositoryRef: meta.NamespacedObjectReference{
227+
Name: imageRepo.Name,
228+
},
229+
FilterTags: &imagev1.TagFilter{
230+
Pattern: `^6\.7\.\d+$`,
231+
},
232+
Policy: imagev1.ImagePolicyChoice{
233+
SemVer: &imagev1.SemVerPolicy{
234+
Range: "6.7.x",
235+
},
236+
},
237+
},
238+
}
239+
g.Expect(k8sClient.Create(ctx, imagePolicy)).NotTo(HaveOccurred())
240+
t.Cleanup(func() {
241+
g.Expect(k8sClient.Delete(ctx, imagePolicy)).NotTo(HaveOccurred())
242+
})
243+
244+
g.Eventually(func() bool {
245+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imagePolicy), imagePolicy)
246+
return err == nil && conditions.IsReady(imagePolicy) &&
247+
imagePolicy.Generation == conditions.GetObservedGeneration(imagePolicy, meta.ReadyCondition) &&
248+
imagePolicy.Status.LatestRef != nil &&
249+
imagePolicy.Status.LatestRef.Tag == "6.7.1"
250+
}, timeout).Should(BeTrue())
251+
expectedImagePolicyLastTransitionTime := conditions.GetLastTransitionTime(imagePolicy, meta.ReadyCondition).Time
252+
253+
// Now force a reconciliation by setting the annotation.
254+
var requestedAt string
255+
g.Eventually(func() bool {
256+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imageRepo), imageRepo)
257+
if err != nil {
258+
return false
259+
}
260+
p := patch.NewSerialPatcher(imageRepo, k8sClient)
261+
requestedAt = time.Now().Format(time.RFC3339Nano)
262+
if imageRepo.Annotations == nil {
263+
imageRepo.Annotations = make(map[string]string)
264+
}
265+
imageRepo.Annotations["reconcile.fluxcd.io/requestedAt"] = requestedAt
266+
return p.Patch(ctx, imageRepo) == nil
267+
}, timeout).Should(BeTrue())
268+
269+
// Wait for the ImageRepository to reconcile.
270+
g.Eventually(func() bool {
271+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imageRepo), imageRepo)
272+
return err == nil && conditions.IsReady(imageRepo) &&
273+
imageRepo.Status.LastHandledReconcileAt == requestedAt
274+
}, timeout).Should(BeTrue())
275+
276+
// Check that the ImagePolicy is still ready and does not get updated.
277+
g.Eventually(func() bool {
278+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imagePolicy), imagePolicy)
279+
return err == nil && conditions.IsReady(imagePolicy) &&
280+
imagePolicy.Status.LatestRef != nil &&
281+
imagePolicy.Status.LatestRef.Tag == "6.7.1"
282+
}, timeout).Should(BeTrue())
283+
284+
// Wait a bit and check that the ImagePolicy remains ready.
285+
time.Sleep(time.Second)
286+
g.Eventually(func() bool {
287+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imagePolicy), imagePolicy)
288+
return err == nil && conditions.IsReady(imagePolicy) &&
289+
imagePolicy.Status.LatestRef != nil &&
290+
imagePolicy.Status.LatestRef.Tag == "6.7.1"
291+
}, timeout).Should(BeTrue())
292+
293+
// Check that the last transition time of the ImagePolicy Ready condition did not change since the beginning.
294+
lastTransitionTime := conditions.GetLastTransitionTime(imagePolicy, meta.ReadyCondition).Time
295+
g.Expect(lastTransitionTime).To(Equal(expectedImagePolicyLastTransitionTime))
296+
297+
// Now add an exclusion rule to force the checksum to change.
298+
firstChecksum := imageRepo.Status.LastScanResult.Revision
299+
g.Eventually(func() bool {
300+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imageRepo), imageRepo)
301+
if err != nil {
302+
return false
303+
}
304+
p := patch.NewSerialPatcher(imageRepo, k8sClient)
305+
imageRepo.Spec.ExclusionList = []string{`^6\.7\.1$`}
306+
return p.Patch(ctx, imageRepo) == nil
307+
}, timeout).Should(BeTrue())
308+
309+
// Wait for the ImageRepository to reconcile.
310+
g.Eventually(func() bool {
311+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imageRepo), imageRepo)
312+
return err == nil && conditions.IsReady(imageRepo) &&
313+
imageRepo.Generation == conditions.GetObservedGeneration(imageRepo, meta.ReadyCondition) &&
314+
imageRepo.Status.LastScanResult.Revision != firstChecksum
315+
}, timeout).Should(BeTrue())
316+
317+
// Check that the ImagePolicy receives the update and the latest tag changes.
318+
g.Eventually(func() bool {
319+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imagePolicy), imagePolicy)
320+
if err != nil {
321+
return false
322+
}
323+
return conditions.IsReady(imagePolicy) &&
324+
imagePolicy.Generation == conditions.GetObservedGeneration(imagePolicy, meta.ReadyCondition) &&
325+
imagePolicy.Status.LatestRef.Tag == "6.7.0"
326+
}, timeout).Should(BeTrue())
327+
}
328+
188329
func TestImagePolicyReconciler_invalidImage(t *testing.T) {
189330
g := NewWithT(t)
190331

0 commit comments

Comments
 (0)