diff --git a/api/v1beta2/ocirepository_types.go b/api/v1beta2/ocirepository_types.go
index 540a18ac2..5c4df35ce 100644
--- a/api/v1beta2/ocirepository_types.go
+++ b/api/v1beta2/ocirepository_types.go
@@ -157,6 +157,10 @@ type OCIRepositoryRef struct {
// +optional
SemVer string `json:"semver,omitempty"`
+ // SemverFilter is a regex pattern to filter the tags within the SemVer range.
+ // +optional
+ SemverFilter string `json:"semverFilter,omitempty"`
+
// Tag is the image tag to pull, defaults to latest.
// +optional
Tag string `json:"tag,omitempty"`
diff --git a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
index f083276ba..25c33512e 100644
--- a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
+++ b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
@@ -146,6 +146,10 @@ spec:
SemVer is the range of tags to pull selecting the latest within
the range, takes precedence over Tag.
type: string
+ semverFilter:
+ description: SemverFilter is a regex pattern to filter the tags
+ within the SemVer range.
+ type: string
tag:
description: Tag is the image tag to pull, defaults to latest.
type: string
diff --git a/docs/api/v1beta2/source.md b/docs/api/v1beta2/source.md
index 04c3e328f..b5d50e9fe 100644
--- a/docs/api/v1beta2/source.md
+++ b/docs/api/v1beta2/source.md
@@ -2938,6 +2938,18 @@ the range, takes precedence over Tag.
+
tag
string
diff --git a/docs/spec/v1beta2/ocirepositories.md b/docs/spec/v1beta2/ocirepositories.md
index 39a34e217..f40dab066 100644
--- a/docs/spec/v1beta2/ocirepositories.md
+++ b/docs/spec/v1beta2/ocirepositories.md
@@ -441,6 +441,37 @@ spec:
This field takes precedence over [`.tag`](#tag-example).
+#### SemverFilter example
+
+`.spec.ref.semverFilter` is an optional field to specify a SemVer filter to apply
+when fetching tags from the OCI repository. The filter is a regular expression
+that is applied to the tags fetched from the repository. Only tags that match
+the filter are considered for the semver range resolution.
+
+**Note:** The filter is only taken into account when the `.spec.ref.semver` field
+is set.
+
+```yaml
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 5m0s
+ url: oci://ghcr.io/stefanprodan/manifests/podinfo
+ ref:
+ # SemVer comparisons using constraints without a prerelease comparator will skip prerelease versions.
+ # Adding a `-0` suffix to the semver range will include prerelease versions.
+ semver: ">= 6.1.x-0"
+ semverFilter: ".*-rc.*"
+```
+
+In the above example, the controller fetches tags from the `ghcr.io/stefanprodan/manifests/podinfo`
+repository and filters them using the regular expression `.*-rc.*`. Only tags that
+contain the `-rc` suffix are considered for the semver range resolution.
+
#### Digest example
To pull a specific digest, use `.spec.ref.digest`:
diff --git a/internal/controller/ocirepository_controller.go b/internal/controller/ocirepository_controller.go
index ff44b414c..3de4faaa7 100644
--- a/internal/controller/ocirepository_controller.go
+++ b/internal/controller/ocirepository_controller.go
@@ -26,6 +26,7 @@ import (
"net/http"
"os"
"path/filepath"
+ "regexp"
"sort"
"strings"
"time"
@@ -116,6 +117,8 @@ var ociRepositoryFailConditions = []string{
sourcev1.StorageOperationFailedCondition,
}
+type filterFunc func(tags []string) ([]string, error)
+
type invalidOCIURLError struct {
err error
}
@@ -821,7 +824,7 @@ func (r *OCIRepositoryReconciler) getArtifactRef(obj *ociv1.OCIRepository, optio
}
if obj.Spec.Reference.SemVer != "" {
- return r.getTagBySemver(repo, obj.Spec.Reference.SemVer, options)
+ return r.getTagBySemver(repo, obj.Spec.Reference.SemVer, filterTags(obj.Spec.Reference.SemverFilter), options)
}
if obj.Spec.Reference.Tag != "" {
@@ -834,19 +837,24 @@ func (r *OCIRepositoryReconciler) getArtifactRef(obj *ociv1.OCIRepository, optio
// getTagBySemver call the remote container registry, fetches all the tags from the repository,
// and returns the latest tag according to the semver expression.
-func (r *OCIRepositoryReconciler) getTagBySemver(repo name.Repository, exp string, options []remote.Option) (name.Reference, error) {
+func (r *OCIRepositoryReconciler) getTagBySemver(repo name.Repository, exp string, filter filterFunc, options []remote.Option) (name.Reference, error) {
tags, err := remote.List(repo, options...)
if err != nil {
return nil, err
}
+ validTags, err := filter(tags)
+ if err != nil {
+ return nil, err
+ }
+
constraint, err := semver.NewConstraint(exp)
if err != nil {
return nil, fmt.Errorf("semver '%s' parse error: %w", exp, err)
}
var matchingVersions []*semver.Version
- for _, t := range tags {
+ for _, t := range validTags {
v, err := version.ParseVersion(t)
if err != nil {
continue
@@ -1298,3 +1306,24 @@ func layerSelectorEqual(a, b *ociv1.OCILayerSelector) bool {
}
return *a == *b
}
+
+func filterTags(filter string) filterFunc {
+ return func(tags []string) ([]string, error) {
+ if filter == "" {
+ return tags, nil
+ }
+
+ match, err := regexp.Compile(filter)
+ if err != nil {
+ return nil, err
+ }
+
+ validTags := []string{}
+ for _, tag := range tags {
+ if match.MatchString(tag) {
+ validTags = append(validTags, tag)
+ }
+ }
+ return validTags, nil
+ }
+}
diff --git a/internal/controller/ocirepository_controller_test.go b/internal/controller/ocirepository_controller_test.go
index faf31fd76..4d0b51c16 100644
--- a/internal/controller/ocirepository_controller_test.go
+++ b/internal/controller/ocirepository_controller_test.go
@@ -2757,7 +2757,14 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
server.Close()
})
- imgs, err := pushMultiplePodinfoImages(server.registryHost, true, "6.1.4", "6.1.5", "6.1.6")
+ imgs, err := pushMultiplePodinfoImages(server.registryHost, true,
+ "6.1.4",
+ "6.1.5-beta.1",
+ "6.1.5-rc.1",
+ "6.1.5",
+ "6.1.6-rc.1",
+ "6.1.6",
+ )
g.Expect(err).ToNot(HaveOccurred())
tests := []struct {
@@ -2801,6 +2808,24 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
url: "ghcr.io/stefanprodan/charts",
wantErr: true,
},
+ {
+ name: "valid url with semver filter",
+ url: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
+ reference: &ociv1.OCIRepositoryRef{
+ SemVer: ">= 6.1.x-0",
+ SemverFilter: ".*-rc.*",
+ },
+ want: server.registryHost + "/podinfo:6.1.6-rc.1",
+ },
+ {
+ name: "valid url with semver filter and unexisting version",
+ url: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
+ reference: &ociv1.OCIRepositoryRef{
+ SemVer: ">= 6.1.x-0",
+ SemverFilter: ".*-alpha.*",
+ },
+ wantErr: true,
+ },
}
clientBuilder := fakeclient.NewClientBuilder().
diff --git a/internal/controller/testdata/podinfo/podinfo-6.1.5-beta.1.tar b/internal/controller/testdata/podinfo/podinfo-6.1.5-beta.1.tar
new file mode 100644
index 000000000..335d6a5ad
Binary files /dev/null and b/internal/controller/testdata/podinfo/podinfo-6.1.5-beta.1.tar differ
diff --git a/internal/controller/testdata/podinfo/podinfo-6.1.5-rc.1.tar b/internal/controller/testdata/podinfo/podinfo-6.1.5-rc.1.tar
new file mode 100644
index 000000000..335d6a5ad
Binary files /dev/null and b/internal/controller/testdata/podinfo/podinfo-6.1.5-rc.1.tar differ
diff --git a/internal/controller/testdata/podinfo/podinfo-6.1.6-rc.1.tar b/internal/controller/testdata/podinfo/podinfo-6.1.6-rc.1.tar
new file mode 100644
index 000000000..09616c2df
Binary files /dev/null and b/internal/controller/testdata/podinfo/podinfo-6.1.6-rc.1.tar differ
|