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.

+semverFilter
+ +string + + + +(Optional) +

SemverFilter is a regex pattern to filter the tags within the SemVer range.

+ + + + 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