Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316//go:build linux
package microvm
import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strings"
"tangled.org/core/spindle/config" "tangled.org/core/spindle/engine")
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"`
// Selects the virtio transport to pass to QEMU, as `-virtio-DEVICE-TRANSPORT`. // One of: // device: The microvm machine's preferred transport, MMIO: // https://www.qemu.org/docs/master/system/i386/microvm.html // Requires the virtio-mmio kernel module in the guest. // pci: Use this if the guest kernel does not support MMIO (CONFIG_VIRTIO_MMIO). // Requires the virtio-pci kernel module in the guest. VirtioTransport string `json:"virtioTransport,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 spec.RunnerConfig.VirtioTransport == "" { spec.RunnerConfig.VirtioTransport = "device" }
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.RunnerConfig.VirtioTransport != "" && s.RunnerConfig.VirtioTransport != "device" && s.RunnerConfig.VirtioTransport != "pci" { return fmt.Errorf("microvm image spec virtioTransport unknown: %s", s.RunnerConfig.VirtioTransport) } } 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 ResolveImageSpec(pipelinesCfg config.MicroVMPipelines, name string) (ImageSpec, string, string, error) { explicitImage := strings.TrimSpace(name) != "" name = strings.TrimSpace(name) if name == "" { name = strings.TrimSpace(pipelinesCfg.DefaultImage) } if name == "" { return ImageSpec{}, "", "", engine.ClassifiedFailure( engine.FailureClassInfrastructure, engine.FailureReasonRuntimeFailed, fmt.Errorf("no image specified in workflow and SPINDLE_MICROVM_PIPELINES_DEFAULT_IMAGE is not set"), ) } if !isPlainImageName(name) { return ImageSpec{}, "", "", engine.ClassifiedFailure( engine.FailureClassUser, engine.FailureReasonConfigurationFailed, fmt.Errorf("invalid microVM image name %q: must be a plain name, not a path", name), ) }
imageDir := strings.TrimSpace(pipelinesCfg.ImageDir) if imageDir == "" { return ImageSpec{}, "", "", engine.ClassifiedFailure( engine.FailureClassInfrastructure, engine.FailureReasonRuntimeFailed, fmt.Errorf("microVM workflows require SPINDLE_MICROVM_PIPELINES_IMAGE_DIR"), ) }
candidates := imageCandidates(imageDir, name) for _, candidate := range candidates { path, ok, err := imageSpecPath(candidate) if err != nil { return ImageSpec{}, "", "", engine.ClassifiedFailure( engine.FailureClassInfrastructure, engine.FailureReasonRuntimeFailed, err, ) } if !ok { continue } imageSpec, err := LoadImageSpec(path) if err != nil { return ImageSpec{}, "", "", engine.ClassifiedFailure( engine.FailureClassInfrastructure, engine.FailureReasonRuntimeFailed, err, ) } return imageSpec, path, name, nil }
class := engine.FailureClassInfrastructure if explicitImage { class = engine.FailureClassUser } return ImageSpec{}, "", "", engine.ClassifiedFailure( class, engine.FailureReasonConfigurationFailed, fmt.Errorf("microVM image %q was not found; looked in: %s", name, strings.Join(candidates, ", ")), )}
func (e *Engine) resolveImage(name string) (ImageSpec, string, string, error) { return ResolveImageSpec(e.cfg.MicroVMPipelines, name)}
// 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() { candidate = filepath.Join(candidate, imageSpecFileName) info, err = os.Stat(candidate) if err != nil { if errors.Is(err, os.ErrNotExist) { return "", false, fmt.Errorf("microVM image directory %q does not contain %s", filepath.Dir(candidate), imageSpecFileName) } return "", false, err } if info.IsDir() { return "", false, fmt.Errorf("microVM image spec %q is a directory", candidate) } }
path, err := filepath.EvalSymlinks(candidate) if err != nil { return "", false, err } return path, true, nil}