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:
parent
e676f85117
commit
89f50af211
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
4
image.go
4
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 {
|
||||
|
@ -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)
|
||||
|
@ -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" {
|
||||
|
Loading…
x
Reference in New Issue
Block a user