Skip to content

Commit

Permalink
Implement policy-level tag prefix matching
Browse files Browse the repository at this point in the history
Tag prefix matching allows the user to filter tags based on
include/exclude criteria and offers the possibility to enable prefix
trimming prior to policy evaluation.

Signed-off-by: Aurel Canciu <[email protected]>
  • Loading branch information
relu committed Dec 9, 2020
1 parent a6a5ffb commit 3dd2d2d
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 24 deletions.
21 changes: 21 additions & 0 deletions api/v1alpha1/imagepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type ImagePolicySpec struct {
// selecting the most recent image
// +required
Policy ImagePolicyChoice `json:"policy"`
// TagPrefixMatcher enables filtering for only a subset of tags based on their
// literal prefix. If not provided, all the tags from the repository will be
// ordered and compared.
// +optional
TagPrefixMatcher *TagPrefixMatcher `json:"tagPrefxMatcher,omitempty"`
}

// ImagePolicyChoice is a union of all the types of policy that can be
Expand Down Expand Up @@ -69,6 +74,22 @@ type AlphabeticalPolicy struct {
Order string `json:"order,omitempty"`
}

// TagPrefixMatcher enables filtering tags based on a set of defined rules
type TagPrefixMatcher struct {
// Include specifies a set of literal tag prefixes used to filter in tags
// from the given list.
// +optional
Include []string `json:"include"`
// Exclude specifies a set of literal tag prefixes used to filter out tags
// from the given list.
// +optional
Exclude []string `json:"exclude"`
// Trim instructs the policy to remove the specified include matched
// prefixes prior to running the sort evaluation.
// +optional
Trim bool `json:"trim"`
}

// ImagePolicyStatus defines the observed state of ImagePolicy
type ImagePolicyStatus struct {
// LatestImage gives the first in the list of images scanned by
Expand Down
30 changes: 30 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.

22 changes: 22 additions & 0 deletions config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ spec:
- range
type: object
type: object
tagPrefxMatcher:
description: TagPrefixMatcher enables filtering for only a subset
of tags based on their literal prefix. If not provided, all the
tags from the repository will be ordered and compared.
properties:
exclude:
description: Exclude specifies a set of literal tag prefixes used
to filter out tags from the given list.
items:
type: string
type: array
include:
description: Include specifies a set of literal tag prefixes used
to filter in tags from the given list.
items:
type: string
type: array
trim:
description: Trim instructs the policy to remove the specified
include matched prefixes prior to running the sort evaluation.
type: boolean
type: object
required:
- imageRepositoryRef
- policy
Expand Down
6 changes: 4 additions & 2 deletions controllers/imagepolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,10 @@ func (r *ImagePolicyReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error)

var latest string
if policer != nil {
tags := r.Database.Tags(repo.Status.CanonicalImageName)
latest, err = policer.Latest(tags)
tags, err := r.Database.Tags(repo.Status.CanonicalImageName)
if err == nil {
latest, err = policer.Latest(tags, pol.Spec.TagPrefixMatcher)
}
}
if err != nil {
imagev1alpha1.SetImagePolicyReadiness(
Expand Down
35 changes: 32 additions & 3 deletions internal/policy/alphabetical.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package policy
import (
"fmt"
"sort"
"strings"

imagev1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
)

const (
Expand Down Expand Up @@ -51,17 +54,43 @@ func NewAlphabetical(order string) (*Alphabetical, error) {
}

// Latest returns latest version from a provided list of strings
func (p *Alphabetical) Latest(versions []string) (string, error) {
func (p *Alphabetical) Latest(versions []string, matcher *imagev1.TagPrefixMatcher) (string, error) {
if len(versions) == 0 {
return "", fmt.Errorf("version list argument cannot be empty")
}

sorted := sort.StringSlice(versions)
if matcher != nil {
versions = PrefixMatchFilter(versions, matcher.Include, matcher.Exclude)
}

var sorted sort.StringSlice
if matcher != nil && matcher.Trim && len(matcher.Include) > 0 {
for _, tag := range versions {
processed := tag
for _, in := range matcher.Include {
processed = strings.TrimPrefix(processed, in)
}
sorted = append(sorted, processed)
}
} else {
sorted = versions
}

if p.Order == AlphabeticalOrderDesc {
sort.Sort(sorted)
} else {
sort.Sort(sort.Reverse(sorted))
}

return sorted[0], nil
latest := sorted[0]
if matcher != nil && matcher.Trim && len(matcher.Include) > 0 {
for _, tag := range versions {
for _, in := range matcher.Include {
if latest == strings.TrimPrefix(tag, in) {
return tag, nil
}
}
}
}
return latest, nil
}
44 changes: 35 additions & 9 deletions internal/policy/alphabetical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package policy

import (
"testing"

imagev1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
)

func TestNewAlphabetical(t *testing.T) {
Expand Down Expand Up @@ -63,48 +65,72 @@ func TestAlphabetical_Latest(t *testing.T) {
label string
order string
versions []string
match *imagev1.TagPrefixMatcher
expectedVersion string
expectErr bool
}{
{
label: "Ubuntu CalVer",
label: "With Ubuntu CalVer",
versions: []string{"16.04", "16.04.1", "16.10", "20.04", "20.10"},
expectedVersion: "20.10",
},

{
label: "Ubuntu CalVer descending",
label: "With Ubuntu CalVer prefix include",
versions: []string{"16.04", "16.04.1", "16.10", "20.04", "20.10"},
match: &imagev1.TagPrefixMatcher{Include: []string{"16"}},
expectedVersion: "16.10",
},
{
label: "With Ubuntu CalVer descending",
versions: []string{"16.04", "16.04.1", "16.10", "20.04", "20.10"},
order: AlphabeticalOrderDesc,
expectedVersion: "16.04",
},
{
label: "Ubuntu code names",
label: "With Ubuntu code names",
versions: []string{"xenial", "yakkety", "zesty", "artful", "bionic"},
expectedVersion: "zesty",
},
{
label: "Ubuntu code names descending",
label: "With Ubuntu code names descending",
versions: []string{"xenial", "yakkety", "zesty", "artful", "bionic"},
order: AlphabeticalOrderDesc,
expectedVersion: "artful",
},
{
label: "Timestamps",
label: "With Timestamps",
versions: []string{"1606234201", "1606364286", "1606334092", "1606334284", "1606334201"},
expectedVersion: "1606364286",
},
{
label: "Timestamps desc",
label: "With Timestamps desc",
versions: []string{"1606234201", "1606364286", "1606334092", "1606334284", "1606334201"},
order: AlphabeticalOrderDesc,
expectedVersion: "1606234201",
},
{
label: "Timestamps with prefix",
label: "With Timestamps prefix",
versions: []string{"rel-1606234201", "rel-1606364286", "rel-1606334092", "rel-1606334284", "rel-1606334201"},
expectedVersion: "rel-1606364286",
},
{
label: "With prefix include",
versions: []string{"rel-1", "rel-2", "rel-3", "rel-4", "dev-5", "dev-6"},
match: &imagev1.TagPrefixMatcher{Include: []string{"rel-"}},
expectedVersion: "rel-4",
},
{
label: "With prefix exclude",
versions: []string{"rel-1", "rel-2", "rel-3", "rel-4", "dev-5", "dev-6"},
match: &imagev1.TagPrefixMatcher{Exclude: []string{"rel-"}},
expectedVersion: "dev-6",
},
{
label: "With prefix include strip",
versions: []string{"rel-11", "rel-12", "rel-13", "rel-15", "dev-5", "dev-6", "ver-12", "ver-10", "gen-50"},
match: &imagev1.TagPrefixMatcher{Include: []string{"rel-", "ver-"}, Trim: true},
expectedVersion: "rel-15",
},
{
label: "Empty version list",
versions: []string{},
Expand All @@ -119,7 +145,7 @@ func TestAlphabetical_Latest(t *testing.T) {
t.Fatalf("returned unexpected error: %s", err)
}

latest, err := policy.Latest(tt.versions)
latest, err := policy.Latest(tt.versions, tt.match)
if tt.expectErr && err == nil {
t.Fatalf("expecting error, got nil")
}
Expand Down
46 changes: 46 additions & 0 deletions internal/policy/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
Copyright 2020 The Flux authors
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 policy

import "strings"

// PrefixMatchFilter applies filtering based on the provided lists of included and
// excluded prefixes
func PrefixMatchFilter(list []string, include []string, exclude []string) []string {
var filtered []string
for _, item := range list {
// Keep by default if no include prefixes specified
var keep bool = len(include) == 0
for _, in := range include {
if strings.HasPrefix(item, in) {
keep = true
}
}

for _, out := range exclude {
if strings.HasPrefix(item, out) {
keep = false
}
}

if keep {
filtered = append(filtered, item)
}
}

return filtered
}
64 changes: 64 additions & 0 deletions internal/policy/filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2020 The Flux authors
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 policy

import (
"reflect"
"testing"
)

func TestPrefixMatchFilter(t *testing.T) {
cases := []struct {
label string
tags []string
include []string
exclude []string
expected []string
}{
{
label: "none",
tags: []string{"a"},
expected: []string{"a"},
},
{
label: "include",
tags: []string{"ver1", "ver2", "ver3", "rel1"},
include: []string{"ver"},
expected: []string{"ver1", "ver2", "ver3"},
},
{
label: "include",
tags: []string{"ver1", "ver2", "ver3", "rel1"},
exclude: []string{"rel"},
expected: []string{"ver1", "ver2", "ver3"},
},
{
label: "include and exclude",
tags: []string{"ver1", "ver2", "rel1", "rel2", "patch1", "patch2"},
exclude: []string{"rel", "patch"},
expected: []string{"ver1", "ver2"},
},
}
for _, tt := range cases {
t.Run(tt.label, func(t *testing.T) {
r := PrefixMatchFilter(tt.tags, tt.include, tt.exclude)
if !reflect.DeepEqual(r, tt.expected) {
t.Errorf("incorrect value returned, got '%s', expected '%s'", r, tt.expected)
}
})
}
}
4 changes: 3 additions & 1 deletion internal/policy/policer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ limitations under the License.

package policy

import imagev1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"

// Policer is an interface representing a policy implementation type
type Policer interface {
Latest([]string) (string, error)
Latest([]string, *imagev1.TagPrefixMatcher) (string, error)
}
Loading

0 comments on commit 3dd2d2d

Please sign in to comment.