Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exclusionList to enable excluding tags based on regex. #256

Merged
merged 2 commits into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ bin
*~

build/
data
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ manager: generate fmt vet

# Run against the configured Kubernetes cluster in ~/.kube/config
run: generate fmt vet manifests
go run ./main.go
go run ./main.go --storage-path=./data
aryan9600 marked this conversation as resolved.
Show resolved Hide resolved

# Install CRDs into a cluster
install: manifests
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/imagerepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ type ImageRepositorySpec struct {
// to the ImageRepository object based on the caller's namespace labels.
// +optional
AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"`

// ExclusionList is a list of regex strings used to exclude certain tags
// from being stored in the database.
// +optional
ExclusionList []string `json:"exclusionList,omitempty"`
}

type ScanResult struct {
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,12 @@ spec:
required:
- name
type: object
exclusionList:
description: ExclusionList is a list of regex strings used to exclude
certain tags from being stored in the database.
items:
type: string
type: array
image:
description: Image is the name of the image repository
type: string
Expand Down
36 changes: 29 additions & 7 deletions controllers/imagerepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ import (
// for consistency (and perhaps this will have its own flux create
// secret subcommand at some point).
const (
ClientCert = "certFile"
ClientKey = "keyFile"
CACert = "caFile"
ClientCert = "certFile"
ClientKey = "keyFile"
CACert = "caFile"
CosignObjectRegex = "^.*\\.sig$"
)

// ImageRepositoryReconciler reconciles a ImageRepository object
Expand Down Expand Up @@ -469,7 +470,9 @@ func (r *ImageRepositoryReconciler) scan(ctx context.Context, imageRepo *imagev1
}
}

tags, err := remote.ListWithContext(ctx, ref.Context(), options...)
options = append(options, remote.WithContext(ctx))

tags, err := remote.List(ref.Context(), options...)
if err != nil {
imagev1.SetImageRepositoryReadiness(
imageRepo,
Expand All @@ -480,14 +483,33 @@ func (r *ImageRepositoryReconciler) scan(ctx context.Context, imageRepo *imagev1
return err
}

// If no exclusion list has been defined, we make sure to always skip tags ending with
// ".sig", since that tag does not point to a valid image.
if len(imageRepo.Spec.ExclusionList) == 0 {
imageRepo.Spec.ExclusionList = append(imageRepo.Spec.ExclusionList, CosignObjectRegex)
}

filteredTags := []string{}
for _, regex := range imageRepo.Spec.ExclusionList {
r, err := regexp.Compile(regex)
if err != nil {
return fmt.Errorf("failed to compile regex %s: %w", regex, err)
}
for _, tag := range tags {
if !r.MatchString(tag) {
filteredTags = append(filteredTags, tag)
}
}
}

canonicalName := ref.Context().String()
if err := r.Database.SetTags(canonicalName, tags); err != nil {
if err := r.Database.SetTags(canonicalName, filteredTags); err != nil {
return fmt.Errorf("failed to set tags for %q: %w", canonicalName, err)
}

scanTime := metav1.Now()
imageRepo.Status.LastScanResult = &imagev1.ScanResult{
TagCount: len(tags),
TagCount: len(filteredTags),
ScanTime: scanTime,
}

Expand All @@ -502,7 +524,7 @@ func (r *ImageRepositoryReconciler) scan(ctx context.Context, imageRepo *imagev1
imageRepo,
metav1.ConditionTrue,
imagev1.ReconciliationSucceededReason,
fmt.Sprintf("successful scan, found %v tags", len(tags)),
fmt.Sprintf("successful scan, found %v tags", len(filteredTags)),
)

return nil
Expand Down
83 changes: 55 additions & 28 deletions controllers/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,37 +81,64 @@ func TestImageRepositoryReconciler_fetchImageTags(t *testing.T) {

registryServer := test.NewRegistryServer()
defer registryServer.Close()

versions := []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
imgRepo, err := test.LoadImages(registryServer, "test-fetch-"+randStringRunes(5), versions)
g.Expect(err).ToNot(HaveOccurred())

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Interval: metav1.Duration{Duration: reconciliationInterval},
Image: imgRepo,
tests := []struct {
name string
versions []string
wantVersions []string
exclusionList []string
}{
{
name: "fetch image tags",
versions: []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.1.0", "1.1.0-alpha"},
wantVersions: []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.1.0", "1.1.0-alpha"},
},
{
name: "fetch image tags - .sig is excluded",
versions: []string{"0.1.0", "0.1.1", "0.1.1.sig", "1.0.0-alpha", "1.0.0", "1.0.0.sig"},
wantVersions: []string{"0.1.0", "0.1.1", "1.0.0-alpha", "1.0.0"},
},
{
name: "fetch image tags - tags in exclusionList are excluded",
versions: []string{"0.1.0", "0.1.1-alpha", "0.1.1", "0.1.1.sig", "1.0.0-alpha", "1.0.0"},
wantVersions: []string{"0.1.0", "0.1.1", "0.1.1.sig", "1.0.0"},
exclusionList: []string{"^.*\\-alpha$"},
},
}
objectName := types.NamespacedName{
Name: "test-fetch-img-tags-" + randStringRunes(5),
Namespace: "default",
}

repo.Name = objectName.Name
repo.Namespace = objectName.Namespace

ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
defer cancel()
g.Expect(testEnv.Create(ctx, &repo)).To(Succeed())

g.Eventually(func() bool {
err := testEnv.Get(context.Background(), objectName, &repo)
return err == nil && repo.Status.LastScanResult != nil
}, timeout, interval).Should(BeTrue())
g.Expect(repo.Status.CanonicalImageName).To(Equal(imgRepo))
g.Expect(repo.Status.LastScanResult.TagCount).To(Equal(len(versions)))
// Cleanup.
g.Expect(testEnv.Delete(ctx, &repo)).To(Succeed())
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
imgRepo, err := test.LoadImages(registryServer, "test-fetch-"+randStringRunes(5), tt.versions)
g.Expect(err).ToNot(HaveOccurred())

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Interval: metav1.Duration{Duration: reconciliationInterval},
Image: imgRepo,
ExclusionList: tt.exclusionList,
},
}
objectName := types.NamespacedName{
Name: "test-fetch-img-tags-" + randStringRunes(5),
Namespace: "default",
}

repo.Name = objectName.Name
repo.Namespace = objectName.Namespace

ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
defer cancel()
g.Expect(testEnv.Create(ctx, &repo)).To(Succeed())

g.Eventually(func() bool {
err := testEnv.Get(context.Background(), objectName, &repo)
return err == nil && repo.Status.LastScanResult != nil
}, timeout, interval).Should(BeTrue())
g.Expect(repo.Status.CanonicalImageName).To(Equal(imgRepo))
g.Expect(repo.Status.LastScanResult.TagCount).To(Equal(len(tt.wantVersions)))
// Cleanup.
g.Expect(testEnv.Delete(ctx, &repo)).To(Succeed())
})
}
}

func TestImageRepositoryReconciler_repositorySuspended(t *testing.T) {
Expand Down
26 changes: 26 additions & 0 deletions docs/api/image-reflector.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,19 @@ github.com/fluxcd/pkg/apis/acl.AccessFrom
to the ImageRepository object based on the caller&rsquo;s namespace labels.</p>
</td>
</tr>
<tr>
<td>
<code>exclusionList</code><br>
<em>
[]string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ExclusionList is a list of regex strings used to exclude certain tags
from being stored in the database.</p>
</td>
</tr>
</table>
</td>
</tr>
Expand Down Expand Up @@ -656,6 +669,19 @@ github.com/fluxcd/pkg/apis/acl.AccessFrom
to the ImageRepository object based on the caller&rsquo;s namespace labels.</p>
</td>
</tr>
<tr>
<td>
<code>exclusionList</code><br>
<em>
[]string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ExclusionList is a list of regex strings used to exclude certain tags
from being stored in the database.</p>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
13 changes: 13 additions & 0 deletions docs/spec/v1beta1/imagerepositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ type ImageRepositorySpec struct {
// to the ImageRepository object based on the caller's namespace labels.
// +optional
AccessFrom *AccessFrom `json:"accessFrom,omitempty"`

// ExclusionList is a list of regex strings used to exclude certain tags
// from being stored in the database.
// +optional
ExclusionList []string `json:"exclusionList,omitempty"`
}
```

Expand Down Expand Up @@ -200,6 +205,14 @@ To grant access to all namespaces, an empty `matchLabels` must be provided:
- matchLabels: {}
```

### Exclude Tags

To exclude certain tags, the `spec.exclusionList` field can be used to specify a list of regex expressions.
Any tags that match any of the regex expressions, will be excluded from the final tag list.
If `spec.exclusionList` is empty, by default the regex `"^.*\\.sig$"` is used to exclude all tags ending with
`.sig`, since these are [Cosign](https://github.com/sigstore/cosign) generated objects and not container images
which can be deployed on a Kubernetes cluster.

## Status

```go
Expand Down