Skip to content

Commit da72ca3

Browse files
authored
vm: add support for custom disk images (#1216)
* vm: add support for custom disk images Signed-off-by: Abiola Ibrahim <[email protected]> * chore: refactor sha validation Signed-off-by: Abiola Ibrahim <[email protected]> * vm: improve support for custom disk image Signed-off-by: Abiola Ibrahim <[email protected]> --------- Signed-off-by: Abiola Ibrahim <[email protected]>
1 parent f829db1 commit da72ca3

File tree

8 files changed

+119
-53
lines changed

8 files changed

+119
-53
lines changed

cmd/start.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ func init() {
163163
startCmd.Flags().StringVarP(&startCmdArgs.Arch, "arch", "a", defaultArch, "architecture (aarch64, x86_64)")
164164
startCmd.Flags().BoolVarP(&startCmdArgs.Flags.Foreground, "foreground", "f", false, "Keep colima in the foreground")
165165
startCmd.Flags().StringVar(&startCmdArgs.Hostname, "hostname", "", "custom hostname for the virtual machine")
166+
startCmd.Flags().StringVarP(&startCmdArgs.DiskImage, "disk-image", "i", "", "file path to a custom disk image")
166167

167168
// host IP addresses
168169
startCmd.Flags().BoolVar(&startCmdArgs.Network.HostAddresses, "network-host-addresses", false, "support port forwarding to specific host IP addresses")

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type Config struct {
4444
VMType string `yaml:"vmType,omitempty"`
4545
VZRosetta bool `yaml:"rosetta,omitempty"`
4646
NestedVirtualization bool `yaml:"nestedVirtualization,omitempty"`
47+
DiskImage string `yaml:"diskImage,omitempty"`
4748

4849
// volume mounts
4950
Mounts []Mount `yaml:"mounts,omitempty"`

config/configmanager/configmanager.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strings"
78

89
"github.com/abiosoft/colima/cli"
910
"github.com/abiosoft/colima/config"
@@ -77,6 +78,12 @@ func ValidateConfig(c config.Config) error {
7778
}
7879
}
7980

81+
if c.DiskImage != "" {
82+
if strings.HasPrefix(c.DiskImage, "http://") || strings.HasPrefix(c.DiskImage, "https://") {
83+
return fmt.Errorf("cannot use diskImage: remote URLs not supported, only local files can be specified")
84+
}
85+
}
86+
8087
return nil
8188
}
8289

embedded/defaults/colima.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,14 @@ sshPort: 0
197197
# Default: []
198198
mounts: []
199199

200+
# Specify a custom disk image for the virtual machine.
201+
# When not specified, Colima downloads an appropriate disk image from Github at
202+
# https://github.com/abiosoft/colima-core/releases.
203+
# The file path to a custom disk image can be specified to override the behaviour.
204+
#
205+
# Default: ""
206+
diskImage: ""
207+
200208
# Environment variables for the virtual machine.
201209
#
202210
# EXAMPLE

environment/vm/lima/lima.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/abiosoft/colima/environment/vm/lima/limaconfig"
1717
"github.com/abiosoft/colima/environment/vm/lima/limautil"
1818
"github.com/abiosoft/colima/util"
19+
"github.com/abiosoft/colima/util/downloader"
1920
"github.com/abiosoft/colima/util/osutil"
2021
"github.com/abiosoft/colima/util/yamlutil"
2122
"github.com/sirupsen/logrus"
@@ -274,11 +275,34 @@ func (l limaVM) Arch() environment.Arch {
274275
func (l *limaVM) downloadDiskImage(ctx context.Context, conf config.Config) error {
275276
log := l.Logger(ctx)
276277

278+
// use a user specified disk image
279+
if conf.DiskImage != "" {
280+
if _, err := os.Stat(conf.DiskImage); err != nil {
281+
return fmt.Errorf("invalid disk image: %w", err)
282+
}
283+
284+
image, err := limautil.Image(l.limaConf.Arch, conf.Runtime)
285+
if err != nil {
286+
return fmt.Errorf("error getting disk image details: %w", err)
287+
}
288+
289+
sha := downloader.SHA{Size: 512, Digest: image.Digest}
290+
if err := sha.ValidateFile(l.host, conf.DiskImage); err != nil {
291+
return fmt.Errorf("disk image must be downloaded from '%s', hash failure: %w", image.Location, err)
292+
}
293+
294+
image.Location = conf.DiskImage
295+
l.limaConf.Images = []limaconfig.File{image}
296+
return nil
297+
}
298+
299+
// use a previously cached image
277300
if image, ok := limautil.ImageCached(l.limaConf.Arch, conf.Runtime); ok {
278301
l.limaConf.Images = []limaconfig.File{image}
279302
return nil
280303
}
281304

305+
// download image
282306
log.Infoln("downloading disk image ...")
283307
image, err := limautil.DownloadImage(l.limaConf.Arch, conf.Runtime)
284308
if err != nil {

environment/vm/lima/limautil/image.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ func findImage(arch environment.Arch, runtime string) (f limaconfig.File, err er
5353
return img, nil
5454
}
5555

56+
// Image returns the details of the disk image to download for the arch and runtime.
57+
func Image(arch environment.Arch, runtime string) (limaconfig.File, error) {
58+
return findImage(arch, runtime)
59+
}
60+
5661
// DownloadImage downloads the image for arch and runtime.
5762
func DownloadImage(arch environment.Arch, runtime string) (f limaconfig.File, err error) {
5863
img, err := findImage(arch, runtime)

util/downloader/download.go

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"os"
66
"path"
77
"path/filepath"
8-
"strconv"
98
"strings"
109

1110
"github.com/abiosoft/colima/config"
@@ -19,57 +18,6 @@ type (
1918
guestActions = environment.GuestActions
2019
)
2120

22-
type SHA struct {
23-
URL string // url to download the shasum file (if Digest is empty)
24-
Size int // one of 256 or 512
25-
Digest string // shasum
26-
}
27-
28-
func (s SHA) validate(host hostActions, url, cacheFilename string) error {
29-
if s.URL == "" && s.Digest == "" {
30-
return fmt.Errorf("error validating SHA: one of Digest or URL must be set")
31-
}
32-
33-
if s.Digest != "" {
34-
s.Digest = strings.TrimPrefix(s.Digest, fmt.Sprintf("sha%d:", s.Size))
35-
}
36-
37-
filename := func() string {
38-
if url == "" {
39-
return ""
40-
}
41-
split := strings.Split(url, "/")
42-
return split[len(split)-1]
43-
}()
44-
dir, cacheFilename := filepath.Split(cacheFilename)
45-
46-
var script string
47-
48-
if s.Digest == "" {
49-
script = strings.NewReplacer(
50-
"{dir}", dir,
51-
"{url}", s.URL,
52-
"{filename}", filename,
53-
"{size}", strconv.Itoa(s.Size),
54-
"{cache_filename}", cacheFilename,
55-
).Replace(
56-
`cd {dir} && echo "$(curl -sL {url} | grep ' {filename}$' | awk -F' ' '{print $1}') {cache_filename}" | shasum -a {size} --check --status`,
57-
)
58-
} else {
59-
script = strings.NewReplacer(
60-
"{dir}", dir,
61-
"{digest}", s.Digest,
62-
"{filename}", filename,
63-
"{size}", strconv.Itoa(s.Size),
64-
"{cache_filename}", cacheFilename,
65-
).Replace(
66-
`cd {dir} && echo "{digest} {cache_filename}" | shasum -a {size} --check --status`,
67-
)
68-
}
69-
70-
return host.Run("sh", "-c", script)
71-
}
72-
7321
// Request is download request
7422
type Request struct {
7523
URL string // request URL
@@ -146,7 +94,7 @@ func (d downloader) downloadFile(r Request) (err error) {
14694

14795
// validate download if sha is present
14896
if r.SHA != nil {
149-
if err := r.SHA.validate(d.host, r.URL, cacheDownloadingFilename); err != nil {
97+
if err := r.SHA.validateDownload(d.host, r.URL, cacheDownloadingFilename); err != nil {
15098

15199
// move file to allow subsequent re-download
152100
// error discarded, would not be actioned anyways

util/downloader/sha.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package downloader
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// SHA is the shasum of a file.
11+
type SHA struct {
12+
Digest string // shasum
13+
URL string // url to download the shasum file (if Digest is empty)
14+
Size int // one of 256 or 512
15+
}
16+
17+
// ValidateFile validates the SHA of the file.
18+
func (s SHA) ValidateFile(host hostActions, file string) error {
19+
dir, filename := filepath.Split(file)
20+
digest := strings.TrimPrefix(s.Digest, fmt.Sprintf("sha%d:", s.Size))
21+
22+
script := strings.NewReplacer(
23+
"{dir}", dir,
24+
"{digest}", digest,
25+
"{size}", strconv.Itoa(s.Size),
26+
"{filename}", filename,
27+
).Replace(
28+
`cd {dir} && echo "{digest} {filename}" | shasum -a {size} --check --status`,
29+
)
30+
31+
return host.Run("sh", "-c", script)
32+
}
33+
34+
func (s SHA) validateDownload(host hostActions, url string, filename string) error {
35+
if s.URL == "" && s.Digest == "" {
36+
return fmt.Errorf("error validating SHA: one of Digest or URL must be set")
37+
}
38+
39+
// fetch digest from URL if empty
40+
if s.Digest == "" {
41+
// retrieve the filename from the download url.
42+
filename := func() string {
43+
if url == "" {
44+
return ""
45+
}
46+
split := strings.Split(url, "/")
47+
return split[len(split)-1]
48+
}()
49+
50+
digest, err := fetchSHAFromURL(host, s.URL, filename)
51+
if err != nil {
52+
return err
53+
}
54+
s.Digest = digest
55+
}
56+
57+
return s.ValidateFile(host, filename)
58+
}
59+
60+
func fetchSHAFromURL(host hostActions, url, filename string) (string, error) {
61+
script := strings.NewReplacer(
62+
"{url}", url,
63+
"{filename}", filename,
64+
).Replace(
65+
"curl -sL {url} | grep ' {filename}$' | awk -F' ' '{print $1}'",
66+
)
67+
sha, err := host.RunOutput("sh", "-c", script)
68+
if err != nil {
69+
return "", fmt.Errorf("error retrieving sha from url '%s': %w", url, err)
70+
}
71+
return strings.TrimSpace(sha), nil
72+
}

0 commit comments

Comments
 (0)