diff --git a/pkg/controller/checker/checker.go b/pkg/controller/checker/checker.go index 8149dcce..4b0c3b1a 100644 --- a/pkg/controller/checker/checker.go +++ b/pkg/controller/checker/checker.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" corev1 "k8s.io/api/core/v1" @@ -18,10 +19,12 @@ type Checker struct { } type Result struct { - CurrentVersion string - LatestVersion string - IsLatest bool - ImageURL string + CurrentVersion string + CurrentTimestamp time.Time + LatestVersion string + LatestTimestamp time.Time + IsLatest bool + ImageURL string } func New(search search.Searcher) *Checker { @@ -47,12 +50,31 @@ func (c *Checker) Container(ctx context.Context, log *logrus.Entry, pod *corev1. } imageURL = c.overrideImageURL(log, imageURL, opts) + timestamp, err := c.getTimestamp(ctx, imageURL, currentTag, currentSHA, opts.UseSHA) + if err != nil { + return nil, err + } + var result *Result if opts.UseSHA { - return c.handleSHA(ctx, imageURL, statusSHA, opts, usingTag, currentTag) + result, err = c.handleSHA(ctx, imageURL, statusSHA, currentTag, usingTag, opts) + } else { + result, err = c.handleSemver(ctx, imageURL, statusSHA, currentTag, usingSHA, opts) } + if err != nil { + return result, err + } + result.CurrentTimestamp = timestamp + return result, err +} - return c.handleSemver(ctx, imageURL, statusSHA, currentTag, usingSHA, opts) +func (c *Checker) getTimestamp(ctx context.Context, imageURL string, tag string, sha string, useSHA bool) (time.Time, error) { + currentImage, err := c.search.Image(ctx, imageURL, tag, sha, useSHA) + var timestamp time.Time + if currentImage != nil { + timestamp = currentImage.Timestamp + } + return timestamp, err } func (c *Checker) handleLatestOrEmptyTag(log *logrus.Entry, currentTag, currentSHA string, opts *api.Options) { @@ -68,7 +90,7 @@ func (c *Checker) overrideImageURL(log *logrus.Entry, imageURL string, opts *api return imageURL } -func (c *Checker) handleSHA(ctx context.Context, imageURL, statusSHA string, opts *api.Options, usingTag bool, currentTag string) (*Result, error) { +func (c *Checker) handleSHA(ctx context.Context, imageURL, statusSHA string, currentTag string, usingTag bool, opts *api.Options) (*Result, error) { result, err := c.isLatestSHA(ctx, imageURL, statusSHA, opts) if err != nil { return nil, err @@ -98,10 +120,11 @@ func (c *Checker) handleSemver(ctx context.Context, imageURL, statusSHA, current } return &Result{ - CurrentVersion: currentTag, - LatestVersion: latestVersion, - IsLatest: isLatest, - ImageURL: imageURL, + CurrentVersion: currentTag, + LatestVersion: latestVersion, + LatestTimestamp: latestImage.Timestamp, + IsLatest: isLatest, + ImageURL: imageURL, }, nil } @@ -168,7 +191,7 @@ func (c *Checker) isLatestSemver(ctx context.Context, imageURL, currentSHA strin return latestImage, isLatest, nil } -// isLatestSHA will return the the result of whether the given image is the latest, according to image SHA. +// isLatestSHA will return the result of whether the given image is the latest, according to image SHA. func (c *Checker) isLatestSHA(ctx context.Context, imageURL, currentSHA string, opts *api.Options) (*Result, error) { latestImage, err := c.search.LatestImage(ctx, imageURL, opts) if err != nil { @@ -182,10 +205,11 @@ func (c *Checker) isLatestSHA(ctx context.Context, imageURL, currentSHA string, } return &Result{ - CurrentVersion: currentSHA, - LatestVersion: latestVersion, - IsLatest: isLatest, - ImageURL: imageURL, + CurrentVersion: currentSHA, + LatestVersion: latestVersion, + LatestTimestamp: latestImage.Timestamp, + IsLatest: isLatest, + ImageURL: imageURL, }, nil } diff --git a/pkg/controller/checker/checker_test.go b/pkg/controller/checker/checker_test.go index ac5dadd6..8e315274 100644 --- a/pkg/controller/checker/checker_test.go +++ b/pkg/controller/checker/checker_test.go @@ -5,6 +5,8 @@ import ( "reflect" "testing" + "github.com/aws/smithy-go/time" + "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -19,6 +21,7 @@ func TestContainer(t *testing.T) { imageURL string opts *api.Options searchResp *api.ImageTag + imageResp *api.ImageTag expResult *Result }{ "no status sha should return nil, nil": { @@ -28,6 +31,29 @@ func TestContainer(t *testing.T) { searchResp: nil, expResult: nil, }, + "set timestamps from images": { + statusSHA: "localhost:5000/version-checker@sha:123", + imageURL: "localhost:5000/version-checker:v0.1.0", + opts: new(api.Options), + searchResp: &api.ImageTag{ + Tag: "v0.2.0", + SHA: "sha:456", + Timestamp: time.ParseEpochSeconds(7654321), + }, + imageResp: &api.ImageTag{ + Tag: "v0.1.0", + SHA: "sha:123", + Timestamp: time.ParseEpochSeconds(1234567), + }, + expResult: &Result{ + CurrentVersion: "v0.1.0", + LatestVersion: "v0.2.0", + ImageURL: "localhost:5000/version-checker", + IsLatest: false, + LatestTimestamp: time.ParseEpochSeconds(7654321), + CurrentTimestamp: time.ParseEpochSeconds(1234567), + }, + }, "if v0.2.0 is latest version, but different sha, then not latest": { statusSHA: "localhost:5000/version-checker@sha:123", imageURL: "localhost:5000/version-checker:v0.2.0", @@ -263,7 +289,9 @@ func TestContainer(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - checker := New(search.New().With(test.searchResp, nil)) + checker := New(search.New(). + WithLatestImage(test.searchResp, nil). + WithImage(test.imageResp, nil)) pod := &corev1.Pod{ Status: corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{ @@ -285,7 +313,7 @@ func TestContainer(t *testing.T) { } if !reflect.DeepEqual(test.expResult, result) { - t.Errorf("got unexpected result, exp=%#+v got=%#+v", + t.Errorf("got unexpected result,\nexp=%#+v\ngot=%#+v", test.expResult, result) } }) @@ -475,7 +503,7 @@ func TestIsLatestSemver(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - checker := New(search.New().With(test.searchResp, nil)) + checker := New(search.New().WithLatestImage(test.searchResp, nil)) latestImage, isLatest, err := checker.isLatestSemver(context.TODO(), test.imageURL, test.currentSHA, test.currentImage, nil) if err != nil { t.Fatal(err) @@ -530,7 +558,7 @@ func TestIsLatestSHA(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - checker := New(search.New().With(test.searchResp, nil)) + checker := New(search.New().WithLatestImage(test.searchResp, nil)) result, err := checker.isLatestSHA(context.TODO(), test.imageURL, test.currentSHA, nil) if err != nil { t.Fatal(err) diff --git a/pkg/controller/internal/fake/search/search.go b/pkg/controller/internal/fake/search/search.go index 2760f4aa..adbfa02a 100644 --- a/pkg/controller/internal/fake/search/search.go +++ b/pkg/controller/internal/fake/search/search.go @@ -11,18 +11,33 @@ import ( var _ search.Searcher = &FakeSearch{} type FakeSearch struct { + imageF func() (*api.ImageTag, error) latestImageF func() (*api.ImageTag, error) } func New() *FakeSearch { return &FakeSearch{ + imageF: func() (*api.ImageTag, error) { + return nil, nil + }, latestImageF: func() (*api.ImageTag, error) { return nil, nil }, } } -func (f *FakeSearch) With(image *api.ImageTag, err error) *FakeSearch { +func (f *FakeSearch) WithImage(image *api.ImageTag, err error) *FakeSearch { + f.imageF = func() (*api.ImageTag, error) { + return image, err + } + return f +} + +func (f *FakeSearch) Image(context.Context, string, string, string, bool) (*api.ImageTag, error) { + return f.imageF() +} + +func (f *FakeSearch) WithLatestImage(image *api.ImageTag, err error) *FakeSearch { f.latestImageF = func() (*api.ImageTag, error) { return image, err } diff --git a/pkg/controller/search/search.go b/pkg/controller/search/search.go index f5f80a26..f5092ca7 100644 --- a/pkg/controller/search/search.go +++ b/pkg/controller/search/search.go @@ -17,6 +17,9 @@ import ( // Searcher is the interface for Search to facilitate testing. type Searcher interface { Run(time.Duration) + // Image will get the api.ImageTag given an image URL, tag, sha and whether to only compare by sha. + Image(context.Context, string, string, string, bool) (*api.ImageTag, error) + // LatestImage will get the latest image given an image URL and options. LatestImage(context.Context, string, *api.Options) (*api.ImageTag, error) } @@ -49,8 +52,22 @@ func (s *Search) Fetch(ctx context.Context, imageURL string, opts *api.Options) return latestImage, nil } -// LatestImage will get the latestImage image given an image URL and -// options. If not found in the cache, or is too old, then will do a fresh +func (s *Search) Image(ctx context.Context, imageURL, tag, sha string, mustUseSHA bool) (*api.ImageTag, error) { + refs, err := s.versionGetter.AllTagsFromImage(ctx, imageURL) + if err != nil { + return nil, err + } + for _, ref := range refs { + if ref.SHA == sha && sha != "" || // same sha is always fine + ref.Tag == tag && !mustUseSHA { // otherwise compare the tag, if sha use is not required + return &ref, nil + } + } + return nil, nil +} + +// LatestImage will get the latest image given an image URL and options. +// If not found in the cache, or is too old, then will do a fresh // lookup and commit to the cache. func (s *Search) LatestImage(ctx context.Context, imageURL string, opts *api.Options) (*api.ImageTag, error) { hashIndex, err := calculateHashIndex(imageURL, opts) diff --git a/pkg/controller/sync.go b/pkg/controller/sync.go index d422acc6..9101b43e 100644 --- a/pkg/controller/sync.go +++ b/pkg/controller/sync.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -97,6 +98,7 @@ func (c *Controller) checkContainer(ctx context.Context, log *logrus.Entry, pod container.Name, containerType, result.ImageURL, result.IsLatest, result.CurrentVersion, result.LatestVersion, + result.CurrentTimestamp.Format(time.DateOnly), ) return nil diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 090baed6..bc747b2b 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -44,7 +44,7 @@ func New(log *logrus.Entry) *Metrics { Help: "Where the container in use is using the latest upstream registry version", }, []string{ - "namespace", "pod", "container", "container_type", "image", "current_version", "latest_version", + "namespace", "pod", "container", "container_type", "image", "current_version", "latest_version", "last_updated", }, ) @@ -87,7 +87,7 @@ func (m *Metrics) Run(servingAddress string) error { return nil } -func (m *Metrics) AddImage(namespace, pod, container, containerType, imageURL string, isLatest bool, currentVersion, latestVersion string) { +func (m *Metrics) AddImage(namespace, pod, container, containerType, imageURL string, isLatest bool, currentVersion, latestVersion, lastUpdated string) { // Remove old image url/version if it exists m.RemoveImage(namespace, pod, container, containerType) @@ -100,7 +100,7 @@ func (m *Metrics) AddImage(namespace, pod, container, containerType, imageURL st } m.containerImageVersion.With( - m.buildLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion), + m.buildLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion, lastUpdated), ).Set(isLatestF) index := m.latestImageIndex(namespace, pod, container, containerType) @@ -131,7 +131,7 @@ func (m *Metrics) latestImageIndex(namespace, pod, container, containerType stri return strings.Join([]string{namespace, pod, container, containerType}, "") } -func (m *Metrics) buildLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion string) prometheus.Labels { +func (m *Metrics) buildLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion, lastUpdated string) prometheus.Labels { return prometheus.Labels{ "namespace": namespace, "pod": pod, @@ -140,6 +140,7 @@ func (m *Metrics) buildLabels(namespace, pod, container, containerType, imageURL "image": imageURL, "current_version": currentVersion, "latest_version": latestVersion, + "last_updated": lastUpdated, } } diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 518d1780..66e395b8 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -8,17 +8,21 @@ import ( "github.com/sirupsen/logrus" ) +const ( + epoch = "1970-01-01" +) + func TestCache(t *testing.T) { m := New(logrus.NewEntry(logrus.New())) for i, typ := range []string{"init", "container"} { version := fmt.Sprintf("0.1.%d", i) - m.AddImage("namespace", "pod", "container", typ, "url", true, version, version) + m.AddImage("namespace", "pod", "container", typ, "url", true, version, version, epoch) } for i, typ := range []string{"init", "container"} { version := fmt.Sprintf("0.1.%d", i) - mt, _ := m.containerImageVersion.GetMetricWith(m.buildLabels("namespace", "pod", "container", typ, "url", version, version)) + mt, _ := m.containerImageVersion.GetMetricWith(m.buildLabels("namespace", "pod", "container", typ, "url", version, version, epoch)) count := testutil.ToFloat64(mt) if count != 1 { t.Error("Should have added metric") @@ -30,7 +34,7 @@ func TestCache(t *testing.T) { } for i, typ := range []string{"init", "container"} { version := fmt.Sprintf("0.1.%d", i) - mt, _ := m.containerImageVersion.GetMetricWith(m.buildLabels("namespace", "pod", "container", typ, "url", version, version)) + mt, _ := m.containerImageVersion.GetMetricWith(m.buildLabels("namespace", "pod", "container", typ, "url", version, version, epoch)) count := testutil.ToFloat64(mt) if count != 0 { t.Error("Should have removed metric") diff --git a/pkg/version/version.go b/pkg/version/version.go index db0f33db..676cb20f 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -41,14 +41,22 @@ func (v *Version) Run(refreshRate time.Duration) { v.imageCache.StartGarbageCollector(refreshRate) } +// AllTagsFromImage will return all tags given an imageURL. +func (v *Version) AllTagsFromImage(ctx context.Context, imageURL string) ([]api.ImageTag, error) { + if tagsI, err := v.imageCache.Get(ctx, imageURL, imageURL, nil); err != nil { + return nil, err + } else { + return tagsI.([]api.ImageTag), err + } +} + // LatestTagFromImage will return the latest tag given an imageURL, according // to the given options. func (v *Version) LatestTagFromImage(ctx context.Context, imageURL string, opts *api.Options) (*api.ImageTag, error) { - tagsI, err := v.imageCache.Get(ctx, imageURL, imageURL, nil) + tags, err := v.AllTagsFromImage(ctx, imageURL) if err != nil { return nil, err } - tags := tagsI.([]api.ImageTag) var tag *api.ImageTag