Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255package microvm
import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strings")
const imageSpecFileName = "spec.json"
type RunnerConfig struct { CPU string `json:"cpu,omitempty"` Machine string `json:"machine,omitempty"` Console string `json:"console,omitempty"` ExtraArgs []string `json:"extraArgs,omitempty"`}
type ImageSpec struct { Arch string `json:"arch"` BootArgs string `json:"bootArgs"` Initrd string `json:"initrd"` Kernel string `json:"kernel"` RunnerType string `json:"runnerType"` RunnerConfig RunnerConfig `json:"runnerConfig"` MemoryMiB int `json:"memoryMiB"` NetworkInterfaces []NetworkInterface `json:"networkInterfaces"` StoreDisk string `json:"storeDisk"` StoreDiskType string `json:"storeDiskType"` // baseConfigHash identifies the base nixos configuration baked into the // image. its only for nixos images as other images won't have a system // to rebuild. BaseConfigHash string `json:"baseConfigHash,omitempty"` // shell is the login shell used to run workflow step commands in the guest. Shell string `json:"shell"` VCPUs int `json:"vcpus"` Volumes []Volume `json:"volumes"`}
func (s ImageSpec) SupportsConfigActivation() bool { return s.BaseConfigHash != ""}
type NetworkInterface struct { Type string `json:"type"` ID string `json:"id"` MAC string `json:"mac"`}
type Volume struct { FSType string `json:"fsType"` Image string `json:"image"` ImageType string `json:"imageType"` MountPoint string `json:"mountPoint"` ReadOnly bool `json:"readOnly"` SizeMiB int64 `json:"sizeMiB"`}
func LoadImageSpec(path string) (ImageSpec, error) { data, err := os.ReadFile(path) if err != nil { return ImageSpec{}, fmt.Errorf("read microvm image spec: %w", err) }
var spec ImageSpec if err := json.Unmarshal(data, &spec); err != nil { return ImageSpec{}, fmt.Errorf("parse microvm image spec: %w", err) }
base := filepath.Dir(path) spec.Kernel = resolveImageSpecPath(base, spec.Kernel) spec.Initrd = resolveImageSpecPath(base, spec.Initrd) spec.StoreDisk = resolveImageSpecPath(base, spec.StoreDisk)
if err := spec.Validate(); err != nil { return ImageSpec{}, err } return spec, nil}
func (s ImageSpec) Validate() error { if s.Kernel == "" { return fmt.Errorf("microvm image spec missing kernel") } if s.Initrd == "" { return fmt.Errorf("microvm image spec missing initrd") } if s.StoreDisk == "" { return fmt.Errorf("microvm image spec missing storeDisk") } if s.BootArgs == "" { return fmt.Errorf("microvm image spec missing bootArgs") } if s.Shell == "" { return fmt.Errorf("microvm image spec missing shell") } if s.RunnerType == "qemu" || s.RunnerType == "" { if s.RunnerConfig.Machine == "" { return fmt.Errorf("microvm image spec missing runnerConfig.machine for qemu runner") } } if s.MemoryMiB <= 0 { return fmt.Errorf("microvm image spec memoryMiB must be positive") } if s.VCPUs <= 0 { return fmt.Errorf("microvm image spec vcpus must be positive") } for _, networkInterface := range s.NetworkInterfaces { if networkInterface.Type == "" { return fmt.Errorf("microvm image spec network interface missing type") } if networkInterface.ID == "" { return fmt.Errorf("microvm image spec network interface missing id") } if networkInterface.MAC == "" { return fmt.Errorf("microvm image spec network interface %q missing mac", networkInterface.ID) } } for _, volume := range s.Volumes { if volume.Image == "" { return fmt.Errorf("microvm image spec volume missing image") } if volume.FSType == "" { return fmt.Errorf("microvm image spec volume %q missing fsType", volume.Image) } if volume.SizeMiB <= 0 { return fmt.Errorf("microvm image spec volume %q sizeMiB must be positive", volume.Image) } } return nil}
func (s ImageSpec) RunnerCmd() string { switch s.RunnerType { case "qemu", "": return "qemu-system-" + s.Arch case "firecracker": return "firecracker" default: return "" }}
// also see Runner.Validate for where Runner specific files are validatedfunc (s ImageSpec) validateImageFiles() error { required := map[string]string{ "kernel": s.Kernel, "initrd": s.Initrd, "storeDisk": s.StoreDisk, } for name, path := range required { if !filepath.IsAbs(path) { continue } if _, err := os.Stat(path); err != nil { return fmt.Errorf("required image spec file %s not found at %q: %w", name, path, err) } }
return nil}
func resolveImageSpecPath(base, path string) string { if path == "" || filepath.IsAbs(path) { return path } return filepath.Join(base, path)}
func (e *Engine) resolveImage(name string) (ImageSpec, string, string, error) { name = strings.TrimSpace(name) if name == "" { name = strings.TrimSpace(e.cfg.MicroVMPipelines.DefaultImage) } if name == "" { return ImageSpec{}, "", "", fmt.Errorf("no image specified in workflow and SPINDLE_MICROVM_PIPELINES_DEFAULT_IMAGE is not set") } if !isPlainImageName(name) { return ImageSpec{}, "", "", fmt.Errorf("invalid microVM image name %q: must be a plain name, not a path", name) }
candidates := imageCandidates(e.cfg.MicroVMPipelines.ImageDir, name) for _, candidate := range candidates { path, ok, err := imageSpecPath(candidate) if err != nil { return ImageSpec{}, "", "", err } if !ok { continue } imageSpec, err := LoadImageSpec(path) if err != nil { return ImageSpec{}, "", "", err } return imageSpec, path, name, nil }
return ImageSpec{}, "", "", fmt.Errorf("microVM image %q was not found; looked in: %s", name, strings.Join(candidates, ", "))}
// check if image name is not a pathfunc isPlainImageName(name string) bool { if name == "" || name == "." || name == ".." { return false } if filepath.IsAbs(name) || strings.ContainsRune(name, '/') || strings.ContainsRune(name, filepath.Separator) { return false } return true}
// returns candidates, which is either a directory or spec file itselffunc imageCandidates(imageDir, name string) []string { if imageDir == "" { return nil } return []string{ filepath.Join(imageDir, name), filepath.Join(imageDir, name+".json"), }}
// resolve the candidate to a spec:// - first check if its a file, if yes, return// - otherwise assume its a directory and check and return `/spec.json`func imageSpecPath(candidate string) (string, bool, error) { info, err := os.Stat(candidate) if err != nil { if errors.Is(err, os.ErrNotExist) { return "", false, nil } return "", false, err } if !info.IsDir() { return candidate, true, nil }
spec := filepath.Join(candidate, imageSpecFileName) info, err = os.Stat(spec) if err != nil { if errors.Is(err, os.ErrNotExist) { return "", false, fmt.Errorf("microVM image directory %q does not contain %s", candidate, imageSpecFileName) } return "", false, err } // this only happens if there is a directory named `spec.json` which would be very silly. // but better output an error for it anyway :p if info.IsDir() { return "", false, fmt.Errorf("microVM image spec %q is a directory", spec) } return spec, true, nil}