diff --git a/cmd/buildah/mkcw.go b/cmd/buildah/mkcw.go index 41dd3cb4d..71e7e711e 100644 --- a/cmd/buildah/mkcw.go +++ b/cmd/buildah/mkcw.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "strings" "github.com/containers/buildah" "github.com/containers/buildah/pkg/parse" @@ -38,6 +39,7 @@ func mkcwCmd(c *cobra.Command, args []string, options buildah.CWConvertImageOpti func init() { var teeType string + var addFile []string var options buildah.CWConvertImageOptions mkcwDescription := `Convert a conventional image to a confidential workload image.` mkcwCommand := &cobra.Command{ @@ -46,6 +48,23 @@ func init() { Long: mkcwDescription, RunE: func(cmd *cobra.Command, args []string) error { options.TeeType = parse.TeeType(teeType) + if len(addFile) > 0 { + options.ExtraImageContent = make(map[string]string) + for _, spec := range addFile { + source, dest, haveDest := strings.Cut(spec, ":") + if !haveDest { + dest = source + } + st, err := os.Stat(source) + if err != nil { + return fmt.Errorf("parsing add-file argument %q: source %q: %w", spec, source, err) + } + if st.IsDir() { + return fmt.Errorf("parsing add-file argument %q: source %q is not a regular file", spec, source) + } + options.ExtraImageContent[dest] = source + } + } return mkcwCmd(cmd, args, options) }, Example: `buildah mkcw localhost/repository:typical localhost/repository:cw`, @@ -57,6 +76,7 @@ func init() { flags.SetInterspersed(false) flags.StringVarP(&teeType, "type", "t", "", "TEE (trusted execution environment) type: SEV,SNP (default: SNP)") + flags.StringArrayVar(&addFile, "add-file", nil, "add contents of a file to the image at a specified path (`source:destination`)") flags.StringVarP(&options.AttestationURL, "attestation-url", "u", "", "attestation server URL") flags.StringVarP(&options.BaseImage, "base-image", "b", "", "alternate base image (default: scratch)") flags.StringVarP(&options.DiskEncryptionPassphrase, "passphrase", "p", "", "disk encryption passphrase") diff --git a/convertcw.go b/convertcw.go index ede4f24c8..be12b7f84 100644 --- a/convertcw.go +++ b/convertcw.go @@ -45,6 +45,7 @@ type CWConvertImageOptions struct { FirmwareLibrary string BaseImage string Logger *logrus.Logger + ExtraImageContent map[string]string // Passed through to BuilderOptions. Most settings won't make // sense to be made available here because we don't launch a process. @@ -172,6 +173,7 @@ func CWConvertImage(ctx context.Context, systemContext *types.SystemContext, sto FirmwareLibrary: options.FirmwareLibrary, Logger: logger, GraphOptions: store.GraphOptions(), + ExtraImageContent: options.ExtraImageContent, } rc, workloadConfig, err := mkcw.Archive(sourceDir, &source.OCIv1, archiveOptions) if err != nil { diff --git a/docs/buildah-mkcw.1.md b/docs/buildah-mkcw.1.md index 273332745..cc49a8218 100644 --- a/docs/buildah-mkcw.1.md +++ b/docs/buildah-mkcw.1.md @@ -21,6 +21,14 @@ A container image, stored locally or in a registry ## OPTIONS +**--add-file** *source[:destination]* + +Read the contents of the file `source` and add it to the committed image as a +file at `destination`. If `destination` is not specified, the path of `source` +will be used. The new file will be owned by UID 0, GID 0, have 0644 +permissions, and be given a current timestamp. This option can be specified +multiple times. + **--attestation-url**, **-u** *url* The location of a key broker / attestation server. If a value is specified, the new image's workload ID, along with the passphrase diff --git a/image.go b/image.go index d1c3a3c11..c3acaf5d2 100644 --- a/image.go +++ b/image.go @@ -204,9 +204,7 @@ func (i *containerImageRef) extractConfidentialWorkloadFS(options ConfidentialWo Slop: options.Slop, FirmwareLibrary: options.FirmwareLibrary, GraphOptions: i.store.GraphOptions(), - } - if len(i.extraImageContent) > 0 { - logrus.Warnf("ignoring extra requested content %v, not implemented (yet)", i.extraImageContent) + ExtraImageContent: i.extraImageContent, } rc, _, err := mkcw.Archive(mountPoint, &image, archiveOptions) if err != nil { diff --git a/internal/mkcw/archive.go b/internal/mkcw/archive.go index 6caea17df..7517205da 100644 --- a/internal/mkcw/archive.go +++ b/internal/mkcw/archive.go @@ -54,6 +54,7 @@ type ArchiveOptions struct { FirmwareLibrary string Logger *logrus.Logger GraphOptions []string // passed in from a storage Store, probably + ExtraImageContent map[string]string } type chainRetrievalError struct { @@ -70,9 +71,7 @@ func (c chainRetrievalError) Error() string { // Archive generates a WorkloadConfig for a specified directory and produces a // tar archive of a container image's rootfs with the expected contents. -// The input directory will have a ".krun_config.json" file added to it while -// this function is running, but it will be removed on completion. -func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadCloser, WorkloadConfig, error) { +func Archive(rootfsPath string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadCloser, WorkloadConfig, error) { const ( teeDefaultCPUs = 2 teeDefaultMemory = 512 @@ -80,7 +79,7 @@ func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadC teeDefaultTeeType = SNP ) - if path == "" { + if rootfsPath == "" { return nil, WorkloadConfig{}, fmt.Errorf("required path not specified") } logger := options.Logger @@ -103,7 +102,7 @@ func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadC filesystem := teeDefaultFilesystem workloadID := options.WorkloadID if workloadID == "" { - digestInput := path + filesystem + time.Now().String() + digestInput := rootfsPath + filesystem + time.Now().String() workloadID = digest.Canonical.FromString(digestInput).Encoded() } workloadConfig := WorkloadConfig{ @@ -176,7 +175,7 @@ func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadC // We're going to want to add some content to the rootfs, so set up an // overlay that uses it as a lower layer so that we can write to it. - st, err := system.Stat(path) + st, err := system.Stat(rootfsPath) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("reading information about the container root filesystem: %w", err) } @@ -220,7 +219,7 @@ func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadC } }() // Create a mount point using that working state. - rootfsMount, err := overlay.Mount(overlayTempDir, path, rootfsDir, 0, 0, options.GraphOptions) + rootfsMount, err := overlay.Mount(overlayTempDir, rootfsPath, rootfsDir, 0, 0, options.GraphOptions) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("setting up support for overlay of container root filesystem: %w", err) } @@ -243,14 +242,46 @@ func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadC } }() // Pretend that we didn't have to do any of the preceding. - path = rootfsDir + rootfsPath = rootfsDir + + // Write extra content to the rootfs, creating intermediate directories if necessary. + for location, content := range options.ExtraImageContent { + err := func() error { + if err := idtools.MkdirAllAndChownNew(filepath.Dir(filepath.Join(rootfsPath, location)), 0o755, idtools.IDPair{UID: int(st.UID()), GID: int(st.GID())}); err != nil { + return fmt.Errorf("ensuring %q is present in container root filesystem: %w", filepath.Dir(location), err) + } + output, err := os.OpenFile(filepath.Join(rootfsPath, location), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("preparing to write %q to container root filesystem: %w", location, err) + } + defer output.Close() + input, err := os.Open(content) + if err != nil { + return err + } + defer input.Close() + if _, err := io.Copy(output, input); err != nil { + return fmt.Errorf("copying contents of %q to %q in container root filesystem: %w", content, location, err) + } + if err := output.Chown(int(st.UID()), int(st.GID())); err != nil { + return fmt.Errorf("setting owner of %q in the container root filesystem: %w", location, err) + } + if err := output.Chmod(0o644); err != nil { + return fmt.Errorf("setting permissions on %q in the container root filesystem: %w", location, err) + } + return nil + }() + if err != nil { + return nil, WorkloadConfig{}, err + } + } // Write part of the config blob where the krun init process will be // looking for it. The oci2cw tool used `buildah inspect` output, but // init is just looking for fields that have the right names in any // object, and the image's config will have that, so let's try encoding // it directly. - krunConfigPath := filepath.Join(path, ".krun_config.json") + krunConfigPath := filepath.Join(rootfsPath, ".krun_config.json") krunConfigBytes, err := json.Marshal(ociConfig) if err != nil { return nil, WorkloadConfig{}, fmt.Errorf("creating .krun_config from image configuration: %w", err) @@ -288,7 +319,7 @@ func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadC imageSize := slop(options.ImageSize, options.Slop) if imageSize == 0 { var sourceSize int64 - if err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { + if err := filepath.WalkDir(rootfsPath, func(path string, d fs.DirEntry, err error) error { if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, os.ErrPermission) { return err } @@ -336,7 +367,7 @@ func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadC } // Format the disk image with the filesystem contents. - if _, stderr, err := MakeFS(path, plain.Name(), filesystem); err != nil { + if _, stderr, err := MakeFS(rootfsPath, plain.Name(), filesystem); err != nil { if strings.TrimSpace(stderr) != "" { return nil, WorkloadConfig{}, fmt.Errorf("%s: %w", strings.TrimSpace(stderr), err) } @@ -456,8 +487,8 @@ func Archive(path string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadC tmpHeader.Name = "tmp/" tmpHeader.Typeflag = tar.TypeDir tmpHeader.Mode = 0o1777 - tmpHeader.Uname, workloadConfigHeader.Gname = "", "" - tmpHeader.Uid, workloadConfigHeader.Gid = 0, 0 + tmpHeader.Uname, tmpHeader.Gname = "", "" + tmpHeader.Uid, tmpHeader.Gid = 0, 0 tmpHeader.Size = 0 if err = tw.WriteHeader(tmpHeader); err != nil { logrus.Errorf("writing header for %q: %v", tmpHeader.Name, err) diff --git a/tests/mkcw.bats b/tests/mkcw.bats index c1a185ef5..54e6a9e66 100644 --- a/tests/mkcw.bats +++ b/tests/mkcw.bats @@ -35,6 +35,15 @@ function mkcw_check_image() { test -d "$TEST_SCRATCH_DIR"/mount/tmp # Should have a /bin/sh file from the base image, at least. test -s "$TEST_SCRATCH_DIR"/mount/bin/sh || test -L "$TEST_SCRATCH_DIR"/mount/bin/sh + if shift ; then + if shift ; then + for pair in "$@" ; do + inner=${pair##*:} + outer=${pair%%:*} + cmp ${outer} "$TEST_SCRATCH_DIR"/mount/${inner} + done + fi + fi # Clean up. umount "$TEST_SCRATCH_DIR"/mount @@ -50,14 +59,16 @@ function mkcw_check_image() { fi _prefetch busybox _prefetch bash + createrandom randomfile1 + createrandom randomfile2 echo -n mkcw-convert > "$TEST_SCRATCH_DIR"/key # image has one layer, check with all-lower-case TEE type name - run_buildah mkcw --ignore-attestation-errors --type snp --passphrase=mkcw-convert busybox busybox-cw - mkcw_check_image busybox-cw + run_buildah mkcw --ignore-attestation-errors --type snp --passphrase=mkcw-convert --add-file randomfile1:/in-a-subdir/rnd1 busybox busybox-cw + mkcw_check_image busybox-cw "" randomfile1:in-a-subdir/rnd1 # image has multiple layers, check with all-upper-case TEE type name - run_buildah mkcw --ignore-attestation-errors --type SNP --passphrase=mkcw-convert bash bash-cw - mkcw_check_image bash-cw + run_buildah mkcw --ignore-attestation-errors --type SNP --passphrase=mkcw-convert --add-file randomfile2:rnd2 bash bash-cw + mkcw_check_image bash-cw "" randomfile2:/rnd2 } @test "mkcw-commit" {