Skip to content

Commit

Permalink
Allow regex matching of artifacts with artifact ingester (#1716)
Browse files Browse the repository at this point in the history
* Allow regex matching of artifacts with artifact ingester

This changes the ingester to also allow a `tag_regex` field as part of its
configuration. The idea is that folks would be able to write a profile with
a configuration that matches all release-related tags for their containers.

Such a configuration would look as follows:

```yaml
artifact:
  - type: artifact_signature
    params:
      tag_regex: "v*"
    def:
      is_signed: true
      is_verified: true
      is_bundle_verified: true
```

* Handle empty tags in configuration

* Add more incoming tags validations
  • Loading branch information
JAORMX authored Nov 23, 2023
1 parent 93e6027 commit 26ca90b
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 20 deletions.
39 changes: 22 additions & 17 deletions internal/engine/ingester/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"fmt"

"google.golang.org/protobuf/proto"
"k8s.io/apimachinery/pkg/util/sets"

evalerrors "github.com/stacklok/minder/internal/engine/errors"
engif "github.com/stacklok/minder/internal/engine/interfaces"
Expand Down Expand Up @@ -102,27 +101,19 @@ func getApplicableArtifactVersions(
return nil, evalerrors.NewErrEvaluationSkipSilently("artifact name mismatch")
}

// Build a tag matcher based on the configuration
tagMatcher, err := buildTagMatcher(cfg.Tags, cfg.TagRegex)
if err != nil {
return nil, err
}

// get all versions of the artifact that are applicable to this rule
for _, artifactVersion := range artifact.Versions {
// skip artifact versions without tags
if len(artifactVersion.Tags) == 0 || artifactVersion.Tags[0] == "" {
continue
}

// rule without tags is treated as a wildcard and matches all tagged artifacts
// this might be configurable in the future
if len(cfg.Tags) == 0 {
applicableArtifactVersions = append(applicableArtifactVersions, struct {
Verification any
GithubWorkflow any
}{artifactVersion.SignatureVerification, artifactVersion.GithubWorkflow})
if !isProcessable(artifactVersion.Tags) {
continue
}

// make sure all rule tags are present in the artifact version tags
haveTags := sets.New(artifactVersion.Tags...)
tagsOk := haveTags.HasAll(cfg.Tags...)
if tagsOk {
if tagMatcher.MatchTag(artifactVersion.Tags...) {
applicableArtifactVersions = append(applicableArtifactVersions, struct {
Verification any
GithubWorkflow any
Expand All @@ -148,3 +139,17 @@ func getApplicableArtifactVersions(
// return the list of applicable artifact versions
return result, nil
}

func isProcessable(tags []string) bool {
if len(tags) == 0 {
return false
}

for _, tag := range tags {
if tag == "" {
return false
}
}

return true
}
174 changes: 174 additions & 0 deletions internal/engine/ingester/artifact/artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,177 @@ func TestArtifactIngestMatchAnyName(t *testing.T) {
require.NoError(t, err, "expected no error")
require.NotNil(t, got, "expected non-nil result")
}

func TestArtifactWithMatchingRegexp(t *testing.T) {
t.Parallel()

ing, err := artifact.NewArtifactDataIngest(nil)
require.NoError(t, err, "expected no error")

got, err := ing.Ingest(context.Background(), &pb.Artifact{
Type: "container",
Name: "matching-name",
Versions: []*pb.ArtifactVersion{
{
Tags: []string{"v1.0.0"},
},
},
}, map[string]interface{}{
"name": "matching-name",
"tag_regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
})

require.NoError(t, err, "expected no error")
require.NotNil(t, got, "expected non-nil result")
}

func TestArtifactWithMultipleTagsAndMatchingRegexp(t *testing.T) {
t.Parallel()

ing, err := artifact.NewArtifactDataIngest(nil)
require.NoError(t, err, "expected no error")

got, err := ing.Ingest(context.Background(), &pb.Artifact{
Type: "container",
Name: "matching-name",
Versions: []*pb.ArtifactVersion{
{
Tags: []string{
"v2.0.0",
"latest",
},
},
},
}, map[string]interface{}{
"name": "matching-name",
"tag_regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
})

require.NoError(t, err, "expected no error")
require.NotNil(t, got, "expected non-nil result")
}

func TestArtifactWithTagThatDoesntMatchRegexp(t *testing.T) {
t.Parallel()

ing, err := artifact.NewArtifactDataIngest(nil)
require.NoError(t, err, "expected no error")

got, err := ing.Ingest(context.Background(), &pb.Artifact{
Type: "container",
Name: "matching-name",
Versions: []*pb.ArtifactVersion{
{
Tags: []string{
"latest",
},
},
},
}, map[string]interface{}{
"name": "matching-name",
"tag_regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
})

require.Error(t, err, "expected error")
require.Nil(t, got, "expected nil result")
}

func TestArtifactWithMultipleTagsThatDontMatchRegexp(t *testing.T) {
t.Parallel()

ing, err := artifact.NewArtifactDataIngest(nil)
require.NoError(t, err, "expected no error")

got, err := ing.Ingest(context.Background(), &pb.Artifact{
Type: "container",
Name: "matching-name",
Versions: []*pb.ArtifactVersion{
{
Tags: []string{
"latest",
"pr-123",
"testing",
},
},
},
}, map[string]interface{}{
"name": "matching-name",
"tag_regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
})

require.Error(t, err, "expected error")
require.Nil(t, got, "expected nil result")
}

func TestArtifactWithEmptyTagShouldError(t *testing.T) {
t.Parallel()

ing, err := artifact.NewArtifactDataIngest(nil)
require.NoError(t, err, "expected no error")

got, err := ing.Ingest(context.Background(), &pb.Artifact{
Type: "container",
Name: "matching-name",
Versions: []*pb.ArtifactVersion{
{
Tags: []string{
"latest",
"pr-123",
"testing",
},
},
},
}, map[string]interface{}{
"name": "matching-name",
"tags": []string{""},
})

require.Error(t, err, "expected error")
require.Nil(t, got, "expected nil result")
}

func TestArtifactVersionWithNoTagsShouldError(t *testing.T) {
t.Parallel()

ing, err := artifact.NewArtifactDataIngest(nil)
require.NoError(t, err, "expected no error")

got, err := ing.Ingest(context.Background(), &pb.Artifact{
Type: "container",
Name: "matching-name",
Versions: []*pb.ArtifactVersion{
{
Tags: []string{},
},
},
}, map[string]interface{}{
"name": "matching-name",
"tags": []string{},
})

require.Error(t, err, "expected error")
require.Nil(t, got, "expected nil result")
}

func TestArtifactVersionWithEmptyStringTagShouldError(t *testing.T) {
t.Parallel()

ing, err := artifact.NewArtifactDataIngest(nil)
require.NoError(t, err, "expected no error")

got, err := ing.Ingest(context.Background(), &pb.Artifact{
Type: "container",
Name: "matching-name",
Versions: []*pb.ArtifactVersion{
{
Tags: []string{""},
},
},
}, map[string]interface{}{
"name": "matching-name",
"tags": []string{},
})

require.Error(t, err, "expected error")
require.Nil(t, got, "expected nil result")
}
7 changes: 4 additions & 3 deletions internal/engine/ingester/artifact/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ func newArtifactIngestType(s string) artifactType {
}

type ingesterConfig struct {
Name string `yaml:"name" json:"name" mapstructure:"name"`
Tags []string `yaml:"tags" json:"tags" mapstructure:"tags"`
Type artifactType `yaml:"type" json:"type" mapstructure:"type"`
Name string `yaml:"name" json:"name" mapstructure:"name"`
Tags []string `yaml:"tags" json:"tags" mapstructure:"tags"`
TagRegex string `yaml:"tag_regex" json:"tag_regex" mapstructure:"tag_regex"`
Type artifactType `yaml:"type" json:"type" mapstructure:"type"`
}

func configFromParams(params map[string]any) (*ingesterConfig, error) {
Expand Down
83 changes: 83 additions & 0 deletions internal/engine/ingester/artifact/tag_match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2023 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package rule provides the CLI subcommand for managing rules

package artifact

import (
"fmt"
"regexp"

"k8s.io/apimachinery/pkg/util/sets"
)

func buildTagMatcher(tags []string, tagRegex string) (tagMatcher, error) {
if len(tags) > 0 && tagRegex != "" {
return nil, fmt.Errorf("cannot specify both tags and tag_regex")
}

// tags specified, build a list matcher
if len(tags) > 0 {
stags := sets.New(tags...)
if stags.HasAny("") {
return nil, fmt.Errorf("cannot specify empty tag")
}
return &tagListMatcher{tags: tags}, nil
}

// no tags specified, but a regex was, compile it
if tagRegex != "" {
re, err := regexp.Compile(tagRegex)
if err != nil {
return nil, fmt.Errorf("error compiling tag regex: %w", err)
}
return &tagRegexMatcher{re: re}, nil
}

// no tags specified, match all
return &tagAllMatcher{}, nil
}

type tagMatcher interface {
MatchTag(tags ...string) bool
}

type tagRegexMatcher struct {
re *regexp.Regexp
}

func (m *tagRegexMatcher) MatchTag(tags ...string) bool {
for _, tag := range tags {
if m.re.MatchString(tag) {
return true
}
}

return false
}

type tagListMatcher struct {
tags []string
}

func (m *tagListMatcher) MatchTag(tags ...string) bool {
haveTags := sets.New(tags...)
return haveTags.HasAll(m.tags...)
}

type tagAllMatcher struct{}

func (*tagAllMatcher) MatchTag(_ ...string) bool {
return true
}

0 comments on commit 26ca90b

Please sign in to comment.