Merge pull request #1407 from fluxcd/add-semverfilter-to-ocirepo

Introduce a semVer filter in OCIRepository API
This commit is contained in:
souleb 2024-03-29 10:25:58 +01:00 committed by GitHub
commit 74c5f99948
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 109 additions and 4 deletions

View File

@ -157,6 +157,10 @@ type OCIRepositoryRef struct {
// +optional // +optional
SemVer string `json:"semver,omitempty"` 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. // Tag is the image tag to pull, defaults to latest.
// +optional // +optional
Tag string `json:"tag,omitempty"` Tag string `json:"tag,omitempty"`

View File

@ -146,6 +146,10 @@ spec:
SemVer is the range of tags to pull selecting the latest within SemVer is the range of tags to pull selecting the latest within
the range, takes precedence over Tag. the range, takes precedence over Tag.
type: string type: string
semverFilter:
description: SemverFilter is a regex pattern to filter the tags
within the SemVer range.
type: string
tag: tag:
description: Tag is the image tag to pull, defaults to latest. description: Tag is the image tag to pull, defaults to latest.
type: string type: string

View File

@ -2938,6 +2938,18 @@ the range, takes precedence over Tag.</p>
</tr> </tr>
<tr> <tr>
<td> <td>
<code>semverFilter</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>SemverFilter is a regex pattern to filter the tags within the SemVer range.</p>
</td>
</tr>
<tr>
<td>
<code>tag</code><br> <code>tag</code><br>
<em> <em>
string string

View File

@ -441,6 +441,37 @@ spec:
This field takes precedence over [`.tag`](#tag-example). 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 #### Digest example
To pull a specific digest, use `.spec.ref.digest`: To pull a specific digest, use `.spec.ref.digest`:

View File

@ -26,6 +26,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -116,6 +117,8 @@ var ociRepositoryFailConditions = []string{
sourcev1.StorageOperationFailedCondition, sourcev1.StorageOperationFailedCondition,
} }
type filterFunc func(tags []string) ([]string, error)
type invalidOCIURLError struct { type invalidOCIURLError struct {
err error err error
} }
@ -821,7 +824,7 @@ func (r *OCIRepositoryReconciler) getArtifactRef(obj *ociv1.OCIRepository, optio
} }
if obj.Spec.Reference.SemVer != "" { 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 != "" { 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, // getTagBySemver call the remote container registry, fetches all the tags from the repository,
// and returns the latest tag according to the semver expression. // 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...) tags, err := remote.List(repo, options...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
validTags, err := filter(tags)
if err != nil {
return nil, err
}
constraint, err := semver.NewConstraint(exp) constraint, err := semver.NewConstraint(exp)
if err != nil { if err != nil {
return nil, fmt.Errorf("semver '%s' parse error: %w", exp, err) return nil, fmt.Errorf("semver '%s' parse error: %w", exp, err)
} }
var matchingVersions []*semver.Version var matchingVersions []*semver.Version
for _, t := range tags { for _, t := range validTags {
v, err := version.ParseVersion(t) v, err := version.ParseVersion(t)
if err != nil { if err != nil {
continue continue
@ -1298,3 +1306,24 @@ func layerSelectorEqual(a, b *ociv1.OCILayerSelector) bool {
} }
return *a == *b 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
}
}

View File

@ -2757,7 +2757,14 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
server.Close() 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()) g.Expect(err).ToNot(HaveOccurred())
tests := []struct { tests := []struct {
@ -2801,6 +2808,24 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
url: "ghcr.io/stefanprodan/charts", url: "ghcr.io/stefanprodan/charts",
wantErr: true, 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(). clientBuilder := fakeclient.NewClientBuilder().

Binary file not shown.

Binary file not shown.

Binary file not shown.