Calculate latest image for (semver) policy

This adds the details of calculating the latest image for a policy. It
relies on the ImageRepository and ImagePolicy controllers having a
shared database of image tags. Usually, this sort of thing would be
objects in the Kubernetes database; but since tags (and images) can
number in the tens of thousands per image, I'm using a separate
database. For the minute, it's just a map.
This commit is contained in:
Michael Bridgen 2020-07-13 08:16:23 +01:00
parent 46cd9cbab1
commit a2b0bd4ed7
6 changed files with 141 additions and 13 deletions

45
controllers/database.go Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"sync"
)
type database struct {
mu sync.RWMutex
repoTags map[string][]string
}
func NewDatabase() *database {
return &database{
repoTags: map[string][]string{},
}
}
func (db *database) Tags(repo string) []string {
db.mu.RLock()
tags := db.repoTags[repo]
db.mu.RUnlock()
return tags
}
func (db *database) SetTags(repo string, tags []string) {
db.mu.Lock()
db.repoTags[repo] = tags
db.mu.Unlock()
}

View File

@ -19,29 +19,74 @@ package controllers
import (
"context"
semver "github.com/Masterminds/semver/v3"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
imagev1alpha1 "github.com/squaremo/image-update/api/v1alpha1"
)
type DatabaseReader interface {
Tags(repo string) []string
}
// ImagePolicyReconciler reconciles a ImagePolicy object
type ImagePolicyReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Log logr.Logger
Scheme *runtime.Scheme
Database DatabaseReader
}
// +kubebuilder:rbac:groups=image.fluxcd.io,resources=imagepolicies,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=image.fluxcd.io,resources=imagepolicies/status,verbs=get;update;patch
func (r *ImagePolicyReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
_ = context.Background()
_ = r.Log.WithValues("imagepolicy", req.NamespacedName)
ctx := context.Background()
log := r.Log.WithValues("imagepolicy", req.NamespacedName)
// your logic here
var pol imagev1alpha1.ImagePolicy
if err := r.Get(ctx, req.NamespacedName, &pol); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
var repo imagev1alpha1.ImageRepository
if err := r.Get(ctx, types.NamespacedName{
Namespace: pol.Namespace,
Name: pol.Spec.ImageRepository.Name,
}, &repo); err != nil {
if client.IgnoreNotFound(err) == nil {
log.Error(err, "referenced ImageRepository does not exist")
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// if the image repo hasn't been scanned, don't bother
if repo.Status.CanonicalImageName == "" {
log.Info("referenced ImageRepository has not been scanned yet")
return ctrl.Result{}, nil
}
policy := pol.Spec.Policy
switch {
case policy.SemVer != nil:
latest, err := r.calculateLatestImageSemver(&policy, repo.Status.CanonicalImageName)
if err != nil {
return ctrl.Result{}, err
}
if latest != "" {
pol.Status.LatestImage = repo.Spec.Image + ":" + latest
err = r.Status().Update(ctx, &pol)
}
return ctrl.Result{}, err
default:
// no recognised policy, do nothing
}
return ctrl.Result{}, nil
}
@ -51,3 +96,26 @@ func (r *ImagePolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
For(&imagev1alpha1.ImagePolicy{}).
Complete(r)
}
// ---
func (r *ImagePolicyReconciler) calculateLatestImageSemver(pol *imagev1alpha1.ImagePolicyChoice, canonImage string) (string, error) {
tags := r.Database.Tags(canonImage)
constraint, err := semver.NewConstraint(pol.SemVer.Range)
if err != nil {
// FIXME this'll get a stack trace in the log, but may not deserve it
return "", err
}
var latestVersion *semver.Version
for _, tag := range tags {
if v, err := semver.NewVersion(tag); err == nil {
if constraint.Check(v) && (latestVersion == nil || v.GreaterThan(latestVersion)) {
latestVersion = v
}
}
}
if latestVersion != nil {
return latestVersion.Original(), nil
}
return "", nil
}

View File

@ -36,11 +36,16 @@ const (
defaultScanInterval = 10 * time.Minute
)
type DatabaseWriter interface {
SetTags(repo string, tags []string)
}
// ImageRepositoryReconciler reconciles a ImageRepository object
type ImageRepositoryReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Log logr.Logger
Scheme *runtime.Scheme
Database DatabaseWriter
}
// +kubebuilder:rbac:groups=image.fluxcd.io,resources=imagerepositories,verbs=get;list;watch;create;update;patch;delete
@ -86,6 +91,8 @@ func (r *ImageRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, er
imageRepo.Status.LastScanTime = &metav1.Time{Time: now}
imageRepo.Status.LastScanResult.TagCount = len(tags)
imageRepo.Status.LastError = ""
// share the information in the database
r.Database.SetTags(canonicalName, tags)
log.Info("successful scan", "tag count", len(tags))
if err = r.Status().Update(ctx, &imageRepo); err != nil {
return ctrl.Result{}, err

1
go.mod
View File

@ -3,6 +3,7 @@ module github.com/squaremo/image-update
go 1.13
require (
github.com/Masterminds/semver/v3 v3.1.0
github.com/go-logr/logr v0.1.0
github.com/google/go-containerregistry v0.1.1
github.com/onsi/ginkgo v1.12.0

3
go.sum
View File

@ -79,8 +79,10 @@ github.com/Djarvur/go-err113 v0.0.0-20200410182137-af658d038157/go.mod h1:4UJr5H
github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo=
github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
@ -1135,6 +1137,7 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/controller-runtime v0.5.0 h1:CbqIy5fbUX+4E9bpnBFd204YAzRYlM9SWW77BbrcDQo=
sigs.k8s.io/controller-runtime v0.5.0/go.mod h1:REiJzC7Y00U+2YkMbT8wxgrsX5USpXKGhb2sCtAXiT8=
sigs.k8s.io/controller-runtime v0.6.1 h1:LcK2+nk0kmaOnKGN+vBcWHqY5WDJNJNB/c5pW+sU8fc=
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/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=

16
main.go
View File

@ -66,18 +66,22 @@ func main() {
os.Exit(1)
}
db := controllers.NewDatabase()
if err = (&controllers.ImageRepositoryReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ImageRepository"),
Scheme: mgr.GetScheme(),
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ImageRepository"),
Scheme: mgr.GetScheme(),
Database: db,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ImageRepository")
os.Exit(1)
}
if err = (&controllers.ImagePolicyReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ImagePolicy"),
Scheme: mgr.GetScheme(),
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ImagePolicy"),
Scheme: mgr.GetScheme(),
Database: db,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ImagePolicy")
os.Exit(1)