Skip to content

Commit

Permalink
Make artifacts ingester work with both GitHub and OCI providers
Browse files Browse the repository at this point in the history
This modifies the ingester to change its behavior depending on whether
the provider is an OCI provider or a github one.

Signed-off-by: Juan Antonio Osorio <[email protected]>
  • Loading branch information
JAORMX committed May 12, 2024
1 parent 4c9a0a8 commit 0640f74
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 78 deletions.
120 changes: 71 additions & 49 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,20 @@ 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))
}

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 +272,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 +284,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)
}

name := artifact.GetName()

// 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)

// 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, name)
}

// If no applicable artifact versions were found for this rule, we can go ahead and fail the rule evaluation here
Expand All @@ -299,33 +325,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
141 changes: 141 additions & 0 deletions internal/engine/ingester/artifact/versioner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2024 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 provides the artifact ingestion engine
package artifact

import (
"context"
"fmt"
"net/url"
"sort"
"time"

v1 "github.com/opencontainers/image-spec/specs-go/v1"
"google.golang.org/protobuf/types/known/timestamppb"

minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
provifv1 "github.com/stacklok/minder/pkg/providers/v1"
)

type versioner interface {
// Gets the available versions of a given artifact
GetVersions(ctx context.Context) ([]*minderv1.ArtifactVersion, error)
}

func getVersioner(prov provifv1.Provider, a *minderv1.Artifact) (versioner, error) {
ghprov, err := provifv1.As[provifv1.GitHub](prov)
if err == nil {
return &githubVersioner{
ghCli: ghprov,
artifact: a,
}, nil
}

ociprov, err := provifv1.As[provifv1.OCI](prov)
if err == nil {
return &ociVersioner{
ocicli: ociprov,
artifact: a,
}, nil
}

return nil, fmt.Errorf("Unable to get version lister from provider implementation")
}

type githubVersioner struct {
ghCli provifv1.GitHub
artifact *minderv1.Artifact
}

// in case of the GitHub provider, a package version may be
// linked to multiple tags
func (gv *githubVersioner) GetVersions(ctx context.Context) ([]*minderv1.ArtifactVersion, error) {
artifactName := url.QueryEscape(gv.artifact.GetName())
upstreamVersions, err := gv.ghCli.GetPackageVersions(
ctx, gv.artifact.GetOwner(), gv.artifact.GetTypeLower(), artifactName,
)
if err != nil {
return nil, fmt.Errorf("error retrieving artifact versions: %w", err)
}

out := make([]*minderv1.ArtifactVersion, 0, 10)
for _, uv := range upstreamVersions {
tags := uv.Metadata.Container.Tags
sort.Strings(tags)

// only the tags and creation time is relevant to us.
out = append(out, &minderv1.ArtifactVersion{
Tags: tags,
CreatedAt: timestamppb.New(uv.CreatedAt.Time),
})
}

return out, nil
}

type ociVersioner struct {
ocicli provifv1.OCI
artifact *minderv1.Artifact
}

func (ov *ociVersioner) GetVersions(ctx context.Context) ([]*minderv1.ArtifactVersion, error) {
tags, err := ov.ocicli.ListTags(ctx, ov.artifact.GetName())
if err != nil {
return nil, fmt.Errorf("error retrieving artifact versions: %w", err)
}

out := make([]*minderv1.ArtifactVersion, 0, 10)
for _, t := range tags {
// TODO: We probably should try to surface errors while returning a subset
// of manifests.
man, err := ov.ocicli.GetManifest(ctx, ov.artifact.GetName(), t)
if err != nil {
return nil, err
}

// NOTE/FIXME: This is going to be a hassle as not a lot of
// container images have the needed annotations. We'd need
// go down to a specific image configuration (e.g. for _some_
// architecture) to actually verify the creation date...
// Anybody has other ideas?
strcreated, ok := man.Annotations[v1.AnnotationCreated]
var createdAt time.Time
if !ok {
// TODO: Verify if this is correct
createdAt, err = time.Parse(time.RFC3339, strcreated)
if err != nil {
return nil, fmt.Errorf("Unable to get creation time for tag %s: %w", t, err)
}
} else {
// FIXME: This is a hack
createdAt = time.Now()
}

// TODO: Consider caching
digest, err := ov.ocicli.GetDigest(ctx, ov.artifact.GetName(), t)
if err != nil {
return nil, fmt.Errorf("Unable to get digest")
}

out = append(out, &minderv1.ArtifactVersion{
Tags: []string{t},
Sha: digest,
CreatedAt: timestamppb.New(createdAt),
})
}

return out, nil
}
6 changes: 1 addition & 5 deletions internal/engine/ingester/ingester.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,7 @@ func NewRuleDataIngest(rt *pb.RuleType, provider provinfv1.Provider) (engif.Inge
if rt.Def.Ingest.GetArtifact() == nil {
return nil, fmt.Errorf("rule type engine missing artifact configuration")
}
client, err := provinfv1.As[provinfv1.GitHub](provider)
if err != nil {
return nil, errors.New("provider does not implement github trait")
}
return artifact.NewArtifactDataIngest(client)
return artifact.NewArtifactDataIngest(provider)

case git.GitRuleDataIngestType:
client, err := provinfv1.As[provinfv1.Git](provider)
Expand Down
Loading

0 comments on commit 0640f74

Please sign in to comment.