diff --git a/controllers/imagerepository_controller.go b/controllers/imagerepository_controller.go index 0bb47b7..07567a2 100644 --- a/controllers/imagerepository_controller.go +++ b/controllers/imagerepository_controller.go @@ -31,7 +31,9 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/recorder" + "github.com/fluxcd/pkg/runtime/predicates" imagev1alpha1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1" ) @@ -169,11 +171,21 @@ func (r *ImageRepositoryReconciler) shouldScan(repo imagev1alpha1.ImageRepositor scanInterval = repo.Spec.ScanInterval.Duration } + // never scanned; do it now lastTransitionTime := imagev1alpha1.GetLastTransitionTime(repo) if lastTransitionTime == nil { return true, scanInterval } + // allow for the "reconcile at" annotation + if syncAt, ok := repo.GetAnnotations()[meta.ReconcileAtAnnotation]; ok { + if t, err := time.Parse(time.RFC3339Nano, syncAt); err == nil && t.Before(now) { + return true, scanInterval + } else if err == nil { + return false, t.Sub(now) + } + } + // when recovering, it's possible that the resource has a last // scan time, but there's no records because the database has been // dropped and created again. @@ -195,5 +207,6 @@ func (r *ImageRepositoryReconciler) shouldScan(repo imagev1alpha1.ImageRepositor func (r *ImageRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&imagev1alpha1.ImageRepository{}). + WithEventFilter(predicates.ChangePredicate{}). Complete(r) } diff --git a/controllers/scan_test.go b/controllers/scan_test.go index 906c86a..0515b0e 100644 --- a/controllers/scan_test.go +++ b/controllers/scan_test.go @@ -29,6 +29,7 @@ import ( "k8s.io/apimachinery/pkg/types" imagev1alpha1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1" + "github.com/fluxcd/pkg/apis/meta" // +kubebuilder:scaffold:imports ) @@ -147,6 +148,50 @@ var _ = Describe("ImageRepository controller", func() { }) }) + Context("when the ImageRepository gets a 'reconcile at' annotation", func() { + It("scans right away", func() { + imgRepo := loadImages("test-fetch", []string{"1.0.0"}) + + repo = imagev1alpha1.ImageRepository{ + Spec: imagev1alpha1.ImageRepositorySpec{ + Image: imgRepo, + }, + } + objectName := types.NamespacedName{ + Name: "random", + Namespace: "default", + } + + repo.Name = objectName.Name + repo.Namespace = objectName.Namespace + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + r := imageRepoReconciler + err := r.Create(ctx, &repo) + Expect(err).ToNot(HaveOccurred()) + + // It'll get scanned on creation + var repoAfter imagev1alpha1.ImageRepository + Eventually(func() bool { + err := r.Get(context.Background(), objectName, &repoAfter) + return err == nil && repoAfter.Status.CanonicalImageName != "" + }, timeout, interval).Should(BeTrue()) + + lastScan := imagev1alpha1.GetLastTransitionTime(repoAfter) + Expect(lastScan).ToNot(BeNil()) + + repoAfter.Annotations = map[string]string{ + meta.ReconcileAtAnnotation: time.Now().Add(-time.Minute).Format(time.RFC3339Nano), + } + Expect(r.Update(ctx, &repoAfter)).To(Succeed()) + Eventually(func() bool { + err := r.Get(context.Background(), objectName, &repoAfter) + return err == nil && imagev1alpha1.GetLastTransitionTime(repoAfter).After(lastScan.Time) + }, timeout, interval).Should(BeTrue()) + }) + }) }) // loadImages uploads images to the local registry, and returns the diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 2848a8f..2b64d3d 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -83,9 +83,6 @@ var _ = BeforeSuite(func(done Done) { err = imagev1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) - err = imagev1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - // +kubebuilder:scaffold:scheme k8sMgr, err = ctrl.NewManager(cfg, ctrl.Options{ @@ -140,7 +137,7 @@ var _ = AfterSuite(func() { // --- -// the go-containerregistry test regsitry implementation does not +// the go-containerregistry test registry implementation does not // serve /myimage/tags/list. Until it does, I'm adding this handler. // NB: // - assumes repo name is a single element diff --git a/go.mod b/go.mod index 121ba97..83a0f9e 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,16 @@ replace github.com/fluxcd/image-reflector-controller/api => ./api require ( github.com/Masterminds/semver/v3 v3.1.0 github.com/fluxcd/image-reflector-controller/api v0.0.0-00010101000000-000000000000 + github.com/fluxcd/pkg/apis/meta v0.0.2 github.com/fluxcd/pkg/recorder v0.0.5 + github.com/fluxcd/pkg/runtime v0.1.0 github.com/go-logr/logr v0.1.0 github.com/google/go-containerregistry v0.1.1 github.com/onsi/ginkgo v1.12.1 github.com/onsi/gomega v1.10.1 go.uber.org/zap v1.10.0 - k8s.io/api v0.18.6 - k8s.io/apimachinery v0.18.6 + k8s.io/api v0.18.9 + k8s.io/apimachinery v0.18.9 k8s.io/client-go v0.18.6 - sigs.k8s.io/controller-runtime v0.6.2 + sigs.k8s.io/controller-runtime v0.6.3 ) diff --git a/go.sum b/go.sum index e4f2087..60c390d 100644 --- a/go.sum +++ b/go.sum @@ -196,10 +196,17 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fluxcd/pkg v0.0.4 h1:fMA6GG3FTSBFDrlB026gQJhxj4xVuvcvwP3rhX/Yqrw= +github.com/fluxcd/pkg/apis/meta v0.0.2 h1:kyA4Y0IzNjf1joBOnFqpWG7aNDHvtLExZcaHQM7qhRI= +github.com/fluxcd/pkg/apis/meta v0.0.2/go.mod h1:nCNps5JJOcEQr3MNDmZqI4o0chjePSUYL6Q2ktDtotU= github.com/fluxcd/pkg/recorder v0.0.5 h1:D8qfupahIvh6ncCMn2yTHsrzG91S05sp4zdpsbKWeaU= github.com/fluxcd/pkg/recorder v0.0.5/go.mod h1:2UG6EroZ6ZbqmqoL8k/cQMe09e6A36WyH4t4UDUGyuU= +github.com/fluxcd/pkg/runtime v0.1.0 h1:mCLj5GlQZqWtK3tvtZTmfgFOLsTUY1iqg3CmEyS1nRs= +github.com/fluxcd/pkg/runtime v0.1.0/go.mod h1:OXkrYtDLw3GhclbzvnzfSktUyxRmC3FFhXj0tVVaIX8= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -415,6 +422,8 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.6.7 h1:8/CAEZt/+F7kR7GevNHulKkUjLht3CPmn7egmhieNKo= +github.com/hashicorp/go-retryablehttp v0.6.7/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -1087,12 +1096,16 @@ k8s.io/api v0.17.4/go.mod h1:5qxx6vjmwUVG2nHQTKGlLts8Tbok8PzHl4vHtVFuZCA= k8s.io/api v0.18.4/go.mod h1:lOIQAKYgai1+vz9J7YcDZwC26Z0zQewYOGWdyIPUUQ4= k8s.io/api v0.18.6 h1:osqrAXbOQjkKIWDTjrqxWQ3w0GkKb1KA1XkUGHHYpeE= k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI= +k8s.io/api v0.18.9 h1:7VDtivqwbvLOf8hmXSd/PDSSbpCBq49MELg84EYBYiQ= +k8s.io/api v0.18.9/go.mod h1:9u/h6sUh6FxfErv7QqetX1EB3yBMIYOBXzdcf0Gf0rc= k8s.io/apiextensions-apiserver v0.18.6 h1:vDlk7cyFsDyfwn2rNAO2DbmUbvXy5yT5GE3rrqOzaMo= k8s.io/apiextensions-apiserver v0.18.6/go.mod h1:lv89S7fUysXjLZO7ke783xOwVTm6lKizADfvUM/SS/M= k8s.io/apimachinery v0.17.4/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= k8s.io/apimachinery v0.18.4/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= k8s.io/apimachinery v0.18.6 h1:RtFHnfGNfd1N0LeSrKCUznz5xtUP1elRGvHJbL3Ntag= k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= +k8s.io/apimachinery v0.18.9 h1:3ZABKQx3F3xPWlsGhCfUl8W+JXRRblV6Wo2A3zn0pvY= +k8s.io/apimachinery v0.18.9/go.mod h1:PF5taHbXgTEJLU+xMypMmYTXTWPJ5LaW8bfsisxnEXk= k8s.io/apiserver v0.17.4/go.mod h1:5ZDQ6Xr5MNBxyi3iUZXS84QOhZl+W7Oq2us/29c0j9I= k8s.io/apiserver v0.18.6/go.mod h1:Zt2XvTHuaZjBz6EFYzpp+X4hTmgWGy8AthNVnTdm3Wg= k8s.io/client-go v0.17.4/go.mod h1:ouF6o5pz3is8qU0/qYL2RnoxOPqgfuidYLowytyLJmc= @@ -1137,6 +1150,8 @@ rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= sigs.k8s.io/controller-runtime v0.6.2 h1:jkAnfdTYBpFwlmBn3pS5HFO06SfxvnTZ1p5PeEF/zAA= sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= +sigs.k8s.io/controller-runtime v0.6.3 h1:SBbr+inLPEKhvlJtrvDcwIpm+uhDvp63Bl72xYJtoOE= +sigs.k8s.io/controller-runtime v0.6.3/go.mod h1:WlZNXcM0++oyaQt4B7C2lEE5JYRs8vJUzRP4N4JpdAY= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=