Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vm: add support for custom disk images #1216

Merged
merged 3 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading