Skip to content

Commit

Permalink
vm: add support for custom disk images (#1216)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
abiosoft authored Dec 16, 2024
1 parent f829db1 commit da72ca3
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 53 deletions.
1 change: 1 addition & 0 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
7 changes: 7 additions & 0 deletions config/configmanager/configmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/abiosoft/colima/cli"
"github.com/abiosoft/colima/config"
Expand Down Expand Up @@ -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
}

Expand Down
8 changes: 8 additions & 0 deletions embedded/defaults/colima.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions environment/vm/lima/lima.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions environment/vm/lima/limautil/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 1 addition & 53 deletions util/downloader/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"
"path"
"path/filepath"
"strconv"
"strings"

"github.com/abiosoft/colima/config"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions util/downloader/sha.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit da72ca3

Please sign in to comment.