diff --git a/cmd/start.go b/cmd/start.go index eac419e79..89b16145b 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -163,6 +163,7 @@ func init() { startCmd.Flags().StringVarP(&startCmdArgs.Arch, "arch", "a", defaultArch, "architecture (aarch64, x86_64)") startCmd.Flags().BoolVarP(&startCmdArgs.Flags.Foreground, "foreground", "f", false, "Keep colima in the foreground") startCmd.Flags().StringVar(&startCmdArgs.Hostname, "hostname", "", "custom hostname for the virtual machine") + startCmd.Flags().StringVarP(&startCmdArgs.DiskImage, "disk-image", "i", "", "file path to a custom disk image") // host IP addresses startCmd.Flags().BoolVar(&startCmdArgs.Network.HostAddresses, "network-host-addresses", false, "support port forwarding to specific host IP addresses") diff --git a/config/config.go b/config/config.go index 9c904b383..27ea4c164 100644 --- a/config/config.go +++ b/config/config.go @@ -44,6 +44,7 @@ type Config struct { VMType string `yaml:"vmType,omitempty"` VZRosetta bool `yaml:"rosetta,omitempty"` NestedVirtualization bool `yaml:"nestedVirtualization,omitempty"` + DiskImage string `yaml:"diskImage,omitempty"` // volume mounts Mounts []Mount `yaml:"mounts,omitempty"` diff --git a/config/configmanager/configmanager.go b/config/configmanager/configmanager.go index adc5c41f1..219400f7e 100644 --- a/config/configmanager/configmanager.go +++ b/config/configmanager/configmanager.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" @@ -77,6 +78,12 @@ func ValidateConfig(c config.Config) error { } } + if c.DiskImage != "" { + if strings.HasPrefix(c.DiskImage, "http://") || strings.HasPrefix(c.DiskImage, "https://") { + return fmt.Errorf("cannot use diskImage: remote URLs not supported, only local files can be specified") + } + } + return nil } diff --git a/embedded/defaults/colima.yaml b/embedded/defaults/colima.yaml index fec149807..aa7dc33d7 100644 --- a/embedded/defaults/colima.yaml +++ b/embedded/defaults/colima.yaml @@ -197,6 +197,14 @@ sshPort: 0 # Default: [] mounts: [] +# Specify a custom disk image for the virtual machine. +# When not specified, Colima downloads an appropriate disk image from Github at +# https://github.com/abiosoft/colima-core/releases. +# The file path to a custom disk image can be specified to override the behaviour. +# +# Default: "" +diskImage: "" + # Environment variables for the virtual machine. # # EXAMPLE diff --git a/environment/vm/lima/lima.go b/environment/vm/lima/lima.go index 645113e5b..abd5488ce 100644 --- a/environment/vm/lima/lima.go +++ b/environment/vm/lima/lima.go @@ -16,6 +16,7 @@ import ( "github.com/abiosoft/colima/environment/vm/lima/limaconfig" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/util" + "github.com/abiosoft/colima/util/downloader" "github.com/abiosoft/colima/util/osutil" "github.com/abiosoft/colima/util/yamlutil" "github.com/sirupsen/logrus" @@ -274,11 +275,34 @@ func (l limaVM) Arch() environment.Arch { func (l *limaVM) downloadDiskImage(ctx context.Context, conf config.Config) error { log := l.Logger(ctx) + // use a user specified disk image + if conf.DiskImage != "" { + if _, err := os.Stat(conf.DiskImage); err != nil { + return fmt.Errorf("invalid disk image: %w", err) + } + + image, err := limautil.Image(l.limaConf.Arch, conf.Runtime) + if err != nil { + return fmt.Errorf("error getting disk image details: %w", err) + } + + sha := downloader.SHA{Size: 512, Digest: image.Digest} + if err := sha.ValidateFile(l.host, conf.DiskImage); err != nil { + return fmt.Errorf("disk image must be downloaded from '%s', hash failure: %w", image.Location, err) + } + + image.Location = conf.DiskImage + l.limaConf.Images = []limaconfig.File{image} + return nil + } + + // use a previously cached image if image, ok := limautil.ImageCached(l.limaConf.Arch, conf.Runtime); ok { l.limaConf.Images = []limaconfig.File{image} return nil } + // download image log.Infoln("downloading disk image ...") image, err := limautil.DownloadImage(l.limaConf.Arch, conf.Runtime) if err != nil { diff --git a/environment/vm/lima/limautil/image.go b/environment/vm/lima/limautil/image.go index a877073f0..62b80582c 100644 --- a/environment/vm/lima/limautil/image.go +++ b/environment/vm/lima/limautil/image.go @@ -53,6 +53,11 @@ func findImage(arch environment.Arch, runtime string) (f limaconfig.File, err er return img, nil } +// Image returns the details of the disk image to download for the arch and runtime. +func Image(arch environment.Arch, runtime string) (limaconfig.File, error) { + return findImage(arch, runtime) +} + // DownloadImage downloads the image for arch and runtime. func DownloadImage(arch environment.Arch, runtime string) (f limaconfig.File, err error) { img, err := findImage(arch, runtime) diff --git a/util/downloader/download.go b/util/downloader/download.go index 59ea6997f..7a8eb9be9 100644 --- a/util/downloader/download.go +++ b/util/downloader/download.go @@ -5,7 +5,6 @@ import ( "os" "path" "path/filepath" - "strconv" "strings" "github.com/abiosoft/colima/config" @@ -19,57 +18,6 @@ type ( guestActions = environment.GuestActions ) -type SHA struct { - URL string // url to download the shasum file (if Digest is empty) - Size int // one of 256 or 512 - Digest string // shasum -} - -func (s SHA) validate(host hostActions, url, cacheFilename string) error { - if s.URL == "" && s.Digest == "" { - return fmt.Errorf("error validating SHA: one of Digest or URL must be set") - } - - if s.Digest != "" { - s.Digest = strings.TrimPrefix(s.Digest, fmt.Sprintf("sha%d:", s.Size)) - } - - filename := func() string { - if url == "" { - return "" - } - split := strings.Split(url, "/") - return split[len(split)-1] - }() - dir, cacheFilename := filepath.Split(cacheFilename) - - var script string - - if s.Digest == "" { - script = strings.NewReplacer( - "{dir}", dir, - "{url}", s.URL, - "{filename}", filename, - "{size}", strconv.Itoa(s.Size), - "{cache_filename}", cacheFilename, - ).Replace( - `cd {dir} && echo "$(curl -sL {url} | grep ' {filename}$' | awk -F' ' '{print $1}') {cache_filename}" | shasum -a {size} --check --status`, - ) - } else { - script = strings.NewReplacer( - "{dir}", dir, - "{digest}", s.Digest, - "{filename}", filename, - "{size}", strconv.Itoa(s.Size), - "{cache_filename}", cacheFilename, - ).Replace( - `cd {dir} && echo "{digest} {cache_filename}" | shasum -a {size} --check --status`, - ) - } - - return host.Run("sh", "-c", script) -} - // Request is download request type Request struct { URL string // request URL @@ -146,7 +94,7 @@ func (d downloader) downloadFile(r Request) (err error) { // validate download if sha is present if r.SHA != nil { - if err := r.SHA.validate(d.host, r.URL, cacheDownloadingFilename); err != nil { + if err := r.SHA.validateDownload(d.host, r.URL, cacheDownloadingFilename); err != nil { // move file to allow subsequent re-download // error discarded, would not be actioned anyways diff --git a/util/downloader/sha.go b/util/downloader/sha.go new file mode 100644 index 000000000..c1292f863 --- /dev/null +++ b/util/downloader/sha.go @@ -0,0 +1,72 @@ +package downloader + +import ( + "fmt" + "path/filepath" + "strconv" + "strings" +) + +// SHA is the shasum of a file. +type SHA struct { + Digest string // shasum + URL string // url to download the shasum file (if Digest is empty) + Size int // one of 256 or 512 +} + +// ValidateFile validates the SHA of the file. +func (s SHA) ValidateFile(host hostActions, file string) error { + dir, filename := filepath.Split(file) + digest := strings.TrimPrefix(s.Digest, fmt.Sprintf("sha%d:", s.Size)) + + script := strings.NewReplacer( + "{dir}", dir, + "{digest}", digest, + "{size}", strconv.Itoa(s.Size), + "{filename}", filename, + ).Replace( + `cd {dir} && echo "{digest} {filename}" | shasum -a {size} --check --status`, + ) + + return host.Run("sh", "-c", script) +} + +func (s SHA) validateDownload(host hostActions, url string, filename string) error { + if s.URL == "" && s.Digest == "" { + return fmt.Errorf("error validating SHA: one of Digest or URL must be set") + } + + // fetch digest from URL if empty + if s.Digest == "" { + // retrieve the filename from the download url. + filename := func() string { + if url == "" { + return "" + } + split := strings.Split(url, "/") + return split[len(split)-1] + }() + + digest, err := fetchSHAFromURL(host, s.URL, filename) + if err != nil { + return err + } + s.Digest = digest + } + + return s.ValidateFile(host, filename) +} + +func fetchSHAFromURL(host hostActions, url, filename string) (string, error) { + script := strings.NewReplacer( + "{url}", url, + "{filename}", filename, + ).Replace( + "curl -sL {url} | grep ' {filename}$' | awk -F' ' '{print $1}'", + ) + sha, err := host.RunOutput("sh", "-c", script) + if err != nil { + return "", fmt.Errorf("error retrieving sha from url '%s': %w", url, err) + } + return strings.TrimSpace(sha), nil +}