Skip to content

Commit

Permalink
Implement Alphabetical order policy
Browse files Browse the repository at this point in the history
This implementation allows one to set a `MatchTagPrefix` to filter a
list of tags and/or use `Order` to set the ordering rule by which tags
are evaluated.

Signed-off-by: Aurel Canciu <[email protected]>
  • Loading branch information
relu committed Nov 25, 2020
1 parent b090d83 commit 3969f81
Show file tree
Hide file tree
Showing 14 changed files with 757 additions and 132 deletions.
17 changes: 17 additions & 0 deletions api/v1alpha1/imagepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type ImagePolicyChoice struct {
// available.
// +optional
SemVer *SemVerPolicy `json:"semver,omitempty"`
// Alphabetical set of rules to use for alphabetical ordering of the tags.
// +optional
Alphabetical *AlphabeticalPolicy `json:"alphabetical,omitempty"`
}

// SemVerPolicy specifices a semantic version policy.
Expand All @@ -53,6 +56,20 @@ type SemVerPolicy struct {
Range string `json:"range"`
}

// AlphabeticalPolicy specifices a alphabetical ordering policy.
type AlphabeticalPolicy struct {
// Order specifies the sorting order of the tags. Given a list of single
// character strings reprezenting the alphabet, ascending order will return
// the value 'z', while descending order will return the value 'a'.
// +kubebuilder:validation:Enum=asc;desc
// +optional
Order string `json:"reverse,omitempty"`
// MatchTagPrefix filters out the tags that do not match the specified
// literal prefix.
// +optional
MatchTagPrefix string `json:"prefix,omitempty"`
}

// ImagePolicyStatus defines the observed state of ImagePolicy
type ImagePolicyStatus struct {
// LatestImage gives the first in the list of images scanned by
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ spec:
description: Policy gives the particulars of the policy to be followed
in selecting the most recent image
properties:
alphabetical:
description: Alphabetical set of rules to use for alphabetical
ordering of the tags.
properties:
prefix:
description: MatchTagPrefix filters out the tags that do not
match the specified literal prefix.
type: string
reverse:
description: Order specifies the sorting order of the tags.
Given a list of single character strings reprezenting the
alphabet, ascending order will return the value 'z', while
descending order will return the value 'a'.
enum:
- asc
- desc
type: string
type: object
semver:
description: SemVer gives a semantic version range to check against
the tags available.
Expand Down
42 changes: 10 additions & 32 deletions controllers/imagepolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"strings"
"time"

semver "github.com/Masterminds/semver/v3"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -36,9 +35,9 @@ import (

"github.com/fluxcd/pkg/runtime/events"
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/version"

imagev1alpha1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
"github.com/fluxcd/image-reflector-controller/internal/policy"
)

// this is used as the key for the index of policy->repository; the
Expand Down Expand Up @@ -103,12 +102,12 @@ func (r *ImagePolicyReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error)
return ctrl.Result{}, nil
}

policy := pol.Spec.Policy
resolver, err := policy.Policy(pol.Spec.Policy)

var latest string
var err error
switch {
case policy.SemVer != nil:
latest, err = r.calculateLatestImageSemver(&policy, repo.Status.CanonicalImageName)
if resolver != nil {
tags := r.Database.Tags(repo.Status.CanonicalImageName)
latest, err = resolver.Latest(tags)
}
if err != nil {
r.event(pol, events.EventSeverityError, err.Error())
Expand All @@ -117,14 +116,14 @@ func (r *ImagePolicyReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error)

if latest != "" {
pol.Status.LatestImage = repo.Spec.Image + ":" + latest
if err := r.Status().Update(ctx, &pol); err != nil {
if err = r.Status().Update(ctx, &pol); err != nil {
r.event(pol, events.EventSeverityError, err.Error())
return ctrl.Result{}, err
} else {
r.event(pol, events.EventSeverityInfo, fmt.Sprintf("Latest image tag for '%s' resolved to: %s", repo.Spec.Image, latest))
}
r.event(pol, events.EventSeverityInfo, fmt.Sprintf("Latest image tag for '%s' resolved to: %s", repo.Spec.Image, latest))
}

return ctrl.Result{}, nil
return ctrl.Result{}, err
}

func (r *ImagePolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
Expand All @@ -149,27 +148,6 @@ func (r *ImagePolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {

// ---

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 := version.ParseVersion(tag); err == nil {
if constraint.Check(v) && (latestVersion == nil || v.GreaterThan(latestVersion)) {
latestVersion = v
}
}
}
if latestVersion != nil {
return latestVersion.Original(), nil
}
return "", nil
}

func (r *ImagePolicyReconciler) imagePoliciesForRepository(obj handler.MapObject) []reconcile.Request {
ctx := context.Background()
var policies imagev1alpha1.ImagePolicyList
Expand Down
202 changes: 134 additions & 68 deletions controllers/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,76 +35,142 @@ import (

var _ = Describe("ImagePolicy controller", func() {

var registryServer *httptest.Server
Context("calculates an image from a repository's tags", func() {
var registryServer *httptest.Server

BeforeEach(func() {
registryServer = newRegistryServer()
})

AfterEach(func() {
registryServer.Close()
})

When("Using SemVerPolicy", func() {
It("calculates an image from a repository's tags", func() {
versions := []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
imgRepo := loadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)

repo := imagev1alpha1.ImageRepository{
Spec: imagev1alpha1.ImageRepositorySpec{
Interval: metav1.Duration{Duration: reconciliationInterval},
Image: imgRepo,
},
}
imageObjectName := types.NamespacedName{
Name: "polimage-" + randStringRunes(5),
Namespace: "default",
}
repo.Name = imageObjectName.Name
repo.Namespace = imageObjectName.Namespace

ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
defer cancel()

r := imageRepoReconciler
Expect(r.Create(ctx, &repo)).To(Succeed())

var repoAfter imagev1alpha1.ImageRepository
Eventually(func() bool {
err := r.Get(ctx, imageObjectName, &repoAfter)
return err == nil && repoAfter.Status.LastScanResult.ScanTime != nil
}, timeout, interval).Should(BeTrue())
Expect(repoAfter.Status.CanonicalImageName).To(Equal(imgRepo))
Expect(repoAfter.Status.LastScanResult.TagCount).To(Equal(len(versions)))

polName := types.NamespacedName{
Name: "random-pol-" + randStringRunes(5),
Namespace: imageObjectName.Namespace,
}
pol := imagev1alpha1.ImagePolicy{
Spec: imagev1alpha1.ImagePolicySpec{
ImageRepositoryRef: corev1.LocalObjectReference{
Name: imageObjectName.Name,
},
Policy: imagev1alpha1.ImagePolicyChoice{
SemVer: &imagev1alpha1.SemVerPolicy{
Range: "1.0.x",
},
},
},
}
pol.Namespace = polName.Namespace
pol.Name = polName.Name

ctx, cancel = context.WithTimeout(context.Background(), contextTimeout)
defer cancel()

Expect(r.Create(ctx, &pol)).To(Succeed())

Eventually(func() bool {
err := r.Get(ctx, polName, &pol)
return err == nil && pol.Status.LatestImage != ""
}, timeout, interval).Should(BeTrue())
Expect(pol.Status.LatestImage).To(Equal(imgRepo + ":1.0.2"))

Expect(r.Delete(ctx, &pol)).To(Succeed())
})
})

When("Usign AlphabeticalPolicy", func() {
It("calculates an image from a repository's tags", func() {
versions := []string{"xenial", "yakkety", "zesty", "artful", "bionic"}
imgRepo := loadImages(registryServer, "test-alphabetical-policy-"+randStringRunes(5), versions)

repo := imagev1alpha1.ImageRepository{
Spec: imagev1alpha1.ImageRepositorySpec{
Interval: metav1.Duration{Duration: reconciliationInterval},
Image: imgRepo,
},
}
imageObjectName := types.NamespacedName{
Name: "polimage-" + randStringRunes(5),
Namespace: "default",
}
repo.Name = imageObjectName.Name
repo.Namespace = imageObjectName.Namespace

ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
defer cancel()

r := imageRepoReconciler
Expect(r.Create(ctx, &repo)).To(Succeed())

var repoAfter imagev1alpha1.ImageRepository
Eventually(func() bool {
err := r.Get(ctx, imageObjectName, &repoAfter)
return err == nil && repoAfter.Status.LastScanResult.ScanTime != nil
}, timeout, interval).Should(BeTrue())
Expect(repoAfter.Status.CanonicalImageName).To(Equal(imgRepo))
Expect(repoAfter.Status.LastScanResult.TagCount).To(Equal(len(versions)))

polName := types.NamespacedName{
Name: "random-pol-" + randStringRunes(5),
Namespace: imageObjectName.Namespace,
}
pol := imagev1alpha1.ImagePolicy{
Spec: imagev1alpha1.ImagePolicySpec{
ImageRepositoryRef: corev1.LocalObjectReference{
Name: imageObjectName.Name,
},
Policy: imagev1alpha1.ImagePolicyChoice{
Alphabetical: &imagev1alpha1.AlphabeticalPolicy{},
},
},
}
pol.Namespace = polName.Namespace
pol.Name = polName.Name

BeforeEach(func() {
registryServer = newRegistryServer()
})
Expect(r.Create(ctx, &pol)).To(Succeed())

AfterEach(func() {
registryServer.Close()
})
Eventually(func() bool {
err := r.Get(ctx, polName, &pol)
return err == nil && pol.Status.LatestImage != ""
}, timeout, interval).Should(BeTrue())
Expect(pol.Status.LatestImage).To(Equal(imgRepo + ":zesty"))

It("calculates an image from a repository's tags", func() {
versions := []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
imgRepo := loadImages(registryServer, "test-semver-policy", versions)

repo := imagev1alpha1.ImageRepository{
Spec: imagev1alpha1.ImageRepositorySpec{
Interval: metav1.Duration{Duration: reconciliationInterval},
Image: imgRepo,
},
}
imageObjectName := types.NamespacedName{
Name: "polimage",
Namespace: "default",
}
repo.Name = imageObjectName.Name
repo.Namespace = imageObjectName.Namespace

ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
defer cancel()

r := imageRepoReconciler
Expect(r.Create(ctx, &repo)).To(Succeed())

var repoAfter imagev1alpha1.ImageRepository
Eventually(func() bool {
err := r.Get(ctx, imageObjectName, &repoAfter)
return err == nil && repoAfter.Status.LastScanResult.ScanTime != nil
}, timeout, interval).Should(BeTrue())
Expect(repoAfter.Status.CanonicalImageName).To(Equal(imgRepo))
Expect(repoAfter.Status.LastScanResult.TagCount).To(Equal(len(versions)))

polName := types.NamespacedName{
Name: "random-pol",
Namespace: imageObjectName.Namespace,
}
pol := imagev1alpha1.ImagePolicy{
Spec: imagev1alpha1.ImagePolicySpec{
ImageRepositoryRef: corev1.LocalObjectReference{
Name: imageObjectName.Name,
},
Policy: imagev1alpha1.ImagePolicyChoice{
SemVer: &imagev1alpha1.SemVerPolicy{
Range: "1.0.x",
},
},
},
}
pol.Namespace = polName.Namespace
pol.Name = polName.Name

ctx, cancel = context.WithTimeout(context.Background(), contextTimeout)
defer cancel()

Expect(r.Create(ctx, &pol)).To(Succeed())

var polAfter imagev1alpha1.ImagePolicy
Eventually(func() bool {
err := r.Get(ctx, polName, &polAfter)
return err == nil && polAfter.Status.LatestImage != ""
}, timeout, interval).Should(BeTrue())
Expect(polAfter.Status.LatestImage).To(Equal(imgRepo + ":1.0.2"))
Expect(r.Delete(ctx, &pol)).To(Succeed())
})
})
})
})
Loading

0 comments on commit 3969f81

Please sign in to comment.