File: load.go

package info (click to toggle)
golang-github-containers-image 5.34.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 5,184 kB
  • sloc: sh: 194; makefile: 83
file content (210 lines) | stat: -rw-r--r-- 7,626 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
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() error { // A scope for defer
		f, err := os.Create(squashFSPath)
		if err != nil {
			return err
		}
		defer f.Close()
		// 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
}