Skip to content

Commit

Permalink
Add previous image on updates and latest tags
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
darkowlzz committed Sep 13, 2022
1 parent aeea295 commit 2b8520b
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 17 deletions.
4 changes: 4 additions & 0 deletions api/v1beta2/imagepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions api/v1beta2/imagerepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,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
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@ spec:
lastScanResult:
description: LastScanResult contains the number of fetched tags.
properties:
latestTags:
type: string
scanTime:
format: date-time
type: string
Expand Down
34 changes: 33 additions & 1 deletion controllers/imagepolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"time"

"github.com/google/go-containerregistry/pkg/name"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -184,6 +185,28 @@ func (r *ImagePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return
}

func composeReadyMessage(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
}

// constructReadyMessage constructs a Ready message for an ImagePolicy based on
// the results of applying the policy.
func constructReadyMessage(previousImage, latestTag, image string) (string, error) {
previousTag := ""
if previousImage != "" {
ref, err := name.NewTag(previousImage)
if err != nil {
return "", fmt.Errorf("failed to parse previous image: %w", err)
}
previousTag = ref.TagStr()
}
return composeReadyMessage(previousTag, latestTag, image), nil
}

func (r *ImagePolicyReconciler) reconcile(ctx context.Context, obj *imagev1.ImagePolicy) (result ctrl.Result, retErr error) {
oldObj := obj.DeepCopy()

Expand All @@ -200,7 +223,11 @@ 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, err := constructReadyMessage(obj.Status.ObservedPreviousImage, resultTag, resultImage)
if err != nil {
retErr = kerrors.NewAggregate([]error{retErr, err})
}

rs := pkgreconcile.NewResultFinalizer(isSuccess, readyMsg)
retErr = rs.Finalize(obj, result, retErr)

Expand Down Expand Up @@ -290,6 +317,11 @@ 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.
if oldObj.Status.LatestImage != obj.Status.LatestImage {
obj.Status.ObservedPreviousImage = oldObj.Status.LatestImage
}

resultImage = repo.Spec.Image
resultTag = latest
Expand Down
29 changes: 15 additions & 14 deletions controllers/imagerepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -56,6 +57,8 @@ import (
"github.com/fluxcd/image-reflector-controller/internal/secret"
)

const latestTagsCount = 10

var ImageRepositoryOwnedConditions = []string{
meta.ReadyCondition,
meta.ReconcilingCondition,
Expand Down Expand Up @@ -209,23 +212,11 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, obj *imagev1.
ref, err := parseImageReference(obj.Spec.Image)
if err != nil {
conditions.MarkStalled(obj, imagev1.ImageURLInvalidReason, err.Error())
// Remove any previously set canonical image name and results. Failure
// to parse means that the image has changed.
obj.Status.CanonicalImageName = ""
obj.Status.LastScanResult = nil
result, retErr = ctrl.Result{}, nil
return
}
conditions.Delete(obj, meta.StalledCondition)

// If the image reference doesn't match the canonical image in the status,
// the image has been changed. Remove the stale results related to the image
// from the status.
if ref.Context().Name() != obj.Status.CanonicalImageName {
obj.Status.CanonicalImageName = ""
obj.Status.LastScanResult = nil
}

opts, err := r.setAuthOptions(ctx, obj, ref)
if err != nil {
e := fmt.Errorf("failed to configure authentication options: %w", err)
Expand Down Expand Up @@ -460,10 +451,20 @@ func (r *ImageRepositoryReconciler) scan(ctx context.Context, imageRepo *imagev1
return 0, fmt.Errorf("failed to set tags for %q: %w", canonicalName, err)
}

// Extract the 10 latest tags.
sort.Slice(filteredTags, func(i, j int) bool { return filteredTags[i] > filteredTags[j] })
latestTags := make([]string, latestTagsCount)
if len(filteredTags) > len(latestTags) {
latestTags = filteredTags[0:latestTagsCount]
} else {
copy(latestTags, filteredTags)
}

scanTime := metav1.Now()
imageRepo.Status.LastScanResult = &imagev1.ScanResult{
TagCount: len(filteredTags),
ScanTime: scanTime,
TagCount: len(filteredTags),
ScanTime: scanTime,
LatestTags: strings.Join(latestTags, ", ") + " ...",
}

// If the reconcile request annotation was set, consider it
Expand Down
23 changes: 23 additions & 0 deletions docs/api/image-reflector.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit 2b8520b

Please sign in to comment.