Skip to content

Commit

Permalink
refactor: layout
Browse files Browse the repository at this point in the history
Signed-off-by: Philip Laine <[email protected]>
  • Loading branch information
phillebaba committed Sep 24, 2024
1 parent 22531dd commit 96f9ac8
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 125 deletions.
7 changes: 4 additions & 3 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,15 @@ var packageMirrorCmd = &cobra.Command{
SkipSignatureValidation: pkgConfig.PkgOpts.SkipSignatureValidation,
Filter: filter,
}
pkgPaths, err := packager2.LoadPackage(cmd.Context(), loadOpt)
pkgLayout, err := packager2.LoadPackage(cmd.Context(), loadOpt)
if err != nil {
return err
}
defer os.RemoveAll(pkgPaths.Base)
//nolint: errcheck // ignore
defer pkgLayout.Cleanup()
mirrorOpt := packager2.MirrorOptions{
Cluster: c,
PackagePaths: *pkgPaths,
PkgLayouit: pkgLayout,
Filter: filter,
RegistryInfo: pkgConfig.InitOpts.RegistryInfo,
GitInfo: pkgConfig.InitOpts.GitServer,
Expand Down
300 changes: 300 additions & 0 deletions src/internal/packager2/layout/layout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package layout contains functions for inteacting the Zarf packages.
package layout

import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/defenseunicorns/pkg/helpers/v2"
goyaml "github.com/goccy/go-yaml"
registryv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/mholt/archiver/v3"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/verify"

"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/config"
"github.com/zarf-dev/zarf/src/pkg/packager/deprecated"
"github.com/zarf-dev/zarf/src/pkg/transform"
"github.com/zarf-dev/zarf/src/pkg/utils"
)

const (
zarfYAML = "zarf.yaml"
signature = "zarf.yaml.sig"
checksums = "checksums.txt"
imagesDir = "images"
componentsDir = "components"
)

// PackageLayout manages the layout for a package.
type PackageLayout struct {
dirPath string
Pkg v1alpha1.ZarfPackage
}

// PackageLayoutOptions are the options used when loading a package.
type PackageLayoutOptions struct {
PublicKeyPath string
SkipSignatureValidation bool
IsPartial bool
}

// LoadFromTar unpacks the give compressed package and loads it.
func LoadFromTar(ctx context.Context, tarPath string, opt PackageLayoutOptions) (*PackageLayout, error) {
dirPath, err := utils.MakeTempDir(config.CommonOptions.TempDirectory)
if err != nil {
return nil, err
}
err = archiver.Walk(tarPath, func(f archiver.File) error {
if f.IsDir() {
return nil
}
header, ok := f.Header.(*tar.Header)
if !ok {
return fmt.Errorf("expected header to be *tar.Header but was %T", f.Header)
}
// If path has nested directories we want to create them.
dir := filepath.Dir(header.Name)
if dir != "." {
err := os.MkdirAll(filepath.Join(dirPath, dir), helpers.ReadExecuteAllWriteUser)
if err != nil {
return err
}
}
dst, err := os.Create(filepath.Join(dirPath, header.Name))
if err != nil {
return err
}
defer dst.Close()
_, err = io.Copy(dst, f)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
p, err := LoadFromDir(ctx, dirPath, opt)
if err != nil {
return nil, err
}
return p, nil
}

// LoadFromDir loads and validates a package from the given directory path.
func LoadFromDir(ctx context.Context, dirPath string, opt PackageLayoutOptions) (*PackageLayout, error) {
b, err := os.ReadFile(filepath.Join(dirPath, zarfYAML))
if err != nil {
return nil, err
}
var pkg v1alpha1.ZarfPackage
err = goyaml.Unmarshal(b, &pkg)
if err != nil {
return nil, err
}
if len(pkg.Build.Migrations) > 0 {
for idx, component := range pkg.Components {
pkg.Components[idx], _ = deprecated.MigrateComponent(pkg.Build, component)
}
}

pkgLayout := &PackageLayout{
dirPath: dirPath,
Pkg: pkg,
}
err = validatePackageIntegrity(pkgLayout, opt.IsPartial)
if err != nil {
return nil, err
}
err = validatePackageSignature(ctx, pkgLayout, opt.PublicKeyPath, opt.SkipSignatureValidation)
if err != nil {
return nil, err
}
return pkgLayout, nil
}

// Cleanup removes any temporary directories created.
func (p *PackageLayout) Cleanup() error {
err := os.RemoveAll(p.dirPath)
if err != nil {
return err
}
return nil
}

// GetDirectory returns a path to the directory in the given component.
// TODO (phillebaba): Make dirType an enum.
func (p *PackageLayout) GetDirectory(component string, dirType string) (string, error) {
sourcePath := filepath.Join(p.dirPath, componentsDir, fmt.Sprintf("%s.tar", component))
_, err := os.Stat(sourcePath)
if err != nil {
return "", err
}
dirPath, err := utils.MakeTempDir(config.CommonOptions.TempDirectory)
if err != nil {
return "", err
}
err = archiver.Extract(sourcePath, filepath.Join(component, dirType), dirPath)
if err != nil {
return "", err
}
_, err = os.Stat(filepath.Join(dirPath, dirType))
if err != nil {
return "", err
}
return filepath.Join(dirPath, dirType), nil
}

// GetImage returns the image with the given reference in the package layout.
func (p *PackageLayout) GetImage(ref transform.Image) (registryv1.Image, error) {
// Use the manifest within the index.json to load the specific image we want
layoutPath := layout.Path(filepath.Join(p.dirPath, imagesDir))
imgIdx, err := layoutPath.ImageIndex()
if err != nil {
return nil, err
}
idxManifest, err := imgIdx.IndexManifest()
if err != nil {
return nil, err
}
// Search through all the manifests within this package until we find the annotation that matches our ref
for _, manifest := range idxManifest.Manifests {
if manifest.Annotations[ocispec.AnnotationBaseImageName] == ref.Reference ||
// A backwards compatibility shim for older Zarf versions that would leave docker.io off of image annotations
(manifest.Annotations[ocispec.AnnotationBaseImageName] == ref.Path+ref.TagOrDigest && ref.Host == "docker.io") {
// This is the image we are looking for, load it and then return
return layoutPath.Image(manifest.Digest)
}
}
return nil, fmt.Errorf("unable to find the image %s", ref.Reference)
}

func validatePackageIntegrity(pkgLayout *PackageLayout, isPartial bool) error {
_, err := os.Stat(filepath.Join(pkgLayout.dirPath, zarfYAML))
if err != nil {
return err
}
_, err = os.Stat(filepath.Join(pkgLayout.dirPath, checksums))
if err != nil {
return err
}
err = helpers.SHAsMatch(filepath.Join(pkgLayout.dirPath, checksums), pkgLayout.Pkg.Metadata.AggregateChecksum)
if err != nil {
return err
}

packageFiles := map[string]interface{}{}
err = filepath.Walk(pkgLayout.dirPath, func(path string, info fs.FileInfo, err error) error {
if info.IsDir() {
return nil
}
packageFiles[path] = nil
return err
})
if err != nil {
return err
}
// Remove files which are not in the checksums.
delete(packageFiles, filepath.Join(pkgLayout.dirPath, zarfYAML))
delete(packageFiles, filepath.Join(pkgLayout.dirPath, checksums))
delete(packageFiles, filepath.Join(pkgLayout.dirPath, signature))

b, err := os.ReadFile(filepath.Join(pkgLayout.dirPath, checksums))
if err != nil {
return err
}
lines := strings.Split(string(b), "\n")
for _, line := range lines {
// If the line is empty (i.e. there is no checksum) simply skip it, this can result from a package with no images/components.
if line == "" {
continue
}

split := strings.Split(line, " ")
if len(split) != 2 {
return fmt.Errorf("invalid checksum line: %s", line)
}
sha := split[0]
rel := split[1]
if sha == "" || rel == "" {
return fmt.Errorf("invalid checksum line: %s", line)
}

path := filepath.Join(pkgLayout.dirPath, rel)
_, ok := packageFiles[path]
if !ok && isPartial {
delete(packageFiles, path)
continue
}
if !ok {
return fmt.Errorf("file %s from checksum missing in layout", rel)
}
err = helpers.SHAsMatch(path, sha)
if err != nil {
return err
}
delete(packageFiles, path)
}

if len(packageFiles) > 0 {
// TODO (phillebaba): Replace with maps.Keys after upgrading to Go 1.23.
filePaths := []string{}
for k := range packageFiles {
filePaths = append(filePaths, k)
}
return fmt.Errorf("package contains additional files not present in the checksum %s", strings.Join(filePaths, ", "))
}

return nil
}

func validatePackageSignature(ctx context.Context, pkgLayout *PackageLayout, publicKeyPath string, skipSignatureValidation bool) error {
if skipSignatureValidation {
return nil
}

signaturePath := filepath.Join(pkgLayout.dirPath, signature)
zarfYamlPath := filepath.Join(pkgLayout.dirPath, zarfYAML)

sigExist := true
_, err := os.Stat(signaturePath)
if err != nil {
sigExist = false
}
if !sigExist && publicKeyPath == "" {
// Nobody was expecting a signature, so we can just return
return nil
} else if sigExist && publicKeyPath == "" {
return errors.New("package is signed but no key was provided")
} else if !sigExist && publicKeyPath != "" {
return errors.New("a key was provided but the package is not signed")
}

keyOptions := options.KeyOpts{KeyRef: publicKeyPath}
cmd := &verify.VerifyBlobCmd{
KeyOpts: keyOptions,
SigRef: signaturePath,
IgnoreSCT: true,
Offline: true,
IgnoreTlog: true,
}
err = cmd.Exec(ctx, zarfYamlPath)
if err != nil {
return fmt.Errorf("package signature did not match the provided key: %w", err)
}
return nil
}
Loading

0 comments on commit 96f9ac8

Please sign in to comment.