Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Alphabetical order policy #58

Merged
merged 1 commit into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions api/v1alpha1/imagepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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 @@ -55,6 +58,17 @@ 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 the letters of the
// alphabet as tags, ascending order would select Z, and descending order
// would select A.
// +kubebuilder:default:="asc"
// +kubebuilder:validation:Enum=asc;desc
// +optional
Order string `json:"order,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.

14 changes: 14 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,20 @@ 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:
order:
default: asc
description: Order specifies the sorting order of the tags.
Given the letters of the alphabet as tags, ascending order
would select Z, and descending order would select A.
enum:
- asc
- desc
type: string
type: object
semver:
description: SemVer gives a semantic version range to check against
the tags available.
Expand Down
41 changes: 9 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"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -39,9 +38,9 @@ import (
"github.com/fluxcd/pkg/apis/meta"
"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 @@ -122,12 +121,14 @@ func (r *ImagePolicyReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error)
return ctrl.Result{}, nil
}

policy := pol.Spec.Policy
policer, err := policy.PolicerFromSpec(pol.Spec.Policy)

var latest string
var err error
switch {
case policy.SemVer != nil:
latest, err = r.calculateLatestImageSemver(&policy, repo.Status.CanonicalImageName)
if policer != nil {
tags, err := r.Database.Tags(repo.Status.CanonicalImageName)
if err == nil {
latest, err = policer.Latest(tags)
}
}
if err != nil {
imagev1alpha1.SetImagePolicyReadiness(
Expand Down Expand Up @@ -174,7 +175,7 @@ func (r *ImagePolicyReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error)
}
r.event(pol, events.EventSeverityInfo, msg)

return ctrl.Result{}, nil
return ctrl.Result{}, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure of the intent of this change, though it doesn't change the meaning of the code as it stands.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely a relic from previous changes made that are no longer part of this PR. Should probably be changed back to nil.

}

func (r *ImagePolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
Expand All @@ -199,30 +200,6 @@ func (r *ImagePolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {

// ---

func (r *ImagePolicyReconciler) calculateLatestImageSemver(pol *imagev1alpha1.ImagePolicyChoice, canonImage string) (string, error) {
tags, err := r.Database.Tags(canonImage)
if err != nil {
return "", fmt.Errorf("failed to read images for %q: %w", canonImage, err)
}
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
200 changes: 132 additions & 68 deletions controllers/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,76 +35,140 @@ 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())

Eventually(func() bool {
err := r.Get(ctx, imageObjectName, &repo)
return err == nil && repo.Status.LastScanResult != nil
}, timeout, interval).Should(BeTrue())
Expect(repo.Status.CanonicalImageName).To(Equal(imgRepo))
Expect(repo.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())

Eventually(func() bool {
err := r.Get(ctx, imageObjectName, &repo)
return err == nil && repo.Status.LastScanResult != nil
}, timeout, interval).Should(BeTrue())
Expect(repo.Status.CanonicalImageName).To(Equal(imgRepo))
Expect(repo.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 != 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