Skip to content
Open
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
139 changes: 139 additions & 0 deletions cmd/image-processing/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ type settings struct {
secretPath string
vulnerabilitySettings resources.VulnerablilityScanParams
vulnerabilityCountLimit int

pushSourceBundle string
sourceBundleImage string

assembleIndex bool
platformImages []string
}

var flagValues settings
Expand Down Expand Up @@ -81,6 +87,12 @@ func initializeFlag() {
pflag.StringVar(&flagValues.resultFileImageVulnerabilities, "result-file-image-vulnerabilities", "", "A file to write the image vulnerabilities to")
pflag.Var(&flagValues.vulnerabilitySettings, "vuln-settings", "Vulnerability settings json string. One can enable the scan by setting {\"enabled\":true} to this option")
pflag.IntVar(&flagValues.vulnerabilityCountLimit, "vuln-count-limit", 50, "vulnerability count limit for the output of vulnerability scan")

pflag.StringVar(&flagValues.pushSourceBundle, "push-source-bundle", "", "Package this directory as an OCI artifact and push it")
pflag.StringVar(&flagValues.sourceBundleImage, "source-bundle-image", "", "Registry reference for the source bundle OCI artifact")

pflag.BoolVar(&flagValues.assembleIndex, "assemble-index", false, "Assemble an OCI image index from per-platform images")
pflag.StringArrayVar(&flagValues.platformImages, "platform-image", nil, "Per-platform image reference in os/arch=ref@digest format (repeatable)")
}

func main() {
Expand Down Expand Up @@ -128,6 +140,14 @@ func Execute(ctx context.Context) error {
flagValues.imageTimestamp = string(data)
}

if flagValues.pushSourceBundle != "" {
return runSourceBundlePush(ctx)
}

if flagValues.assembleIndex {
return runAssembleIndex(ctx)
}

return runImageProcessing(ctx)
}

Expand Down Expand Up @@ -302,3 +322,122 @@ func serializeVulnerabilities(Vulnerabilities []buildapi.Vulnerability) []byte {
}
return []byte(strings.Join(output, ","))
}

func runSourceBundlePush(ctx context.Context) error {
if flagValues.sourceBundleImage == "" {
return &ExitError{Code: 100, Message: "the 'source-bundle-image' argument is required when using 'push-source-bundle'"}
}

bundleRef, err := name.ParseReference(flagValues.sourceBundleImage)
if err != nil {
return fmt.Errorf("failed to parse source bundle image reference: %w", err)
}

log.Printf("Bundling source directory %q as OCI artifact\n", flagValues.pushSourceBundle)
img, err := image.BundleSourceDirectory(flagValues.pushSourceBundle)
if err != nil {
return fmt.Errorf("failed to bundle source directory: %w", err)
}

options, _, err := image.GetOptions(ctx, bundleRef, flagValues.insecure, flagValues.secretPath, "Shipwright Build")
if err != nil {
return fmt.Errorf("failed to get registry options: %w", err)
}

log.Printf("Pushing source bundle to %q\n", bundleRef.String())
digest, _, err := image.PushImageOrImageIndex(bundleRef, img, nil, options)
if err != nil {
return fmt.Errorf("failed to push source bundle: %w", err)
}
log.Printf("Source bundle %s@%s pushed\n", bundleRef.String(), digest)

if flagValues.resultFileImageDigest != "" {
if err := os.WriteFile(flagValues.resultFileImageDigest, []byte(digest), 0400); err != nil {
return err
}
}

return nil
}

func runAssembleIndex(ctx context.Context) error {
if flagValues.image == "" {
return &ExitError{Code: 100, Message: "the 'image' argument is required when using 'assemble-index'"}
}
if len(flagValues.platformImages) == 0 {
return &ExitError{Code: 100, Message: "at least one 'platform-image' argument is required when using 'assemble-index'"}
}

outputRef, err := name.ParseReference(flagValues.image)
if err != nil {
return fmt.Errorf("failed to parse output image reference: %w", err)
}

options, _, err := image.GetOptions(ctx, outputRef, flagValues.insecure, flagValues.secretPath, "Shipwright Build")
if err != nil {
return fmt.Errorf("failed to get registry options: %w", err)
}

platformEntries, err := ParsePlatformImages(flagValues.platformImages)
if err != nil {
return err
}

log.Printf("Assembling OCI image index for %d platforms\n", len(platformEntries))
imageIndex, err := image.AssembleImageIndex(platformEntries, options)
if err != nil {
return fmt.Errorf("failed to assemble image index: %w", err)
}

log.Printf("Pushing image index to %q\n", outputRef.String())
digest, size, err := image.PushImageOrImageIndex(outputRef, nil, imageIndex, options)
if err != nil {
return fmt.Errorf("failed to push image index: %w", err)
}
log.Printf("Image index %s@%s pushed\n", outputRef.String(), digest)

if digest != "" && flagValues.resultFileImageDigest != "" {
if err := os.WriteFile(flagValues.resultFileImageDigest, []byte(digest), 0400); err != nil {
return err
}
}

if size > 0 && flagValues.resultFileImageSize != "" {
if err := os.WriteFile(flagValues.resultFileImageSize, []byte(strconv.FormatInt(size, 10)), 0400); err != nil {
return err
}
}

return nil
}

// ParsePlatformImages parses --platform-image flags in the format "os/arch=ref@digest"
func ParsePlatformImages(entries []string) ([]image.PlatformImageEntry, error) {
var result []image.PlatformImageEntry
for _, entry := range entries {
parts := strings.SplitN(entry, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid platform-image format %q, expected os/arch=ref@digest", entry)
}

platformStr := parts[0]
imageRef := parts[1]

platformParts := strings.SplitN(platformStr, "/", 2)
if len(platformParts) != 2 {
return nil, fmt.Errorf("invalid platform format %q, expected os/arch", platformStr)
}

ref, err := name.ParseReference(imageRef)
if err != nil {
return nil, fmt.Errorf("failed to parse image reference %q: %w", imageRef, err)
}

result = append(result, image.PlatformImageEntry{
OS: platformParts[0],
Arch: platformParts[1],
ImageRef: ref,
})
}
return result, nil
}
181 changes: 181 additions & 0 deletions cmd/image-processing/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"path"
"strconv"
"strings"
"testing"
"time"

"github.com/google/go-containerregistry/pkg/crane"
Expand Down Expand Up @@ -491,6 +492,9 @@ var _ = Describe("Image Processing Resource", Ordered, func() {
})

It("should run vulnerability scanning on an image that is already pushed by the strategy", func() {
if testing.Short() {
Skip("skipping network-dependent test in -short mode")
}
ignoreVulnerabilities := buildapi.IgnoredHigh
vulnOptions := &buildapi.VulnerabilityScanOptions{
Enabled: true,
Expand Down Expand Up @@ -537,3 +541,180 @@ var _ = Describe("Image Processing Resource", Ordered, func() {
})
})
})

var _ = Describe("Source Bundle Push", func() {
run := func(args ...string) error {
log.SetOutput(GinkgoWriter)
os.Args = append([]string{"tool"}, args...)
tmp := os.Stderr
defer func() { os.Stderr = tmp }()
os.Stderr = nil
return Execute(context.Background())
}

AfterEach(func() {
pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
})

It("should bundle a source directory and push it as an OCI artifact", func() {
logLogger := log.Logger{}
logLogger.SetOutput(GinkgoWriter)
s := httptest.NewServer(registry.New(registry.Logger(&logLogger)))
defer s.Close()
u, err := url.Parse(s.URL)
Expect(err).ToNot(HaveOccurred())
endpoint := u.Host

srcDir, err := os.MkdirTemp("", "source-bundle-test")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(srcDir)

Expect(os.WriteFile(fmt.Sprintf("%s/main.go", srcDir), []byte("package main"), 0600)).To(Succeed())
Expect(os.MkdirAll(fmt.Sprintf("%s/pkg", srcDir), 0750)).To(Succeed())
Expect(os.WriteFile(fmt.Sprintf("%s/pkg/lib.go", srcDir), []byte("package pkg"), 0600)).To(Succeed())

imageRef := fmt.Sprintf("%s/test/source-bundle:latest", endpoint)
digestFile, err := os.CreateTemp("", "digest")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(digestFile.Name())

Expect(run(
"--push-source-bundle", srcDir,
"--source-bundle-image", imageRef,
"--insecure",
"--result-file-image-digest", digestFile.Name(),
)).To(Succeed())

ref, err := name.ParseReference(imageRef)
Expect(err).ToNot(HaveOccurred())

desc, err := remote.Get(ref)
Expect(err).ToNot(HaveOccurred())

img, err := desc.Image()
Expect(err).ToNot(HaveOccurred())

layers, err := img.Layers()
Expect(err).ToNot(HaveOccurred())
Expect(layers).To(HaveLen(1))

digestData, err := os.ReadFile(digestFile.Name())
Expect(err).ToNot(HaveOccurred())
Expect(string(digestData)).To(HavePrefix("sha256:"))
})

It("should fail when --source-bundle-image is missing", func() {
srcDir, err := os.MkdirTemp("", "source-bundle-test")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(srcDir)

Expect(run(
"--push-source-bundle", srcDir,
)).To(HaveOccurred())
})
})

var _ = Describe("Assemble Index", func() {
run := func(args ...string) error {
log.SetOutput(GinkgoWriter)
os.Args = append([]string{"tool"}, args...)
tmp := os.Stderr
defer func() { os.Stderr = tmp }()
os.Stderr = nil
return Execute(context.Background())
}

AfterEach(func() {
pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
})

It("should assemble an OCI image index from per-platform images", func() {
logLogger := log.Logger{}
logLogger.SetOutput(GinkgoWriter)
s := httptest.NewServer(registry.New(registry.Logger(&logLogger)))
defer s.Close()
u, err := url.Parse(s.URL)
Expect(err).ToNot(HaveOccurred())
endpoint := u.Host

amd64Tag := fmt.Sprintf("%s/test/app-linux-amd64:latest", endpoint)
arm64Tag := fmt.Sprintf("%s/test/app-linux-arm64:latest", endpoint)

amd64Ref, err := name.ParseReference(amd64Tag)
Expect(err).ToNot(HaveOccurred())
arm64Ref, err := name.ParseReference(arm64Tag)
Expect(err).ToNot(HaveOccurred())

Expect(remote.Write(amd64Ref, empty.Image)).To(Succeed())
Expect(remote.Write(arm64Ref, empty.Image)).To(Succeed())

amd64Digest, err := empty.Image.Digest()
Expect(err).ToNot(HaveOccurred())
arm64Digest, err := empty.Image.Digest()
Expect(err).ToNot(HaveOccurred())

outputRef := fmt.Sprintf("%s/test/app:latest", endpoint)
digestFile, err := os.CreateTemp("", "index-digest")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(digestFile.Name())

Expect(run(
"--assemble-index",
"--image", outputRef,
"--insecure",
"--platform-image", fmt.Sprintf("linux/amd64=%s@%s", amd64Tag, amd64Digest),
"--platform-image", fmt.Sprintf("linux/arm64=%s@%s", arm64Tag, arm64Digest),
"--result-file-image-digest", digestFile.Name(),
)).To(Succeed())

digestData, err := os.ReadFile(digestFile.Name())
Expect(err).ToNot(HaveOccurred())
Expect(string(digestData)).To(HavePrefix("sha256:"))

ref, err := name.ParseReference(outputRef)
Expect(err).ToNot(HaveOccurred())

desc, err := remote.Get(ref)
Expect(err).ToNot(HaveOccurred())
idx, err := desc.ImageIndex()
Expect(err).ToNot(HaveOccurred())

indexManifest, err := idx.IndexManifest()
Expect(err).ToNot(HaveOccurred())
Expect(indexManifest.Manifests).To(HaveLen(2))
})

It("should fail when --image is missing", func() {
Expect(run(
"--assemble-index",
"--platform-image", "linux/amd64=localhost:5000/test@sha256:abc",
)).To(HaveOccurred())
})

It("should fail when no --platform-image is provided", func() {
Expect(run(
"--assemble-index",
"--image", "localhost:5000/test:latest",
)).To(HaveOccurred())
})
})

var _ = Describe("ParsePlatformImages", func() {
It("should parse valid platform image entries", func() {
entries, err := ParsePlatformImages([]string{
"linux/amd64=registry.example.com/app@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"linux/arm64=registry.example.com/app@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
})
Expect(err).ToNot(HaveOccurred())
Expect(entries).To(HaveLen(2))
Expect(entries[0].OS).To(Equal("linux"))
Expect(entries[0].Arch).To(Equal("amd64"))
Expect(entries[1].OS).To(Equal("linux"))
Expect(entries[1].Arch).To(Equal("arm64"))
})

It("should fail when the format is invalid", func() {
_, err := ParsePlatformImages([]string{"not-a-valid-entry"})
Expect(err).To(HaveOccurred())
})
})
Loading
Loading