Skip to content

Commit 74c5f99

Browse files
authored
Merge pull request #1407 from fluxcd/add-semverfilter-to-ocirepo
Introduce a semVer filter in OCIRepository API
2 parents 295fb73 + 6d7189d commit 74c5f99

File tree

9 files changed

+109
-4
lines changed

9 files changed

+109
-4
lines changed

api/v1beta2/ocirepository_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ type OCIRepositoryRef struct {
157157
// +optional
158158
SemVer string `json:"semver,omitempty"`
159159

160+
// SemverFilter is a regex pattern to filter the tags within the SemVer range.
161+
// +optional
162+
SemverFilter string `json:"semverFilter,omitempty"`
163+
160164
// Tag is the image tag to pull, defaults to latest.
161165
// +optional
162166
Tag string `json:"tag,omitempty"`

config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ spec:
146146
SemVer is the range of tags to pull selecting the latest within
147147
the range, takes precedence over Tag.
148148
type: string
149+
semverFilter:
150+
description: SemverFilter is a regex pattern to filter the tags
151+
within the SemVer range.
152+
type: string
149153
tag:
150154
description: Tag is the image tag to pull, defaults to latest.
151155
type: string

docs/api/v1beta2/source.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2938,6 +2938,18 @@ the range, takes precedence over Tag.</p>
29382938
</tr>
29392939
<tr>
29402940
<td>
2941+
<code>semverFilter</code><br>
2942+
<em>
2943+
string
2944+
</em>
2945+
</td>
2946+
<td>
2947+
<em>(Optional)</em>
2948+
<p>SemverFilter is a regex pattern to filter the tags within the SemVer range.</p>
2949+
</td>
2950+
</tr>
2951+
<tr>
2952+
<td>
29412953
<code>tag</code><br>
29422954
<em>
29432955
string

docs/spec/v1beta2/ocirepositories.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,37 @@ spec:
441441

442442
This field takes precedence over [`.tag`](#tag-example).
443443

444+
#### SemverFilter example
445+
446+
`.spec.ref.semverFilter` is an optional field to specify a SemVer filter to apply
447+
when fetching tags from the OCI repository. The filter is a regular expression
448+
that is applied to the tags fetched from the repository. Only tags that match
449+
the filter are considered for the semver range resolution.
450+
451+
**Note:** The filter is only taken into account when the `.spec.ref.semver` field
452+
is set.
453+
454+
```yaml
455+
---
456+
apiVersion: source.toolkit.fluxcd.io/v1beta2
457+
kind: OCIRepository
458+
metadata:
459+
name: podinfo
460+
namespace: default
461+
spec:
462+
interval: 5m0s
463+
url: oci://ghcr.io/stefanprodan/manifests/podinfo
464+
ref:
465+
# SemVer comparisons using constraints without a prerelease comparator will skip prerelease versions.
466+
# Adding a `-0` suffix to the semver range will include prerelease versions.
467+
semver: ">= 6.1.x-0"
468+
semverFilter: ".*-rc.*"
469+
```
470+
471+
In the above example, the controller fetches tags from the `ghcr.io/stefanprodan/manifests/podinfo`
472+
repository and filters them using the regular expression `.*-rc.*`. Only tags that
473+
contain the `-rc` suffix are considered for the semver range resolution.
474+
444475
#### Digest example
445476

446477
To pull a specific digest, use `.spec.ref.digest`:

internal/controller/ocirepository_controller.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"net/http"
2727
"os"
2828
"path/filepath"
29+
"regexp"
2930
"sort"
3031
"strings"
3132
"time"
@@ -116,6 +117,8 @@ var ociRepositoryFailConditions = []string{
116117
sourcev1.StorageOperationFailedCondition,
117118
}
118119

120+
type filterFunc func(tags []string) ([]string, error)
121+
119122
type invalidOCIURLError struct {
120123
err error
121124
}
@@ -821,7 +824,7 @@ func (r *OCIRepositoryReconciler) getArtifactRef(obj *ociv1.OCIRepository, optio
821824
}
822825

823826
if obj.Spec.Reference.SemVer != "" {
824-
return r.getTagBySemver(repo, obj.Spec.Reference.SemVer, options)
827+
return r.getTagBySemver(repo, obj.Spec.Reference.SemVer, filterTags(obj.Spec.Reference.SemverFilter), options)
825828
}
826829

827830
if obj.Spec.Reference.Tag != "" {
@@ -834,19 +837,24 @@ func (r *OCIRepositoryReconciler) getArtifactRef(obj *ociv1.OCIRepository, optio
834837

835838
// getTagBySemver call the remote container registry, fetches all the tags from the repository,
836839
// and returns the latest tag according to the semver expression.
837-
func (r *OCIRepositoryReconciler) getTagBySemver(repo name.Repository, exp string, options []remote.Option) (name.Reference, error) {
840+
func (r *OCIRepositoryReconciler) getTagBySemver(repo name.Repository, exp string, filter filterFunc, options []remote.Option) (name.Reference, error) {
838841
tags, err := remote.List(repo, options...)
839842
if err != nil {
840843
return nil, err
841844
}
842845

846+
validTags, err := filter(tags)
847+
if err != nil {
848+
return nil, err
849+
}
850+
843851
constraint, err := semver.NewConstraint(exp)
844852
if err != nil {
845853
return nil, fmt.Errorf("semver '%s' parse error: %w", exp, err)
846854
}
847855

848856
var matchingVersions []*semver.Version
849-
for _, t := range tags {
857+
for _, t := range validTags {
850858
v, err := version.ParseVersion(t)
851859
if err != nil {
852860
continue
@@ -1298,3 +1306,24 @@ func layerSelectorEqual(a, b *ociv1.OCILayerSelector) bool {
12981306
}
12991307
return *a == *b
13001308
}
1309+
1310+
func filterTags(filter string) filterFunc {
1311+
return func(tags []string) ([]string, error) {
1312+
if filter == "" {
1313+
return tags, nil
1314+
}
1315+
1316+
match, err := regexp.Compile(filter)
1317+
if err != nil {
1318+
return nil, err
1319+
}
1320+
1321+
validTags := []string{}
1322+
for _, tag := range tags {
1323+
if match.MatchString(tag) {
1324+
validTags = append(validTags, tag)
1325+
}
1326+
}
1327+
return validTags, nil
1328+
}
1329+
}

internal/controller/ocirepository_controller_test.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2757,7 +2757,14 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
27572757
server.Close()
27582758
})
27592759

2760-
imgs, err := pushMultiplePodinfoImages(server.registryHost, true, "6.1.4", "6.1.5", "6.1.6")
2760+
imgs, err := pushMultiplePodinfoImages(server.registryHost, true,
2761+
"6.1.4",
2762+
"6.1.5-beta.1",
2763+
"6.1.5-rc.1",
2764+
"6.1.5",
2765+
"6.1.6-rc.1",
2766+
"6.1.6",
2767+
)
27612768
g.Expect(err).ToNot(HaveOccurred())
27622769

27632770
tests := []struct {
@@ -2801,6 +2808,24 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
28012808
url: "ghcr.io/stefanprodan/charts",
28022809
wantErr: true,
28032810
},
2811+
{
2812+
name: "valid url with semver filter",
2813+
url: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
2814+
reference: &ociv1.OCIRepositoryRef{
2815+
SemVer: ">= 6.1.x-0",
2816+
SemverFilter: ".*-rc.*",
2817+
},
2818+
want: server.registryHost + "/podinfo:6.1.6-rc.1",
2819+
},
2820+
{
2821+
name: "valid url with semver filter and unexisting version",
2822+
url: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
2823+
reference: &ociv1.OCIRepositoryRef{
2824+
SemVer: ">= 6.1.x-0",
2825+
SemverFilter: ".*-alpha.*",
2826+
},
2827+
wantErr: true,
2828+
},
28042829
}
28052830

28062831
clientBuilder := fakeclient.NewClientBuilder().
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)