Add previous image on updates and latest tags
Introduce new field in ImagePolicy status, observedPreviousImage, to store the previous image. This is needed to show image update version change in alerts. Introduce new field in ImageRepository status, latestTags, to store the 10 recent tags. This would help users debug image policy better. Signed-off-by: Sunny <darkowlzz@protonmail.com>
This commit is contained in:
parent
685f7be017
commit
091a00cb2e
|
|
@ -105,6 +105,10 @@ type ImagePolicyStatus struct {
|
|||
// the image repository, when filtered and ordered according to
|
||||
// the policy.
|
||||
LatestImage string `json:"latestImage,omitempty"`
|
||||
// ObservedPreviousImage is the observed previous LatestImage. It is used
|
||||
// to keep track of the previous and current images.
|
||||
// +optional
|
||||
ObservedPreviousImage string `json:"observedPreviousImage,omitempty"`
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
// +optional
|
||||
|
|
|
|||
|
|
@ -94,8 +94,9 @@ type ImageRepositorySpec struct {
|
|||
}
|
||||
|
||||
type ScanResult struct {
|
||||
TagCount int `json:"tagCount"`
|
||||
ScanTime metav1.Time `json:"scanTime,omitempty"`
|
||||
TagCount int `json:"tagCount"`
|
||||
ScanTime metav1.Time `json:"scanTime,omitempty"`
|
||||
LatestTags []string `json:"latestTags,omitempty"`
|
||||
}
|
||||
|
||||
// ImageRepositoryStatus defines the observed state of ImageRepository
|
||||
|
|
|
|||
|
|
@ -328,6 +328,11 @@ func (in *NumericalPolicy) DeepCopy() *NumericalPolicy {
|
|||
func (in *ScanResult) DeepCopyInto(out *ScanResult) {
|
||||
*out = *in
|
||||
in.ScanTime.DeepCopyInto(&out.ScanTime)
|
||||
if in.LatestTags != nil {
|
||||
in, out := &in.LatestTags, &out.LatestTags
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScanResult.
|
||||
|
|
|
|||
|
|
@ -391,6 +391,10 @@ spec:
|
|||
observedGeneration:
|
||||
format: int64
|
||||
type: integer
|
||||
observedPreviousImage:
|
||||
description: ObservedPreviousImage is the observed previous LatestImage.
|
||||
It is used to keep track of the previous and current images.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
|
|
|
|||
|
|
@ -432,6 +432,10 @@ spec:
|
|||
lastScanResult:
|
||||
description: LastScanResult contains the number of fetched tags.
|
||||
properties:
|
||||
latestTags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
scanTime:
|
||||
format: date-time
|
||||
type: string
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
|
@ -192,10 +193,20 @@ func (r *ImagePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
|||
return
|
||||
}
|
||||
|
||||
// composeImagePolicyReadyMessage composes a Ready message for an ImagePolicy
|
||||
// based on the results of applying the policy.
|
||||
func composeImagePolicyReadyMessage(previousTag, latestTag, image string) string {
|
||||
readyMsg := fmt.Sprintf("Latest image tag for '%s' resolved to %s", image, latestTag)
|
||||
if previousTag != "" && previousTag != latestTag {
|
||||
readyMsg = fmt.Sprintf("Latest image tag for '%s' updated from %s to %s", image, previousTag, latestTag)
|
||||
}
|
||||
return readyMsg
|
||||
}
|
||||
|
||||
func (r *ImagePolicyReconciler) reconcile(ctx context.Context, obj *imagev1.ImagePolicy) (result ctrl.Result, retErr error) {
|
||||
oldObj := obj.DeepCopy()
|
||||
|
||||
var resultImage, resultTag string
|
||||
var resultImage, resultTag, previousTag string
|
||||
|
||||
// If there's no error and no requeue is requested, it's a success. Unlike
|
||||
// other reconcilers, this reconciler doesn't requeue on its own with a
|
||||
|
|
@ -208,7 +219,8 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, obj *imagev1.Imag
|
|||
}
|
||||
|
||||
defer func() {
|
||||
readyMsg := fmt.Sprintf("Latest image tag for '%s' resolved to: %s", resultImage, resultTag)
|
||||
readyMsg := composeImagePolicyReadyMessage(previousTag, resultTag, resultImage)
|
||||
|
||||
rs := pkgreconcile.NewResultFinalizer(isSuccess, readyMsg)
|
||||
retErr = rs.Finalize(obj, result, retErr)
|
||||
|
||||
|
|
@ -286,6 +298,26 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, obj *imagev1.Imag
|
|||
|
||||
// Write the observations on status.
|
||||
obj.Status.LatestImage = repo.Spec.Image + ":" + latest
|
||||
// If the old latest image and new latest image don't match, set the old
|
||||
// image as the observed previous image.
|
||||
// NOTE: The following allows the previous image to be set empty when
|
||||
// there's a failure and a subsequent recovery from it. This behavior helps
|
||||
// avoid creating an update event as there's no previous image to infer
|
||||
// from. Recovery from a failure shouldn't result in an update event.
|
||||
if oldObj.Status.LatestImage != obj.Status.LatestImage {
|
||||
obj.Status.ObservedPreviousImage = oldObj.Status.LatestImage
|
||||
}
|
||||
// Parse the observed previous image if any and extract previous tag. This
|
||||
// is used to determine image tag update path.
|
||||
if obj.Status.ObservedPreviousImage != "" {
|
||||
prevRef, err := name.NewTag(obj.Status.ObservedPreviousImage)
|
||||
if err != nil {
|
||||
e := fmt.Errorf("failed to parse previous image '%s': %w", obj.Status.ObservedPreviousImage, err)
|
||||
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error())
|
||||
result, retErr = ctrl.Result{}, e
|
||||
}
|
||||
previousTag = prevRef.TagStr()
|
||||
}
|
||||
|
||||
resultImage = repo.Spec.Image
|
||||
resultTag = latest
|
||||
|
|
|
|||
|
|
@ -316,3 +316,42 @@ func TestImagePolicyReconciler_applyPolicy(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeImagePolicyReadyMessage(t *testing.T) {
|
||||
testImage := "foo/bar"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
previousTag string
|
||||
latestTag string
|
||||
image string
|
||||
wantMessage string
|
||||
}{
|
||||
{
|
||||
name: "no previous tag",
|
||||
latestTag: "1.0.0",
|
||||
wantMessage: "Latest image tag for 'foo/bar' resolved to 1.0.0",
|
||||
},
|
||||
{
|
||||
name: "different previous tag",
|
||||
previousTag: "1.0.0",
|
||||
latestTag: "1.1.0",
|
||||
wantMessage: "Latest image tag for 'foo/bar' updated from 1.0.0 to 1.1.0",
|
||||
},
|
||||
{
|
||||
name: "same previous and latest tags",
|
||||
previousTag: "1.0.0",
|
||||
latestTag: "1.0.0",
|
||||
wantMessage: "Latest image tag for 'foo/bar' resolved to 1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
result := composeImagePolicyReadyMessage(tt.previousTag, tt.latestTag, testImage)
|
||||
g.Expect(result).To(Equal(tt.wantMessage))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -57,6 +58,9 @@ import (
|
|||
"github.com/fluxcd/image-reflector-controller/internal/secret"
|
||||
)
|
||||
|
||||
// latestTagsCount is the number of tags to use as latest tags.
|
||||
const latestTagsCount = 10
|
||||
|
||||
// imageRepositoryOwnedConditions is a list of conditions owned by the
|
||||
// ImageRepositoryReconciler.
|
||||
var imageRepositoryOwnedConditions = []string{
|
||||
|
|
@ -462,8 +466,9 @@ func (r *ImageRepositoryReconciler) scan(ctx context.Context, obj *imagev1.Image
|
|||
|
||||
scanTime := metav1.Now()
|
||||
obj.Status.LastScanResult = &imagev1.ScanResult{
|
||||
TagCount: len(filteredTags),
|
||||
ScanTime: scanTime,
|
||||
TagCount: len(filteredTags),
|
||||
ScanTime: scanTime,
|
||||
LatestTags: getLatestTags(filteredTags),
|
||||
}
|
||||
|
||||
// If the reconcile request annotation was set, consider it
|
||||
|
|
@ -551,6 +556,21 @@ func filterOutTags(tags []string, patterns []string) ([]string, error) {
|
|||
return filteredTags, nil
|
||||
}
|
||||
|
||||
// getLatestTags takes a slice of tags, sorts them in descending order of their
|
||||
// values and returns the 10 latest tags.
|
||||
func getLatestTags(tags []string) []string {
|
||||
var result []string
|
||||
sort.SliceStable(tags, func(i, j int) bool { return tags[i] > tags[j] })
|
||||
|
||||
if len(tags) >= latestTagsCount {
|
||||
latestTags := tags[0:latestTagsCount]
|
||||
result = append(result, latestTags...)
|
||||
} else {
|
||||
result = append(result, tags...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// isEqualSliceContent compares two string slices to check if they have the same
|
||||
// content.
|
||||
func isEqualSliceContent(a, b []string) bool {
|
||||
|
|
|
|||
|
|
@ -440,37 +440,48 @@ func TestImageRepositoryReconciler_scan(t *testing.T) {
|
|||
defer registryServer.Close()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tags []string
|
||||
exclusionList []string
|
||||
annotation string
|
||||
db *mockDatabase
|
||||
wantErr bool
|
||||
wantTags []string
|
||||
name string
|
||||
tags []string
|
||||
exclusionList []string
|
||||
annotation string
|
||||
db *mockDatabase
|
||||
wantErr bool
|
||||
wantTags []string
|
||||
wantLatestTags []string
|
||||
}{
|
||||
{
|
||||
name: "no tags",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "simple tags",
|
||||
tags: []string{"a", "b", "c", "d"},
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"a", "b", "c", "d"},
|
||||
name: "simple tags",
|
||||
tags: []string{"a", "b", "c", "d"},
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"a", "b", "c", "d"},
|
||||
wantLatestTags: []string{"d", "c", "b", "a"},
|
||||
},
|
||||
{
|
||||
name: "with single exclusion pattern",
|
||||
tags: []string{"a", "b", "c", "d"},
|
||||
exclusionList: []string{"c"},
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"a", "b", "d"},
|
||||
name: "simple tags, 10+",
|
||||
tags: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"},
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"},
|
||||
wantLatestTags: []string{"k", "j", "i", "h, g, f, e, d, c, b"},
|
||||
},
|
||||
{
|
||||
name: "with multiple exclusion pattern",
|
||||
tags: []string{"a", "b", "c", "d"},
|
||||
exclusionList: []string{"c", "a"},
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"b", "d"},
|
||||
name: "with single exclusion pattern",
|
||||
tags: []string{"a", "b", "c", "d"},
|
||||
exclusionList: []string{"c"},
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"a", "b", "d"},
|
||||
wantLatestTags: []string{"d", "b", "a"},
|
||||
},
|
||||
{
|
||||
name: "with multiple exclusion pattern",
|
||||
tags: []string{"a", "b", "c", "d"},
|
||||
exclusionList: []string{"c", "a"},
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"b", "d"},
|
||||
wantLatestTags: []string{"d", "b"},
|
||||
},
|
||||
{
|
||||
name: "bad exclusion pattern",
|
||||
|
|
@ -485,11 +496,12 @@ func TestImageRepositoryReconciler_scan(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "with reconcile annotation",
|
||||
tags: []string{"a", "b"},
|
||||
annotation: "foo",
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"a", "b"},
|
||||
name: "with reconcile annotation",
|
||||
tags: []string{"a", "b"},
|
||||
annotation: "foo",
|
||||
db: &mockDatabase{},
|
||||
wantTags: []string{"a", "b"},
|
||||
wantLatestTags: []string{"b", "a"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -535,6 +547,62 @@ func TestImageRepositoryReconciler_scan(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetLatestTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tags []string
|
||||
wantLatestTags []string
|
||||
}{
|
||||
{
|
||||
name: "no tags",
|
||||
wantLatestTags: nil,
|
||||
},
|
||||
{
|
||||
name: "few semver tags",
|
||||
tags: []string{"1.0.0", "0.0.8", "1.2.5", "3.0.1", "1.0.1"},
|
||||
wantLatestTags: []string{"3.0.1", "1.2.5", "1.0.1", "1.0.0", "0.0.8"},
|
||||
},
|
||||
{
|
||||
name: "10 semver tags",
|
||||
tags: []string{"1.0.0", "0.0.8", "1.2.5", "3.0.1", "1.0.1", "5.1.1", "4.1.0", "4.5.0", "4.0.3", "2.2.2"},
|
||||
wantLatestTags: []string{"5.1.1", "4.5.0", "4.1.0", "4.0.3", "3.0.1", "2.2.2", "1.2.5", "1.0.1", "1.0.0", "0.0.8"},
|
||||
},
|
||||
{
|
||||
name: "10+ semver tags",
|
||||
tags: []string{"1.0.0", "0.0.8", "1.2.5", "3.0.1", "1.0.1", "5.1.1", "4.1.0", "4.5.0", "4.0.3", "2.2.2", "0.5.1", "0.1.0"},
|
||||
wantLatestTags: []string{"5.1.1", "4.5.0", "4.1.0", "4.0.3", "3.0.1", "2.2.2", "1.2.5", "1.0.1", "1.0.0", "0.5.1"},
|
||||
},
|
||||
{
|
||||
name: "few numerical tags",
|
||||
tags: []string{"-62", "-88", "73", "72", "15"},
|
||||
wantLatestTags: []string{"73", "72", "15", "-88", "-62"},
|
||||
},
|
||||
{
|
||||
name: "few numerical tags",
|
||||
tags: []string{"-62", "-88", "73", "72", "15", "16", "15", "29", "-33", "-91", "100", "101"},
|
||||
wantLatestTags: []string{"73", "72", "29", "16", "15", "15", "101", "100", "-91", "-88"},
|
||||
},
|
||||
{
|
||||
name: "few word tags",
|
||||
tags: []string{"aaa", "bbb", "ccc"},
|
||||
wantLatestTags: []string{"ccc", "bbb", "aaa"},
|
||||
},
|
||||
{
|
||||
name: "few word tags",
|
||||
tags: []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll"},
|
||||
wantLatestTags: []string{"lll", "kkk", "jjj", "iii", "hhh", "ggg", "fff", "eee", "ddd", "ccc"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
g.Expect(getLatestTags(tt.tags)).To(Equal(tt.wantLatestTags))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseImageReference(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -313,6 +313,19 @@ the policy.</p>
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>observedPreviousImage</code><br>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>ObservedPreviousImage is the observed previous LatestImage. It is used
|
||||
to keep track of the previous and current images.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>observedGeneration</code><br>
|
||||
<em>
|
||||
int64
|
||||
|
|
@ -865,6 +878,16 @@ Kubernetes meta/v1.Time
|
|||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>latestTags</code><br>
|
||||
<em>
|
||||
[]string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue