-
Notifications
You must be signed in to change notification settings - Fork 394
Initial sif transport implementation #1438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
13f7888
sif: initial sif transport implementation
yhcote 9b33dd1
sif: bring code in
yhcote 1757663
sif: limit platform to linux
yhcote a7517b4
sif: satisfy linter
yhcote 8f3f546
sif: use upstream sif module
44ef87d
Re-update golang.org/x/crypto
mtrmac 67c18a6
Update build directives
mtrmac 04e1b8d
Rename sif/* files
mtrmac eb8254b
Make the sif/load.go code package-private
mtrmac 71dfda1
Allow building the SIF transport on non-Linux systems
mtrmac a281e36
Extensive refactoring to address review comments and hopefully simplify
mtrmac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| package sif | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "io/ioutil" | ||
| "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 := ioutil.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 := ioutil.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 contructing 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 ioutil.Temp*, 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 compresed 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package sif | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestParseDefFile(t *testing.T) { | ||
| for _, c := range []struct { | ||
| name string | ||
| input string | ||
| environment []string | ||
| runscript []string | ||
| }{ | ||
| {"Empty input", "", []string{}, []string{}}, | ||
| { | ||
| name: "Basic smoke test", | ||
| input: "Bootstrap: library\n" + | ||
| "%environment\n" + | ||
| " export FOO=world\n" + | ||
| " export BAR=baz\n" + | ||
| "%runscript\n" + | ||
| ` echo "Hello $FOO"` + "\n" + | ||
| " sleep 5\n" + | ||
| "%help\n" + | ||
| " Abandon all hope.\n", | ||
| environment: []string{"export FOO=world", "export BAR=baz"}, | ||
| runscript: []string{`echo "Hello $FOO"`, "sleep 5"}, | ||
| }, | ||
| { | ||
| name: "Trailing section marker", | ||
| input: "Bootstrap: library\n" + | ||
| "%environment\n" + | ||
| " export FOO=world\n" + | ||
| "%runscript", | ||
| environment: []string{"export FOO=world"}, | ||
| runscript: []string{}, | ||
| }, | ||
| } { | ||
| env, rs, err := parseDefFile(bytes.NewReader([]byte(c.input))) | ||
| require.NoError(t, err, c.name) | ||
| assert.Equal(t, c.environment, env, c.name) | ||
| assert.Equal(t, c.runscript, rs, c.name) | ||
| } | ||
| } | ||
|
|
||
| func TestGenerateInjectedScript(t *testing.T) { | ||
| res := generateInjectedScript([]string{"export FOO=world", "export BAR=baz"}, | ||
| []string{`echo "Hello $FOO"`, "sleep 5"}) | ||
| assert.Equal(t, "#!/bin/bash\n"+ | ||
| "export FOO=world\n"+ | ||
| "export BAR=baz\n"+ | ||
| `echo "Hello $FOO"`+"\n"+ | ||
| "sleep 5\n", string(res)) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function can take a lot of time. What I would love to have here is a progress-bar spinner to indicate that we're doing some very heavy lifting.
Unfinished thought: I wonder if we could add an io.Writer to
types.SystemContextsimilar as incopy.Options.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, a progress indication would definitely be useful. It’s not reasonably possible in the current API (and several other transports that e.g. make temporary files on disk would benefit). We now have
internal/types, so it’s something that can be built (at least forc/image/copy, where we don’t have to worry whether to expose MBP as a public API commitment).Just an
io.Writerwould not be great, this needs to account for interactive use (frequent progress updates), completely non-interactive use (no progress updates, just a log of successes), and (per a RFE) a middle ground of plain-text updates a few times a minute. We might need to build an internal progress abstraction, and I don’t think we can commit to a publicio.WriterAPI intypes.SystemContext, just like that, at this point.Given the Podman 4.0 timing, would it be sufficient to just wrap that operation in a pair of debug logs?