Skip to content
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
47 changes: 42 additions & 5 deletions pkg/clients/oras/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package oras

import (
"context"
"encoding/json"
"fmt"
"os"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/openshift/library-go/pkg/image/reference"
oras "oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/file"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
Expand All @@ -27,12 +31,12 @@ func PullArtifacts(imagePullSpec string) (string, error) {

imageRef, err := reference.Parse(imagePullSpec)
if err != nil {
return "", err
return "", fmt.Errorf("cannot parse %s: %w", imagePullSpec, err)
}

repo, err := remote.NewRepository(imagePullSpec)
if err != nil {
return "", err
return "", fmt.Errorf("cannot get repository from %s: %w", imagePullSpec, err)
}
repo.Client = &auth.Client{
Client: retry.DefaultClient,
Expand All @@ -43,10 +47,43 @@ func PullArtifacts(imagePullSpec string) (string, error) {
}

ctx := context.Background()
tag := imageRef.Tag
if _, err := oras.Copy(ctx, repo, tag, fs, tag, oras.DefaultCopyOptions); err != nil {
return "", err
srcRef := imageRef.ID
if srcRef == "" {
srcRef = imageRef.Tag
}
dstRef := srcRef

opts := oras.DefaultCopyOptions
// Fetch only the artifacts directly referenced in the Image Manifest.
opts.FindSuccessors = noSuccessors

if _, err := oras.Copy(ctx, repo, srcRef, fs, dstRef, opts); err != nil {
return "", fmt.Errorf("copying %s: %w", imagePullSpec, err)
}

return storePath, nil
}

// noSuccessors returns the nodes directly pointed by the current node. By default oras will follow
// the "subject" of an Image Manifest. For artifacts that are attached to an image, this causes the
// image itself to also be pulled. Since oras doesn't provide a public function for fetching only
// the direct nodes we must roll our own. Ironically, the oras CLI also uses a custom behavior:
// https://github.com/oras-project/oras/blob/00a19d20644fe57d051d3b871579167dc2ff98e5/internal/graph/graph.go#L61
// This function only supports a very small set of subsets which is sufficient for e2e-tests.
func noSuccessors(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {

switch node.MediaType {
case ocispec.MediaTypeImageManifest:
content, err := content.FetchAll(ctx, fetcher, node)
if err != nil {
return nil, err
}
var manifest ocispec.Manifest
if err := json.Unmarshal(content, &manifest); err != nil {
return nil, err
}
return manifest.Layers, nil
default:
return nil, nil
}
}
57 changes: 55 additions & 2 deletions pkg/utils/build/task_results.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"io/fs"
"path/filepath"
"strings"

"github.com/konflux-ci/e2e-tests/pkg/clients/oras"
"github.com/konflux-ci/e2e-tests/pkg/constants"
pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -28,6 +32,8 @@ type ClairScanResult struct {
Vulnerabilities Vulnerabilities `json:"vulnerabilities"`
}

type ClairScanReports map[string]string

type Vulnerabilities struct {
Critical int `json:"critical"`
High int `json:"high"`
Expand All @@ -36,6 +42,17 @@ type Vulnerabilities struct {
}

func ValidateBuildPipelineTestResults(pipelineRun *pipeline.PipelineRun, c crclient.Client, isFBCBuild bool) error {
var imageURL string
for _, result := range pipelineRun.Status.Results {
if result.Name == "IMAGE_URL" {
imageURL = strings.TrimSpace(result.Value.StringVal)
break
}
}
if imageURL == "" {
return fmt.Errorf("unable to find IMAGE_URL result from PipelineRun %s", pipelineRun.Name)
}

for _, taskName := range taskNames {
// The inspect-image task is only required for FBC pipelines which we can infer by the component name

Expand All @@ -55,13 +72,14 @@ func ValidateBuildPipelineTestResults(pipelineRun *pipeline.PipelineRun, c crcli
switch taskName {
case "clair-scan":
resultsToValidate = append(resultsToValidate, "SCAN_OUTPUT")
resultsToValidate = append(resultsToValidate, "REPORTS")
case "deprecated-image-check":
resultsToValidate = append(resultsToValidate, "PYXIS_HTTP_CODE")
case "inspect-image":
resultsToValidate = append(resultsToValidate, "BASE_IMAGE", "BASE_IMAGE_REPOSITORY")
}

if err := validateTaskRunResult(results, resultsToValidate, taskName); err != nil {
if err := validateTaskRunResult(imageURL, results, resultsToValidate, taskName); err != nil {
return err
}

Expand All @@ -85,7 +103,7 @@ func fetchTaskRunResults(c crclient.Client, pr *pipeline.PipelineRun, pipelineTa
"pipelineTaskName %q not found in PipelineRun %s/%s", pipelineTaskName, pr.GetName(), pr.GetNamespace())
}

func validateTaskRunResult(trResults []pipeline.TaskRunResult, expectedResultNames []string, taskName string) error {
func validateTaskRunResult(imageURL string, trResults []pipeline.TaskRunResult, expectedResultNames []string, taskName string) error {
for _, rn := range expectedResultNames {
found := false
for _, r := range trResults {
Expand All @@ -104,6 +122,41 @@ func validateTaskRunResult(trResults []pipeline.TaskRunResult, expectedResultNam
if err != nil {
return fmt.Errorf("cannot parse SCAN_OUTPUT result: %+v", err)
}
case "REPORTS":
var reports = ClairScanReports{}
err := json.Unmarshal([]byte(r.Value.StringVal), &reports)
if err != nil {
return fmt.Errorf("cannot parse REPORTS result: %w", err)
}
for _, reportDigest := range reports {
reportRef := fmt.Sprintf("%s@%s", imageURL, reportDigest)

imageDir, err := oras.PullArtifacts(reportRef)
if err != nil {
return fmt.Errorf("cannot fetch report from ref %s: %w", reportRef, err)
}

var hasNonEmptyReport bool
if err := filepath.Walk(imageDir, func(p string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if info.Size() == 0 {
return fmt.Errorf("report %s from %s is empty", p, reportRef)
}
hasNonEmptyReport = true
return nil
}); err != nil {
return err
}

if !hasNonEmptyReport {
return fmt.Errorf("no report files were found in %s", reportRef)
}
}
case "PYXIS_HTTP_CODE", "BASE_IMAGE", "BASE_IMAGE_REPOSITORY":
if len(r.Value.StringVal) < 1 {
return fmt.Errorf("value of %q result is empty", r.Name)
Expand Down