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

Make artifacts ingester work with both GitHub and OCI providers #3309

Merged
merged 8 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
6 changes: 5 additions & 1 deletion cmd/dev/app/image/list_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func CmdListTags() *cobra.Command {
}

listCmd.Flags().StringP("base-url", "b", "", "base URL for the OCI registry")
listCmd.Flags().StringP("owner", "o", "", "owner of the artifact")
listCmd.Flags().StringP("container", "c", "", "container name to list tags for")
//nolint:goconst // let's not use a const for this one
listCmd.Flags().StringP("token", "t", "", "token to authenticate to the provider."+
Expand All @@ -57,6 +58,7 @@ func runCmdListTags(cmd *cobra.Command, _ []string) error {

// get the provider
baseURL := cmd.Flag("base-url")
owner := cmd.Flag("owner")
contname := cmd.Flag("container")

if baseURL.Value.String() == "" {
Expand All @@ -66,8 +68,10 @@ func runCmdListTags(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("container name is required")
}

regWithOwner := fmt.Sprintf("%s/%s", owner.Value.String(), baseURL.Value.String())

cred := credentials.NewOAuth2TokenCredential(viper.GetString("auth.token"))
prov := oci.New(cred, baseURL.Value.String())
prov := oci.New(cred, baseURL.Value.String(), regWithOwner)

// get the containers
containers, err := prov.ListTags(ctx, contname.Value.String())
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/image-spec v1.1.0
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
Expand Down
133 changes: 83 additions & 50 deletions internal/engine/ingester/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ package artifact
import (
"context"
"fmt"
"net/url"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -50,7 +48,7 @@ const (
// Ingest is the engine for a rule type that uses artifact data ingest
// Implements enginer.ingester.Ingester
type Ingest struct {
ghCli provifv1.GitHub
prov provifv1.Provider

// artifactVerifier is the verifier for sigstore. It's only used in the Ingest method
// but we store it in the Ingest structure to allow tests to set a custom artifactVerifier
Expand All @@ -74,9 +72,9 @@ type verifiedAttestation struct {
}

// NewArtifactDataIngest creates a new artifact rule data ingest engine
func NewArtifactDataIngest(ghCli provifv1.GitHub) (*Ingest, error) {
func NewArtifactDataIngest(prov provifv1.Provider) (*Ingest, error) {
return &Ingest{
ghCli: ghCli,
prov: prov,
}, nil
}

Expand Down Expand Up @@ -124,18 +122,17 @@ func (i *Ingest) getApplicableArtifactVersions(
artifact *pb.Artifact,
cfg *ingesterConfig,
) ([]map[string]any, error) {
// Make sure the artifact type matches
if newArtifactIngestType(artifact.Type) != cfg.Type {
return nil, evalerrors.NewErrEvaluationSkipSilently("artifact type mismatch")
if err := validateConfiguration(artifact, cfg); err != nil {
return nil, err
}

// If a name is specified, make sure it matches
if cfg.Name != "" && cfg.Name != artifact.Name {
return nil, evalerrors.NewErrEvaluationSkipSilently("artifact name mismatch")
vers, err := getVersioner(i.prov, artifact)
if err != nil {
return nil, err
}

// Get all artifact versions filtering out those that don't apply to this rule
versions, err := getAndFilterArtifactVersions(ctx, cfg, i.ghCli, artifact)
versions, err := getAndFilterArtifactVersions(ctx, cfg, vers, artifact)
if err != nil {
return nil, err
}
Expand All @@ -160,6 +157,27 @@ func (i *Ingest) getApplicableArtifactVersions(
return result, nil
}

func validateConfiguration(
artifact *pb.Artifact,
cfg *ingesterConfig,
) error {
// Make sure the artifact type matches
if newArtifactIngestType(artifact.Type) != cfg.Type {
return evalerrors.NewErrEvaluationSkipSilently("artifact type mismatch")
}

if cfg.Type != artifactTypeContainer {
return evalerrors.NewErrEvaluationSkipSilently("only container artifacts are supported at the moment")
}

// If a name is specified, make sure it matches
if cfg.Name != "" && cfg.Name != artifact.Name {
return evalerrors.NewErrEvaluationSkipSilently("artifact name mismatch")
}

return nil
}

func (i *Ingest) getVerificationResult(
ctx context.Context,
cfg *ingesterConfig,
Expand Down Expand Up @@ -230,10 +248,31 @@ func getVerifier(i *Ingest, cfg *ingesterConfig) (verifyif.ArtifactVerifier, err
return i.artifactVerifier, nil
}

verifieropts := []container.AuthMethod{}
if i.prov.CanImplement(pb.ProviderType_PROVIDER_TYPE_GITHUB) {
ghcli, err := provifv1.As[provifv1.GitHub](i.prov)
if err != nil {
return nil, fmt.Errorf("unable to get github provider from provider configuration")
}
verifieropts = append(verifieropts, container.WithGitHubClient(ghcli))
} else if i.prov.CanImplement(pb.ProviderType_PROVIDER_TYPE_OCI) {
ocicli, err := provifv1.As[provifv1.OCI](i.prov)
if err != nil {
return nil, fmt.Errorf("unable to get oci provider from provider configuration")
}
cauthn, err := ocicli.GetAuthenticator()
if err != nil {
return nil, fmt.Errorf("unable to get oci authenticator: %w", err)
}
verifieropts = append(verifieropts, container.WithRegistry(ocicli.GetRegistry()),
container.WithAuthenticator(cauthn))
}

artifactVerifier, err := verifier.NewVerifier(
verifier.VerifierSigstore,
cfg.Sigstore,
container.WithGitHubClient(i.ghCli))
verifieropts...,
)
if err != nil {
return nil, fmt.Errorf("error getting sigstore verifier: %w", err)
}
Expand All @@ -244,7 +283,7 @@ func getVerifier(i *Ingest, cfg *ingesterConfig) (verifyif.ArtifactVerifier, err
func getAndFilterArtifactVersions(
ctx context.Context,
cfg *ingesterConfig,
ghCli provifv1.GitHub,
vers versioner,
artifact *pb.Artifact,
) ([]string, error) {
var res []string
Expand All @@ -256,32 +295,30 @@ func getAndFilterArtifactVersions(
}

// Fetch all available versions of the artifact
artifactName := url.QueryEscape(artifact.GetName())
upstreamVersions, err := ghCli.GetPackageVersions(
ctx, artifact.Owner, artifact.GetTypeLower(), artifactName,
)
upstreamVersions, err := vers.GetVersions(ctx)
if err != nil {
return nil, fmt.Errorf("error retrieving artifact versions: %w", err)
}

// Loop through all and filter out the versions that don't apply to this rule
for _, version := range upstreamVersions {
tags := version.Metadata.Container.Tags
sort.Strings(tags)
name := artifact.GetName()

// Loop through all and filter out the versions that don't apply to this rule
for vname, version := range upstreamVersions {
// Decide if the artifact version should be skipped or not
err = isSkippable(verifyif.ArtifactTypeContainer, version.CreatedAt.Time, map[string]interface{}{"tags": tags}, filter)
tags := version.GetTags()
tagsopt := map[string]interface{}{"tags": tags}
err = isSkippable(version.GetCreatedAt().AsTime(), tagsopt, filter)
if err != nil {
zerolog.Ctx(ctx).Debug().Str("name", *version.Name).Strs("tags", tags).Str(
zerolog.Ctx(ctx).Debug().Str("name", name).Strs("tags", tags).Str(
"reason",
err.Error(),
).Msg("skipping artifact version")
continue
}

// If the artifact version is applicable to this rule, add it to the list
zerolog.Ctx(ctx).Debug().Str("name", *version.Name).Strs("tags", tags).Msg("artifact version matched")
res = append(res, *version.Name)
zerolog.Ctx(ctx).Debug().Str("name", name).Strs("tags", tags).Msg("artifact version matched")
res = append(res, vname)
}

// If no applicable artifact versions were found for this rule, we can go ahead and fail the rule evaluation here
Expand All @@ -299,33 +336,29 @@ var (
)

// isSkippable determines if an artifact should be skipped
// Note this is only applicable to container artifacts.
// TODO - this should be refactored as well, for now just a forklift from reconciler
func isSkippable(artifactType verifyif.ArtifactType, createdAt time.Time, opts map[string]interface{}, filter tagMatcher) error {
switch artifactType {
case verifyif.ArtifactTypeContainer:
// if the artifact is older than the retention period, skip it
if createdAt.Before(ArtifactTypeContainerRetentionPeriod) {
return fmt.Errorf("artifact is older than retention period - %s", ArtifactTypeContainerRetentionPeriod)
}
tags, ok := opts["tags"].([]string)
if !ok {
return nil
} else if len(tags) == 0 {
// if the artifact has no tags, skip it
return fmt.Errorf("artifact has no tags")
}
// if the artifact has a .sig tag it's a signature, skip it
if verifier.GetSignatureTag(tags) != "" {
return fmt.Errorf("artifact is a signature")
}
// if the artifact tags don't match the tag matcher, skip it
if !filter.MatchTag(tags...) {
return fmt.Errorf("artifact tags does not match")
}
return nil
default:
func isSkippable(createdAt time.Time, opts map[string]interface{}, filter tagMatcher) error {
// if the artifact is older than the retention period, skip it
if createdAt.Before(ArtifactTypeContainerRetentionPeriod) {
return fmt.Errorf("artifact is older than retention period - %s", ArtifactTypeContainerRetentionPeriod)
}
tags, ok := opts["tags"].([]string)
if !ok {
return nil
} else if len(tags) == 0 {
// if the artifact has no tags, skip it
return fmt.Errorf("artifact has no tags")
}
// if the artifact has a .sig tag it's a signature, skip it
if verifier.GetSignatureTag(tags) != "" {
return fmt.Errorf("artifact is a signature")
}
// if the artifact tags don't match the tag matcher, skip it
if !filter.MatchTag(tags...) {
return fmt.Errorf("artifact tags does not match")
}
return nil
}

func branchFromRef(ref string) string {
Expand Down
2 changes: 1 addition & 1 deletion internal/engine/ingester/artifact/artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ func TestArtifactIngestMatching(t *testing.T) {
ing, err := NewArtifactDataIngest(prov)
require.NoError(t, err, "expected no error")

ing.ghCli = mockGhClient
ing.prov = mockGhClient
ing.artifactVerifier = mockVerifier

tt.mockSetup(mockGhClient, mockVerifier)
Expand Down
Loading