Skip to content

Commit 56da2d6

Browse files
authored
Merge pull request #41 from AkihiroSuda/dev
add HTTP cache (~/Library/Caches/lima)
2 parents 98e0f62 + 184ce42 commit 56da2d6

File tree

9 files changed

+295
-28
lines changed

9 files changed

+295
-28
lines changed

cmd/limactl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func newApp() *cli.App {
4646
shellCommand,
4747
lsCommand,
4848
deleteCommand,
49+
pruneCommand,
4950
completionCommand,
5051
}
5152
return app

cmd/limactl/prune.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/sirupsen/logrus"
8+
"github.com/urfave/cli/v2"
9+
)
10+
11+
var pruneCommand = &cli.Command{
12+
Name: "prune",
13+
Usage: "Prune garbage objects",
14+
Action: pruneAction,
15+
}
16+
17+
func pruneAction(clicontext *cli.Context) error {
18+
ucd, err := os.UserCacheDir()
19+
if err != nil {
20+
return err
21+
}
22+
cacheDir := filepath.Join(ucd, "lima")
23+
logrus.Infof("Pruning %q", cacheDir)
24+
return os.RemoveAll(cacheDir)
25+
}

docs/internal.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ An instance directory contains the following files:
1313
- `ga.sock`: Forwarded to `/run/user/$UID/lima-guestagent.sock`
1414
- `serial.log`: QEMU serial log, for debugging
1515
- `serial.sock`: QEMU serial socket, for debugging (Usage: `socat -,echo=0,icanon=0 unix-connect:serial.sock`)
16+
17+
18+
## Cache directory (`~/Library/Caches/lima/download/by-url-sha256/<SHA256_OF_URL>`)
19+
20+
The directory contains the following files:
21+
22+
- `url`: raw url text, without "\n"
23+
- `data`: data

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/AkihiroSuda/sshocker v0.1.1-0.20210510144941-56aa3c7472b0
77
github.com/alessio/shellescape v1.4.1
88
github.com/containerd/containerd v1.5.0
9+
github.com/containerd/continuity v0.1.0
910
github.com/diskfs/go-diskfs v1.1.2-0.20210512141858-8a6b8b88d14a
1011
github.com/docker/go-units v0.4.0
1112
github.com/gorilla/mux v1.8.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL
164164
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
165165
github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
166166
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
167+
github.com/containerd/continuity v0.1.0 h1:UFRRY5JemiAhPZrr/uE0n8fMTLcZsUvySPr1+D7pgr8=
167168
github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
168169
github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
169170
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
@@ -524,6 +525,7 @@ github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1
524525
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
525526
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
526527
github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
528+
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
527529
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
528530
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
529531
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
@@ -812,6 +814,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
812814
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
813815
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
814816
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
817+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
815818
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
816819
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
817820
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

pkg/cidata/cidata.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@ package cidata
22

33
import (
44
"bytes"
5+
"fmt"
56
"io"
67
"io/fs"
8+
"io/ioutil"
79
"os"
810
"os/user"
911
"path/filepath"
1012
"strconv"
1113

14+
"github.com/AkihiroSuda/lima/pkg/downloader"
1215
"github.com/AkihiroSuda/lima/pkg/iso9660util"
1316
"github.com/AkihiroSuda/lima/pkg/limayaml"
1417
"github.com/AkihiroSuda/lima/pkg/localpathutil"
1518
"github.com/AkihiroSuda/lima/pkg/sshutil"
1619
"github.com/pkg/errors"
20+
"github.com/sirupsen/logrus"
1721
)
1822

23+
const NerdctlVersion = "0.8.3"
24+
1925
func GenerateISO9660(isoPath, name string, y *limayaml.LimaYAML) error {
2026
if err := limayaml.ValidateRaw(*y); err != nil {
2127
return err
@@ -81,6 +87,50 @@ func GenerateISO9660(isoPath, name string, y *limayaml.LimaYAML) error {
8187
})
8288
}
8389

90+
if args.Containerd.System || args.Containerd.User {
91+
var nftgzBase string
92+
switch y.Arch {
93+
case limayaml.X8664:
94+
nftgzBase = fmt.Sprintf("nerdctl-full-%s-linux-amd64.tar.gz", NerdctlVersion)
95+
case limayaml.AARCH64:
96+
nftgzBase = fmt.Sprintf("nerdctl-full-%s-linux-arm64.tar.gz", NerdctlVersion)
97+
default:
98+
return errors.Errorf("unexpected arch %q", y.Arch)
99+
}
100+
td, err := ioutil.TempDir("", "lima-download-nerdctl")
101+
if err != nil {
102+
return err
103+
}
104+
defer os.RemoveAll(td)
105+
nftgzLocal := filepath.Join(td, nftgzBase)
106+
nftgzURL := fmt.Sprintf("https://github.com/containerd/nerdctl/releases/download/v%s/%s",
107+
NerdctlVersion, nftgzBase)
108+
logrus.Infof("Downloading %q", nftgzURL)
109+
res, err := downloader.Download(nftgzLocal, nftgzURL, downloader.WithCache())
110+
if err != nil {
111+
return errors.Wrapf(err, "failed to download %q", nftgzURL)
112+
}
113+
switch res.Status {
114+
case downloader.StatusDownloaded:
115+
logrus.Infof("Downloaded %q", nftgzBase)
116+
case downloader.StatusUsedCache:
117+
logrus.Infof("Using cache %q", res.CachePath)
118+
default:
119+
logrus.Warnf("Unexpected result from downloader.Download(): %+v", res)
120+
}
121+
// TODO: verify sha256
122+
nftgzR, err := os.Open(nftgzLocal)
123+
if err != nil {
124+
return err
125+
}
126+
defer nftgzR.Close()
127+
layout = append(layout, iso9660util.Entry{
128+
// ISO9660 requires len(Path) <= 30
129+
Path: "nerdctl-full.tgz",
130+
Reader: nftgzR,
131+
})
132+
}
133+
84134
return iso9660util.Write(isoPath, "cidata", layout)
85135
}
86136

pkg/cidata/user-data.TEMPLATE

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,10 @@ write_files:
101101
#!/bin/bash
102102
set -eux -o pipefail
103103
if [ ! -x /usr/local/bin/nerdctl ]; then
104-
version="0.8.3"
105-
goarch="amd64"
106-
if [ "$(uname -m )" = "aarch64" ]; then
107-
goarch="arm64"
108-
fi
109-
curl -fsSL https://github.com/containerd/nerdctl/releases/download/v${version}/nerdctl-full-${version}-linux-${goarch}.tar.gz | tar Cxz /usr/local
104+
mkdir -p -m 600 /mnt/lima-cidata
105+
mount -t iso9660 -o ro /dev/disk/by-label/cidata /mnt/lima-cidata
106+
tar Cxzf /usr/local /mnt/lima-cidata/nerdctl-full.tgz
107+
umount /mnt/lima-cidata
110108
fi
111109
{{- if .Containerd.System}}
112110
cat >"/etc/containerd/config.toml" <<EOF

pkg/downloader/downloader.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package downloader
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/AkihiroSuda/lima/pkg/localpathutil"
12+
"github.com/containerd/continuity/fs"
13+
"github.com/pkg/errors"
14+
"github.com/sirupsen/logrus"
15+
)
16+
17+
type Status = string
18+
19+
const (
20+
StatusUnknown Status = ""
21+
StatusDownloaded Status = "downloaded"
22+
StatusSkipped Status = "skipped"
23+
StatusUsedCache Status = "used-cache"
24+
)
25+
26+
type Result struct {
27+
Status Status
28+
CachePath string // "/Users/foo/Library/Caches/lima/download/by-url-sha256/<SHA256_OF_URL>/data"
29+
}
30+
31+
type options struct {
32+
cacheDir string // default: empty (disables caching)
33+
}
34+
35+
type Opt func(*options) error
36+
37+
// WithCache enables caching using filepath.Join(os.UserCacheDir(), "lima") as the cache dir.
38+
func WithCache() Opt {
39+
return func(o *options) error {
40+
ucd, err := os.UserCacheDir()
41+
if err != nil {
42+
return err
43+
}
44+
cacheDir := filepath.Join(ucd, "lima")
45+
return WithCacheDir(cacheDir)(o)
46+
}
47+
}
48+
49+
// WithCacheDir enables caching using the specified dir.
50+
// Empty value disables caching.
51+
func WithCacheDir(cacheDir string) Opt {
52+
return func(o *options) error {
53+
o.cacheDir = cacheDir
54+
return nil
55+
}
56+
}
57+
58+
func Download(local, remote string, opts ...Opt) (*Result, error) {
59+
var o options
60+
for _, f := range opts {
61+
if err := f(&o); err != nil {
62+
return nil, err
63+
}
64+
}
65+
localPath, err := localPath(local)
66+
if err != nil {
67+
return nil, err
68+
}
69+
if _, err := os.Stat(localPath); err == nil {
70+
logrus.Debugf("file %q already exists, skipping downloading from %q", localPath, remote)
71+
res := &Result{
72+
Status: StatusSkipped,
73+
}
74+
return res, nil
75+
} else if !errors.Is(err, os.ErrNotExist) {
76+
return nil, err
77+
}
78+
79+
localPathDir := filepath.Dir(localPath)
80+
if err := os.MkdirAll(localPathDir, 0755); err != nil {
81+
return nil, err
82+
}
83+
84+
if isLocal(remote) {
85+
if err := copyLocal(localPath, remote); err != nil {
86+
return nil, err
87+
}
88+
res := &Result{
89+
Status: StatusDownloaded,
90+
}
91+
return res, nil
92+
}
93+
94+
if o.cacheDir == "" {
95+
if err := downloadHTTP(localPath, remote); err != nil {
96+
return nil, err
97+
}
98+
res := &Result{
99+
Status: StatusDownloaded,
100+
}
101+
return res, nil
102+
}
103+
104+
shad := filepath.Join(o.cacheDir, "download", "by-url-sha256", fmt.Sprintf("%x", sha256.Sum256([]byte(remote))))
105+
shadData := filepath.Join(shad, "data")
106+
if _, err := os.Stat(shadData); err == nil {
107+
logrus.Debugf("file %q is cached as %q", localPath, shadData)
108+
if err := copyLocal(localPath, shadData); err != nil {
109+
return nil, err
110+
}
111+
res := &Result{
112+
Status: StatusUsedCache,
113+
CachePath: shadData,
114+
}
115+
return res, nil
116+
}
117+
if err := os.RemoveAll(shad); err != nil {
118+
return nil, err
119+
}
120+
if err := os.MkdirAll(shad, 0700); err != nil {
121+
return nil, err
122+
}
123+
shadURL := filepath.Join(shad, "url")
124+
if err := os.WriteFile(shadURL, []byte(remote), 0644); err != nil {
125+
return nil, err
126+
}
127+
if err := downloadHTTP(shadData, remote); err != nil {
128+
return nil, err
129+
}
130+
if err := copyLocal(localPath, shadData); err != nil {
131+
return nil, err
132+
}
133+
134+
res := &Result{
135+
Status: StatusDownloaded,
136+
CachePath: shadData,
137+
}
138+
return res, nil
139+
}
140+
141+
func isLocal(s string) bool {
142+
return !strings.Contains(s, "://") || strings.HasPrefix(s, "file://")
143+
}
144+
145+
func localPath(s string) (string, error) {
146+
if !isLocal(s) {
147+
return "", errors.Errorf("got non-local path: %q", s)
148+
}
149+
if strings.HasPrefix(s, "file://") {
150+
res := strings.TrimPrefix(s, "file://")
151+
if !filepath.IsAbs(res) {
152+
return "", errors.Errorf("got non-absolute path %q", res)
153+
}
154+
return res, nil
155+
}
156+
return localpathutil.Expand(s)
157+
}
158+
159+
func copyLocal(dst, src string) error {
160+
srcPath, err := localPath(src)
161+
if err != nil {
162+
return err
163+
}
164+
dstPath, err := localPath(dst)
165+
if err != nil {
166+
return err
167+
}
168+
return fs.CopyFile(dstPath, srcPath)
169+
}
170+
171+
func downloadHTTP(localPath, url string) error {
172+
logrus.Debugf("downloading %q into %q", url, localPath)
173+
localPathTmp := localPath + ".tmp"
174+
if err := os.RemoveAll(localPathTmp); err != nil {
175+
return err
176+
}
177+
// use curl for printing progress
178+
cmd := exec.Command("curl", "-fSL", "-o", localPathTmp, url)
179+
cmd.Stdout = os.Stdout
180+
cmd.Stderr = os.Stderr
181+
if err := cmd.Run(); err != nil {
182+
return errors.Wrapf(err, "failed to run %v", cmd.Args)
183+
}
184+
if err := os.RemoveAll(localPath); err != nil {
185+
return err
186+
}
187+
if err := os.Rename(localPathTmp, localPath); err != nil {
188+
return err
189+
}
190+
return nil
191+
}

0 commit comments

Comments
 (0)