1
0
mirror of https://github.com/containers/buildah.git synced 2025-04-18 07:04:05 +03:00

internal/mkcw.Archive(): handle extra image content

When we have extra files to add to the image, handle them by adding them
to the upper overlay layer before creating the plaintext filesystem
image.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
Nalin Dahyabhai 2023-12-13 16:01:38 -05:00
parent e676f85117
commit 89f50af211
6 changed files with 90 additions and 20 deletions

View File

@ -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")

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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" {