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

feat: support cosign signatures / attestations and discover in zarf prepare find-images #2027

Merged
merged 32 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
968e7f5
add signatures
rjferguson21 Sep 21, 2023
9c40306
Merge branch 'main' into signatures
mjnagel Sep 21, 2023
65eb086
fix: cache, sbom, skipping
mjnagel Sep 22, 2023
fd77f55
chore: move example to examples
mjnagel Sep 25, 2023
ada9357
fix: comment
mjnagel Sep 25, 2023
af0929a
Merge branch 'main' into signatures
Racer159 Sep 26, 2023
d376d39
Merge branch 'main' into signatures
mjnagel Sep 27, 2023
2eee4ce
fix: pr comments
mjnagel Sep 27, 2023
1328b17
docs: additions from autogen
mjnagel Sep 27, 2023
cda70d8
fix: skip cosign for diff test
mjnagel Sep 27, 2023
ac792e9
fix: s
mjnagel Sep 27, 2023
6520970
fix: doh
mjnagel Sep 27, 2023
0696c8a
Merge branch 'main' into signatures
Racer159 Oct 3, 2023
e15af7f
Merge branch 'main' into signatures
mjnagel Oct 4, 2023
87eaf8b
Merge branch 'main' into signatures
mjnagel Oct 5, 2023
44173fc
chore: cleanup things
mjnagel Oct 5, 2023
642f1b6
fix: test
mjnagel Oct 5, 2023
5276585
ci: skip cosign for sbom test
mjnagel Oct 5, 2023
2ae7cbd
fix: ci/cleanup things
mjnagel Oct 6, 2023
3f0b90b
fix: add comment
mjnagel Oct 6, 2023
caed87e
chore: refactor to helpers
mjnagel Oct 6, 2023
95d4d74
ci: test fix length check
mjnagel Oct 6, 2023
dfe133a
chore: rename to cosign util
mjnagel Oct 6, 2023
5bae063
chore: cleanup
mjnagel Oct 6, 2023
f202d1e
ci: fix length checks
mjnagel Oct 6, 2023
219db79
Merge branch 'main' into signatures
mjnagel Oct 12, 2023
b35379b
chore: move to prepare find-images
mjnagel Oct 12, 2023
71b429c
change pull return
mjnagel Oct 12, 2023
2e69b17
cleanups
mjnagel Oct 12, 2023
3f2d56d
updates from pr feedback
mjnagel Oct 12, 2023
062809d
add comment
mjnagel Oct 12, 2023
f61288f
Update src/pkg/utils/cosign.go
mjnagel Oct 12, 2023
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
74 changes: 49 additions & 25 deletions src/internal/packager/images/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,23 @@ import (
"github.com/pterm/pterm"
)

// ImgInfo wraps references/information about an image
type ImgInfo struct {
RefInfo transform.Image
Img v1.Image
HasImageLayers bool
}

// PullAll pulls all of the images in the provided tag map.
func (i *ImageConfig) PullAll() (map[transform.Image]v1.Image, error) {
func (i *ImageConfig) PullAll() ([]ImgInfo, error) {
var (
longer string
imageCount = len(i.ImageList)
refInfoToImage = map[transform.Image]v1.Image{}
referenceToDigest = make(map[string]string)
imgInfoList []ImgInfo
)

type imgInfo struct {
refInfo transform.Image
img v1.Image
}

type digestInfo struct {
refInfo transform.Image
digest string
Expand All @@ -65,7 +68,7 @@ func (i *ImageConfig) PullAll() (map[transform.Image]v1.Image, error) {
logs.Warn.SetOutput(&message.DebugWriter{})
logs.Progress.SetOutput(&message.DebugWriter{})

metadataImageConcurrency := utils.NewConcurrencyTools[imgInfo, error](len(i.ImageList))
metadataImageConcurrency := utils.NewConcurrencyTools[ImgInfo, error](len(i.ImageList))

defer metadataImageConcurrency.Cancel()

Expand Down Expand Up @@ -97,7 +100,7 @@ func (i *ImageConfig) PullAll() (map[transform.Image]v1.Image, error) {
return
}

img, err := i.PullImage(actualSrc, spinner)
img, hasImageLayers, err := i.PullImage(actualSrc, spinner)
if err != nil {
metadataImageConcurrency.ErrorChan <- fmt.Errorf("failed to pull image %s: %w", actualSrc, err)
return
Expand All @@ -107,13 +110,14 @@ func (i *ImageConfig) PullAll() (map[transform.Image]v1.Image, error) {
return
}

metadataImageConcurrency.ProgressChan <- imgInfo{refInfo: refInfo, img: img}
metadataImageConcurrency.ProgressChan <- ImgInfo{RefInfo: refInfo, Img: img, HasImageLayers: hasImageLayers}
}()
}

onMetadataProgress := func(finishedImage imgInfo, iteration int) {
spinner.Updatef("Fetching image metadata (%d of %d): %s", iteration+1, len(i.ImageList), finishedImage.refInfo.Reference)
refInfoToImage[finishedImage.refInfo] = finishedImage.img
onMetadataProgress := func(finishedImage ImgInfo, iteration int) {
spinner.Updatef("Fetching image metadata (%d of %d): %s", iteration+1, len(i.ImageList), finishedImage.RefInfo.Reference)
refInfoToImage[finishedImage.RefInfo] = finishedImage.Img
imgInfoList = append(imgInfoList, finishedImage)
}

onMetadataError := func(err error) error {
Expand Down Expand Up @@ -426,15 +430,23 @@ func (i *ImageConfig) PullAll() (map[transform.Image]v1.Image, error) {
doneSaving <- 1
progressBarWaitGroup.Wait()

return refInfoToImage, nil
return imgInfoList, nil
}

// PullImage returns a v1.Image either by loading a local tarball or the wider internet.
func (i *ImageConfig) PullImage(src string, spinner *message.Spinner) (img v1.Image, err error) {
// PullImage returns a v1.Image either by loading a local tarball or pulling from the wider internet.
func (i *ImageConfig) PullImage(src string, spinner *message.Spinner) (img v1.Image, hasImageLayers bool, err error) {
// Load image tarballs from the local filesystem.
if strings.HasSuffix(src, ".tar") || strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") {
spinner.Updatef("Reading image tarball: %s", src)
return crane.Load(src, config.GetCraneOptions(true, i.Architectures...)...)
img, err = crane.Load(src, config.GetCraneOptions(true, i.Architectures...)...)
if err != nil {
return nil, false, err
}
hasImageLayers, err = utils.HasImageLayers(img)
if err != nil {
return nil, false, fmt.Errorf("failed to check image %s layer mediatype: %w", src, err)
}
mjnagel marked this conversation as resolved.
Show resolved Hide resolved
return img, hasImageLayers, err
}

// If crane is unable to pull the image, try to load it from the local docker daemon.
Expand All @@ -445,21 +457,21 @@ func (i *ImageConfig) PullImage(src string, spinner *message.Spinner) (img v1.Im
// Parse the image reference to get the image name.
reference, err := name.ParseReference(src)
if err != nil {
return nil, fmt.Errorf("failed to parse image reference %s: %w", src, err)
return nil, false, fmt.Errorf("failed to parse image reference %s: %w", src, err)
}

// Attempt to connect to the local docker daemon.
ctx := context.TODO()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, fmt.Errorf("docker not available: %w", err)
return nil, false, fmt.Errorf("docker not available: %w", err)
}
cli.NegotiateAPIVersion(ctx)

// Inspect the image to get the size.
rawImg, _, err := cli.ImageInspectWithRaw(ctx, src)
if err != nil {
return nil, fmt.Errorf("failed to inspect image %s via docker: %w", src, err)
return nil, false, fmt.Errorf("failed to inspect image %s via docker: %w", src, err)
}

// Warn the user if the image is large.
Expand All @@ -473,21 +485,33 @@ func (i *ImageConfig) PullImage(src string, spinner *message.Spinner) (img v1.Im
// Use unbuffered opener to avoid OOM Kill issues https://github.com/defenseunicorns/zarf/issues/1214.
// This will also take for ever to load large images.
if img, err = daemon.Image(reference, daemon.WithUnbufferedOpener()); err != nil {
return nil, fmt.Errorf("failed to load image %s from docker daemon: %w", src, err)
return nil, false, fmt.Errorf("failed to load image %s from docker daemon: %w", src, err)
}

// The pull from the docker daemon was successful, return the image.
return img, err
hasImageLayers, err = utils.HasImageLayers(img)
if err != nil {
return nil, false, fmt.Errorf("failed to check image %s layer mediatype: %w", src, err)
}
return img, hasImageLayers, err
}

// Manifest was found, so use crane to pull the image.
if img, err = crane.Pull(src, config.GetCraneOptions(i.Insecure, i.Architectures...)...); err != nil {
return nil, fmt.Errorf("failed to pull image %s: %w", src, err)
return nil, false, fmt.Errorf("failed to pull image %s: %w", src, err)
}

spinner.Updatef("Preparing image %s", src)
imageCachePath := filepath.Join(config.GetAbsCachePath(), layout.ImagesDir)
img = cache.Image(img, cache.NewFilesystemCache(imageCachePath))

return img, nil
hasImageLayers, err = utils.HasImageLayers(img)
if err != nil {
return nil, false, fmt.Errorf("failed to check image %s layer mediatype: %w", src, err)
}

if hasImageLayers {
imageCachePath := filepath.Join(config.GetAbsCachePath(), layout.ImagesDir)
img = cache.Image(img, cache.NewFilesystemCache(imageCachePath))
}

return img, hasImageLayers, nil
}
13 changes: 8 additions & 5 deletions src/pkg/packager/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
"github.com/defenseunicorns/zarf/src/pkg/utils/helpers"
"github.com/defenseunicorns/zarf/src/types"
"github.com/go-git/go-git/v5/plumbing"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/mholt/archiver/v3"
)

Expand Down Expand Up @@ -156,14 +155,15 @@ func (p *Packager) Create() (err error) {
}

imageList := helpers.Unique(combinedImageList)
var sbomImageList []transform.Image

// Images are handled separately from other component assets.
if len(imageList) > 0 {
message.HeaderInfof("📦 PACKAGE IMAGES")

p.layout = p.layout.AddImages()

pulled := map[transform.Image]v1.Image{}
var pulled []images.ImgInfo

doPull := func() error {
imgConfig := images.ImageConfig{
Expand All @@ -182,10 +182,13 @@ func (p *Packager) Create() (err error) {
return fmt.Errorf("unable to pull images after 3 attempts: %w", err)
}

for _, img := range pulled {
if err := p.layout.Images.AddV1Image(img); err != nil {
for _, imgInfo := range pulled {
if err := p.layout.Images.AddV1Image(imgInfo.Img); err != nil {
return err
}
if imgInfo.HasImageLayers {
sbomImageList = append(sbomImageList, imgInfo.RefInfo)
}
}
}

Expand All @@ -194,7 +197,7 @@ func (p *Packager) Create() (err error) {
message.Debug("Skipping image SBOM processing per --skip-sbom flag")
} else {
p.layout = p.layout.AddSBOMs()
if err := sbom.Catalog(componentSBOMs, imageList, p.layout); err != nil {
if err := sbom.Catalog(componentSBOMs, sbomImageList, p.layout); err != nil {
return fmt.Errorf("unable to create an SBOM catalog for the package: %w", err)
}
}
Expand Down
32 changes: 29 additions & 3 deletions src/pkg/packager/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {

imagesMap := make(map[string][]string)
erroredCharts := []string{}
erroredCosignLookups := []string{}

cwd, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -235,6 +236,15 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
// Use print because we want this dumped to stdout
imagesMap[component.Name] = append(imagesMap[component.Name], image)
componentDefinition += fmt.Sprintf(" - %s\n", image)
cosignArtifacts, err := utils.GetCosignArtifacts(image)
if err != nil {
message.WarnErrf(err, "Problem looking up cosign artifacts for %s: %s", image, err.Error())
erroredCosignLookups = append(erroredCosignLookups, image)
}
imagesMap[component.Name] = append(imagesMap[component.Name], cosignArtifacts...)
for _, cosignArtifact := range cosignArtifacts {
componentDefinition += fmt.Sprintf(" - %s\n", cosignArtifact)
}
}
}

Expand All @@ -249,6 +259,12 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
// Otherwise, add to the list of images
message.Debugf("Imaged digest found: %s", descriptor.Digest)
validImages = append(validImages, image)
cosignArtifacts, err := utils.GetCosignArtifacts(image)
if err != nil {
message.WarnErrf(err, "Problem looking up cosign artifacts for %s: %s", image, err.Error())
erroredCosignLookups = append(erroredCosignLookups, image)
}
validImages = append(validImages, cosignArtifacts...)
mjnagel marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -268,16 +284,26 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
return nil, err
}

if len(erroredCharts) > 0 {
return imagesMap, fmt.Errorf("the following charts had errors: %s", erroredCharts)
if len(erroredCharts) > 0 || len(erroredCosignLookups) > 0 {
errMsg := ""
if len(erroredCharts) > 0 {
errMsg = fmt.Sprintf("the following charts had errors: %s", erroredCharts)
}
if len(erroredCosignLookups) > 0 {
if errMsg != "" {
errMsg += "\n"
}
errMsg += fmt.Sprintf("the following images errored on cosign lookups: %s", erroredCosignLookups)
}
return imagesMap, fmt.Errorf(errMsg)
}

return imagesMap, nil
}

func (p *Packager) processUnstructuredImages(resource *unstructured.Unstructured, matchedImages, maybeImages k8s.ImageMap) (k8s.ImageMap, k8s.ImageMap, error) {
var imageSanityCheck = regexp.MustCompile(`(?mi)"image":"([^"]+)"`)
var imageFuzzyCheck = regexp.MustCompile(`(?mi)"([a-z0-9\-.\/]+:[\w][\w.\-]{0,127})"`)
var imageFuzzyCheck = regexp.MustCompile(`(?mi)["|=]([a-z0-9\-.\/:]+:[\w.\-]*[a-z\.\-][\w.\-]*)"`)
var json string

contents := resource.UnstructuredContent()
Expand Down
37 changes: 37 additions & 0 deletions src/pkg/utils/sget.go → src/pkg/utils/cosign.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,40 @@ func CosignSignBlob(blobPath string, outputSigPath string, keyPath string, passw

return sig, err
}

// GetCosignArtifacts returns signatures and attestations for the given image
func GetCosignArtifacts(image string) (cosignList []string, err error) {
mjnagel marked this conversation as resolved.
Show resolved Hide resolved
var cosignArtifactList []string
var nameOpts []name.Option
ref, err := name.ParseReference(image, nameOpts...)

if err != nil {
return cosignArtifactList, err
}

var remoteOpts []ociremote.Option
simg, _ := ociremote.SignedEntity(ref, remoteOpts...)
if simg == nil {
return cosignArtifactList, nil
}
// Errors are dogsled because these functions always return a name.Tag which we can check for layers
sigRef, _ := ociremote.SignatureTag(ref, remoteOpts...)
attRef, _ := ociremote.AttestationTag(ref, remoteOpts...)
mjnagel marked this conversation as resolved.
Show resolved Hide resolved

sigs, err := simg.Signatures()
if err == nil {
layers, _ := sigs.Layers()
if len(layers) > 0 {
cosignArtifactList = append(cosignArtifactList, sigRef.String())
}
}

atts, err := simg.Attestations()
if err == nil {
layers, _ := atts.Layers()
if len(layers) > 0 {
cosignArtifactList = append(cosignArtifactList, attRef.String())
}
}
mjnagel marked this conversation as resolved.
Show resolved Hide resolved
return cosignArtifactList, nil
}
19 changes: 19 additions & 0 deletions src/pkg/utils/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,22 @@ func AddImageNameAnnotation(ociPath string, referenceToDigest map[string]string)
}
return os.WriteFile(indexPath, indexJSONBytes, 0600)
}

// HasImageLayers checks if any layers in the v1.Image are known image layers.
func HasImageLayers(img v1.Image) (bool, error) {
layers, err := img.Layers()
if err != nil {
return false, err
}
for _, layer := range layers {
mediatype, err := layer.MediaType()
if err != nil {
return false, err
}
// Check if mediatype is a known image layer
if mediatype.IsLayer() {
return true, nil
}
}
return false, nil
}
1 change: 1 addition & 0 deletions src/test/e2e/00_use_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func TestUseCLI(t *testing.T) {
stdOut, stdErr, err = e2e.Zarf("prepare", "find-images", "--kube-version=v1.22.0", "src/test/packages/00-kube-version-override")
require.NoError(t, err, stdOut, stdErr)
require.Contains(t, stdOut, "quay.io/jetstack/cert-manager-controller:v1.11.1", "The chart image should be found by Zarf")
require.Contains(t, stdOut, "quay.io/jetstack/cert-manager-controller:sha256-4f1782c8316f34aae6b9ab823c3e6b7e6e4d92ec5dac21de6a17c3da44c364f1.sig", "The image signature should be found by Zarf")
})

t.Run("zarf deploy should fail when given a bad component input", func(t *testing.T) {
Expand Down
33 changes: 33 additions & 0 deletions src/test/e2e/10_create_cosign_artifacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package test provides e2e tests for Zarf.
package test

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
)

func TestCosignArtifacts(t *testing.T) {
t.Log("E2E: Cosign artifacts")

var (
createPath = "src/test/packages/10-cosign-artifacts"
packageName = fmt.Sprintf("zarf-package-cosign-artifacts-%s.tar.zst", e2e.Arch)
)

e2e.CleanFiles(packageName)

// Create the package
stdOut, stdErr, err := e2e.Zarf("package", "create", createPath, "--confirm")
require.NoError(t, err, stdOut, stdErr)

// Create the package a second time to validate caching does not cause errors
stdOut, stdErr, err = e2e.Zarf("package", "create", createPath, "--confirm")
require.NoError(t, err, stdOut, stdErr)

e2e.CleanFiles(packageName)
}
9 changes: 9 additions & 0 deletions src/test/packages/10-cosign-artifacts/zarf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
kind: ZarfPackageConfig
metadata:
name: cosign-artifacts
description: Test package for cosign image artifact pulls

components:
- name: test
images:
- ghcr.io/defenseunicorns/zarf/agent:sha256-90863c246da361499e8f59dfae728c34b50ee2057e61e06dacdcfad983425c32.sig
mjnagel marked this conversation as resolved.
Show resolved Hide resolved
Loading