mirror of
https://github.com/containers/image.git
synced 2025-04-18 19:44:05 +03:00
For objects (particularly files) that we write to, we need to check the return value of f.Close() and pass error back to the caller if this fails. Signed-off-by: Adam Eijdenberg <adam@continusec.com>
217 lines
7.6 KiB
Go
217 lines
7.6 KiB
Go
package sif
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/sylabs/sif/v2/pkg/sif"
|
|
)
|
|
|
|
// injectedScriptTargetPath is the path injectedScript should be written to in the created image.
|
|
const injectedScriptTargetPath = "/podman/runscript"
|
|
|
|
// parseDefFile parses a SIF definition file from reader,
|
|
// and returns non-trivial contents of the %environment and %runscript sections.
|
|
func parseDefFile(reader io.Reader) ([]string, []string, error) {
|
|
type parserState int
|
|
const (
|
|
parsingOther parserState = iota
|
|
parsingEnvironment
|
|
parsingRunscript
|
|
)
|
|
|
|
environment := []string{}
|
|
runscript := []string{}
|
|
|
|
state := parsingOther
|
|
scanner := bufio.NewScanner(reader)
|
|
for scanner.Scan() {
|
|
s := strings.TrimSpace(scanner.Text())
|
|
switch {
|
|
case s == `%environment`:
|
|
state = parsingEnvironment
|
|
case s == `%runscript`:
|
|
state = parsingRunscript
|
|
case strings.HasPrefix(s, "%"):
|
|
state = parsingOther
|
|
case state == parsingEnvironment:
|
|
if s != "" && !strings.HasPrefix(s, "#") {
|
|
environment = append(environment, s)
|
|
}
|
|
case state == parsingRunscript:
|
|
runscript = append(runscript, s)
|
|
default: // parsingOther: ignore the line
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, nil, fmt.Errorf("reading lines from SIF definition file object: %w", err)
|
|
}
|
|
return environment, runscript, nil
|
|
}
|
|
|
|
// generateInjectedScript generates a shell script based on
|
|
// SIF definition file %environment and %runscript data, and returns it.
|
|
func generateInjectedScript(environment []string, runscript []string) []byte {
|
|
script := fmt.Sprintf("#!/bin/bash\n"+
|
|
"%s\n"+
|
|
"%s\n", strings.Join(environment, "\n"), strings.Join(runscript, "\n"))
|
|
return []byte(script)
|
|
}
|
|
|
|
// processDefFile finds sif.DataDeffile in sifImage, if any,
|
|
// and returns:
|
|
// - the command to run
|
|
// - contents of a script to inject as injectedScriptTargetPath, or nil
|
|
func processDefFile(sifImage *sif.FileImage) (string, []byte, error) {
|
|
var environment, runscript []string
|
|
|
|
desc, err := sifImage.GetDescriptor(sif.WithDataType(sif.DataDeffile))
|
|
if err == nil {
|
|
environment, runscript, err = parseDefFile(desc.GetReader())
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
var command string
|
|
var injectedScript []byte
|
|
if len(environment) == 0 && len(runscript) == 0 {
|
|
command = "bash"
|
|
injectedScript = nil
|
|
} else {
|
|
injectedScript = generateInjectedScript(environment, runscript)
|
|
command = injectedScriptTargetPath
|
|
}
|
|
|
|
return command, injectedScript, nil
|
|
}
|
|
|
|
func writeInjectedScript(extractedRootPath string, injectedScript []byte) error {
|
|
if injectedScript == nil {
|
|
return nil
|
|
}
|
|
filePath := filepath.Join(extractedRootPath, injectedScriptTargetPath)
|
|
parentDirPath := filepath.Dir(filePath)
|
|
if err := os.MkdirAll(parentDirPath, 0755); err != nil {
|
|
return fmt.Errorf("creating %s: %w", parentDirPath, err)
|
|
}
|
|
if err := os.WriteFile(filePath, injectedScript, 0755); err != nil {
|
|
return fmt.Errorf("writing %s to %s: %w", injectedScriptTargetPath, filePath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createTarFromSIFInputs creates a tar file at tarPath, using a squashfs image at squashFSPath.
|
|
// It can also use extractedRootPath and scriptPath, which are allocated for its exclusive use,
|
|
// if necessary.
|
|
func createTarFromSIFInputs(ctx context.Context, tarPath, squashFSPath string, injectedScript []byte, extractedRootPath, scriptPath string) error {
|
|
// It's safe for the Remove calls to happen even before we create the files, because tempDir is exclusive
|
|
// for our use.
|
|
defer os.RemoveAll(extractedRootPath)
|
|
|
|
// Almost everything in extractedRootPath comes from squashFSPath.
|
|
conversionCommand := fmt.Sprintf("unsquashfs -d %s -f %s && tar --acls --xattrs -C %s -cpf %s ./",
|
|
extractedRootPath, squashFSPath, extractedRootPath, tarPath)
|
|
script := "#!/bin/sh\n" + conversionCommand + "\n"
|
|
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(scriptPath)
|
|
|
|
// On top of squashFSPath, we only add injectedScript, if necessary.
|
|
if err := writeInjectedScript(extractedRootPath, injectedScript); err != nil {
|
|
return err
|
|
}
|
|
|
|
logrus.Debugf("Converting squashfs to tar, command: %s ...", conversionCommand)
|
|
cmd := exec.CommandContext(ctx, "fakeroot", "--", scriptPath)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("converting image: %w, output: %s", err, string(output))
|
|
}
|
|
logrus.Debugf("... finished converting squashfs to tar")
|
|
return nil
|
|
}
|
|
|
|
// convertSIFToElements processes sifImage and creates/returns
|
|
// the relevant elements for constructing an OCI-like image:
|
|
// - A path to a tar file containing a root filesystem,
|
|
// - A command to run.
|
|
// The returned tar file path is inside tempDir, which can be assumed to be empty
|
|
// at start, and is exclusively used by the current process (i.e. it is safe
|
|
// to use hard-coded relative paths within it).
|
|
func convertSIFToElements(ctx context.Context, sifImage *sif.FileImage, tempDir string) (string, []string, error) {
|
|
// We could allocate unique names for all of these using os.{CreateTemp,MkdirTemp}, but tempDir is exclusive,
|
|
// so we can just hard-code a set of unique values here.
|
|
// We create and/or manage cleanup of these two paths.
|
|
squashFSPath := filepath.Join(tempDir, "rootfs.squashfs")
|
|
tarPath := filepath.Join(tempDir, "rootfs.tar")
|
|
// We only allocate these paths, the user is responsible for cleaning them up.
|
|
extractedRootPath := filepath.Join(tempDir, "rootfs")
|
|
scriptPath := filepath.Join(tempDir, "script")
|
|
|
|
succeeded := false
|
|
// It's safe for the Remove calls to happen even before we create the files, because tempDir is exclusive
|
|
// for our use.
|
|
// Ideally we would remove squashFSPath immediately after creating extractedRootPath, but we need
|
|
// to run both creation and consumption of extractedRootPath in the same fakeroot context.
|
|
// So, overall, this process requires at least 2 compressed copies (SIF and squashFSPath) and 2
|
|
// uncompressed copies (extractedRootPath and tarPath) of the data, all using up space at the same time.
|
|
// That's rather unsatisfactory, ideally we would be streaming the data directly from a squashfs parser
|
|
// reading from the SIF file to a tarball, for 1 compressed and 1 uncompressed copy.
|
|
defer os.Remove(squashFSPath)
|
|
defer func() {
|
|
if !succeeded {
|
|
os.Remove(tarPath)
|
|
}
|
|
}()
|
|
|
|
command, injectedScript, err := processDefFile(sifImage)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
rootFS, err := sifImage.GetDescriptor(sif.WithPartitionType(sif.PartPrimSys))
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("looking up rootfs from SIF file: %w", err)
|
|
}
|
|
// TODO: We'd prefer not to make a full copy of the file here; unsquashfs ≥ 4.4
|
|
// has an -o option that allows extracting a squashfs from the SIF file directly,
|
|
// but that version is not currently available in RHEL 8.
|
|
logrus.Debugf("Creating a temporary squashfs image %s ...", squashFSPath)
|
|
if err := func() (retErr error) { // A scope for defer
|
|
f, err := os.Create(squashFSPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// since we are writing to this file, make sure we handle err on Close()
|
|
defer func() {
|
|
closeErr := f.Close()
|
|
if retErr == nil {
|
|
retErr = closeErr
|
|
}
|
|
}()
|
|
// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
|
|
if _, err := io.CopyN(f, rootFS.GetReader(), rootFS.Size()); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}(); err != nil {
|
|
return "", nil, err
|
|
}
|
|
logrus.Debugf("... finished creating a temporary squashfs image")
|
|
|
|
if err := createTarFromSIFInputs(ctx, tarPath, squashFSPath, injectedScript, extractedRootPath, scriptPath); err != nil {
|
|
return "", nil, err
|
|
}
|
|
succeeded = true
|
|
return tarPath, []string{command}, nil
|
|
}
|