Skip to content

Commit 44393d3

Browse files
committed
Support compressed image files with os/exec
Fork an external program, rather than linking everything. Keep some files compressed, such as the nerdctl-full tgz. Signed-off-by: Anders F Björklund <[email protected]>
1 parent 2cf4c26 commit 44393d3

File tree

6 files changed

+103
-11
lines changed

6 files changed

+103
-11
lines changed

pkg/downloader/downloader.go

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package downloader
22

33
import (
4+
"bytes"
45
"crypto/sha256"
56
"errors"
67
"fmt"
78
"io"
89
"net/http"
910
"os"
11+
"os/exec"
12+
"path"
1013
"path/filepath"
1114
"strings"
1215
"time"
@@ -38,6 +41,7 @@ type Result struct {
3841

3942
type options struct {
4043
cacheDir string // default: empty (disables caching)
44+
decompress bool // default: false (keep compression)
4145
description string // default: url
4246
expectedDigest digest.Digest
4347
}
@@ -73,6 +77,14 @@ func WithDescription(description string) Opt {
7377
}
7478
}
7579

80+
// WithDecompress decompress the download from the cache.
81+
func WithDecompress(decompress bool) Opt {
82+
return func(o *options) error {
83+
o.decompress = decompress
84+
return nil
85+
}
86+
}
87+
7688
// WithExpectedDigest is used to validate the downloaded file against the expected digest.
7789
//
7890
// The digest is not verified in the following cases:
@@ -142,8 +154,9 @@ func Download(local, remote string, opts ...Opt) (*Result, error) {
142154
}
143155
}
144156

157+
ext := path.Ext(remote)
145158
if IsLocal(remote) {
146-
if err := copyLocal(localPath, remote, o.description, o.expectedDigest); err != nil {
159+
if err := copyLocal(localPath, remote, ext, o.decompress, o.description, o.expectedDigest); err != nil {
147160
return nil, err
148161
}
149162
res := &Result{
@@ -183,11 +196,11 @@ func Download(local, remote string, opts ...Opt) (*Result, error) {
183196
if o.expectedDigest.String() != shadDigestS {
184197
return nil, fmt.Errorf("expected digest %q does not match the cached digest %q", o.expectedDigest.String(), shadDigestS)
185198
}
186-
if err := copyLocal(localPath, shadData, "", ""); err != nil {
199+
if err := copyLocal(localPath, shadData, ext, o.decompress, "", ""); err != nil {
187200
return nil, err
188201
}
189202
} else {
190-
if err := copyLocal(localPath, shadData, o.description, o.expectedDigest); err != nil {
203+
if err := copyLocal(localPath, shadData, ext, o.decompress, o.description, o.expectedDigest); err != nil {
191204
return nil, err
192205
}
193206
}
@@ -212,7 +225,7 @@ func Download(local, remote string, opts ...Opt) (*Result, error) {
212225
return nil, err
213226
}
214227
// no need to pass the digest to copyLocal(), as we already verified the digest
215-
if err := copyLocal(localPath, shadData, "", ""); err != nil {
228+
if err := copyLocal(localPath, shadData, ext, o.decompress, "", ""); err != nil {
216229
return nil, err
217230
}
218231
if shadDigest != "" && o.expectedDigest != "" {
@@ -253,7 +266,7 @@ func canonicalLocalPath(s string) (string, error) {
253266
return localpathutil.Expand(s)
254267
}
255268

256-
func copyLocal(dst, src string, description string, expectedDigest digest.Digest) error {
269+
func copyLocal(dst, src, ext string, decompress bool, description string, expectedDigest digest.Digest) error {
257270
srcPath, err := canonicalLocalPath(src)
258271
if err != nil {
259272
return err
@@ -274,9 +287,60 @@ func copyLocal(dst, src string, description string, expectedDigest digest.Digest
274287
if description != "" {
275288
// TODO: progress bar for copy
276289
}
290+
if _, ok := Decompressor(ext); ok && decompress {
291+
return decompressLocal(dstPath, srcPath, ext)
292+
}
277293
return fs.CopyFile(dstPath, srcPath)
278294
}
279295

296+
func Decompressor(ext string) ([]string, bool) {
297+
var program string
298+
switch ext {
299+
case ".gz":
300+
program = "gzip"
301+
case ".bz2":
302+
program = "bzip2"
303+
case ".xz":
304+
program = "xz"
305+
case ".zst":
306+
program = "zstd"
307+
default:
308+
return nil, false
309+
}
310+
// -d --decompress
311+
return []string{program, "-d"}, true
312+
}
313+
314+
func decompressLocal(dst, src, ext string) error {
315+
command, found := Decompressor(ext)
316+
if !found {
317+
return fmt.Errorf("decompressLocal: unknown extension %s", ext)
318+
}
319+
logrus.Infof("decompressing %s with %v", ext, command)
320+
in, err := os.Open(src)
321+
if err != nil {
322+
return err
323+
}
324+
defer in.Close()
325+
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0644)
326+
if err != nil {
327+
return err
328+
}
329+
defer out.Close()
330+
buf := new(bytes.Buffer)
331+
cmd := exec.Command(command[0], command[1:]...)
332+
cmd.Stdin = in
333+
cmd.Stdout = out
334+
cmd.Stderr = buf
335+
err = cmd.Run()
336+
if err != nil {
337+
if ee, ok := err.(*exec.ExitError); ok {
338+
ee.Stderr = buf.Bytes()
339+
}
340+
}
341+
return err
342+
}
343+
280344
func validateLocalFileDigest(localPath string, expectedDigest digest.Digest) error {
281345
if localPath == "" {
282346
return fmt.Errorf("validateLocalFileDigest: got empty localPath")

pkg/downloader/downloader_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package downloader
33
import (
44
"io/ioutil"
55
"os"
6+
"os/exec"
67
"path/filepath"
78
"runtime"
89
"testing"
@@ -130,3 +131,29 @@ func TestDownloadLocal(t *testing.T) {
130131
})
131132

132133
}
134+
135+
func TestDownloadCompressed(t *testing.T) {
136+
137+
if runtime.GOOS == "windows" {
138+
// FIXME: `assertion failed: error is not nil: exec: "gzip": executable file not found in %PATH%`
139+
t.Skip("Skipping on windows")
140+
}
141+
142+
t.Run("gzip", func(t *testing.T) {
143+
localPath := filepath.Join(t.TempDir(), t.Name())
144+
localFile := filepath.Join(t.TempDir(), "test-file")
145+
testDownloadCompressedContents := []byte("TestDownloadCompressed")
146+
ioutil.WriteFile(localFile, testDownloadCompressedContents, 0644)
147+
assert.NilError(t, exec.Command("gzip", localFile).Run())
148+
localFile += ".gz"
149+
testLocalFileURL := "file://" + localFile
150+
151+
r, err := Download(localPath, testLocalFileURL, WithDecompress(true))
152+
assert.NilError(t, err)
153+
assert.Equal(t, StatusDownloaded, r.Status)
154+
155+
got, err := os.ReadFile(localPath)
156+
assert.NilError(t, err)
157+
assert.Equal(t, string(got), string(testDownloadCompressedContents))
158+
})
159+
}

pkg/fileutils/download.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import (
1010
)
1111

1212
// DownloadFile downloads a file to the cache, optionally copying it to the destination. Returns path in cache.
13-
func DownloadFile(dest string, f limayaml.File, description string, expectedArch limayaml.Arch) (string, error) {
13+
func DownloadFile(dest string, f limayaml.File, decompress bool, description string, expectedArch limayaml.Arch) (string, error) {
1414
if f.Arch != expectedArch {
1515
return "", fmt.Errorf("unsupported arch: %q", f.Arch)
1616
}
1717
fields := logrus.Fields{"location": f.Location, "arch": f.Arch, "digest": f.Digest}
1818
logrus.WithFields(fields).Infof("Attempting to download %s", description)
1919
res, err := downloader.Download(dest, f.Location,
2020
downloader.WithCache(),
21+
downloader.WithDecompress(decompress),
2122
downloader.WithDescription(fmt.Sprintf("%s (%s)", description, path.Base(f.Location))),
2223
downloader.WithExpectedDigest(f.Digest),
2324
)

pkg/qemu/qemu.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ func EnsureDisk(cfg Config) error {
5050
var ensuredBaseDisk bool
5151
errs := make([]error, len(cfg.LimaYAML.Images))
5252
for i, f := range cfg.LimaYAML.Images {
53-
if _, err := fileutils.DownloadFile(baseDisk, f.File, "the image", *cfg.LimaYAML.Arch); err != nil {
53+
if _, err := fileutils.DownloadFile(baseDisk, f.File, true, "the image", *cfg.LimaYAML.Arch); err != nil {
5454
errs[i] = err
5555
continue
5656
}
5757
if f.Kernel != nil {
58-
if _, err := fileutils.DownloadFile(kernel, f.Kernel.File, "the kernel", *cfg.LimaYAML.Arch); err != nil {
58+
if _, err := fileutils.DownloadFile(kernel, f.Kernel.File, false, "the kernel", *cfg.LimaYAML.Arch); err != nil {
5959
errs[i] = err
6060
continue
6161
}
@@ -67,7 +67,7 @@ func EnsureDisk(cfg Config) error {
6767
}
6868
}
6969
if f.Initrd != nil {
70-
if _, err := fileutils.DownloadFile(initrd, *f.Initrd, "the initrd", *cfg.LimaYAML.Arch); err != nil {
70+
if _, err := fileutils.DownloadFile(initrd, *f.Initrd, false, "the initrd", *cfg.LimaYAML.Arch); err != nil {
7171
errs[i] = err
7272
continue
7373
}

pkg/start/start.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func ensureNerdctlArchiveCache(y *limayaml.LimaYAML) (string, error) {
3939

4040
errs := make([]error, len(y.Containerd.Archives))
4141
for i, f := range y.Containerd.Archives {
42-
path, err := fileutils.DownloadFile("", f, "the nerdctl archive", *y.Arch)
42+
path, err := fileutils.DownloadFile("", f, false, "the nerdctl archive", *y.Arch)
4343
if err != nil {
4444
errs[i] = err
4545
continue

pkg/vz/disk.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func EnsureDisk(driver *driver.BaseDriver) error {
2929
var ensuredBaseDisk bool
3030
errs := make([]error, len(driver.Yaml.Images))
3131
for i, f := range driver.Yaml.Images {
32-
if _, err := fileutils.DownloadFile(baseDisk, f.File, "the image", *driver.Yaml.Arch); err != nil {
32+
if _, err := fileutils.DownloadFile(baseDisk, f.File, true, "the image", *driver.Yaml.Arch); err != nil {
3333
errs[i] = err
3434
continue
3535
}

0 commit comments

Comments
 (0)