This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / engines / microvm / image.go
7.5 kB 262 lines
1//go:build linux 2 3package microvm 4 5import ( 6 "encoding/json" 7 "errors" 8 "fmt" 9 "os" 10 "path/filepath" 11 "strings" 12) 13 14const imageSpecFileName = "spec.json" 15 16type RunnerConfig struct { 17 CPU string `json:"cpu,omitempty"` 18 Machine string `json:"machine,omitempty"` 19 Console string `json:"console,omitempty"` 20 ExtraArgs []string `json:"extraArgs,omitempty"` 21} 22 23type ImageSpec struct { 24 Arch string `json:"arch"` 25 BootArgs string `json:"bootArgs"` 26 Initrd string `json:"initrd"` 27 Kernel string `json:"kernel"` 28 RunnerType string `json:"runnerType"` 29 RunnerConfig RunnerConfig `json:"runnerConfig"` 30 MemoryMiB int `json:"memoryMiB"` 31 NetworkInterfaces []NetworkInterface `json:"networkInterfaces"` 32 StoreDisk string `json:"storeDisk"` 33 StoreDiskType string `json:"storeDiskType"` 34 // baseConfigHash identifies the base nixos configuration baked into the 35 // image. its only for nixos images as other images won't have a system 36 // to rebuild. 37 BaseConfigHash string `json:"baseConfigHash,omitempty"` 38 // shell is the login shell used to run workflow step commands in the guest. 39 Shell string `json:"shell"` 40 VCPUs int `json:"vcpus"` 41 Volumes []Volume `json:"volumes"` 42} 43 44func (s ImageSpec) SupportsConfigActivation() bool { 45 return s.BaseConfigHash != "" 46} 47 48type NetworkInterface struct { 49 Type string `json:"type"` 50 ID string `json:"id"` 51 MAC string `json:"mac"` 52} 53 54type Volume struct { 55 FSType string `json:"fsType"` 56 Image string `json:"image"` 57 ImageType string `json:"imageType"` 58 MountPoint string `json:"mountPoint"` 59 ReadOnly bool `json:"readOnly"` 60 SizeMiB int64 `json:"sizeMiB"` 61} 62 63func LoadImageSpec(path string) (ImageSpec, error) { 64 data, err := os.ReadFile(path) 65 if err != nil { 66 return ImageSpec{}, fmt.Errorf("read microvm image spec: %w", err) 67 } 68 69 var spec ImageSpec 70 if err := json.Unmarshal(data, &spec); err != nil { 71 return ImageSpec{}, fmt.Errorf("parse microvm image spec: %w", err) 72 } 73 74 base := filepath.Dir(path) 75 spec.Kernel = resolveImageSpecPath(base, spec.Kernel) 76 spec.Initrd = resolveImageSpecPath(base, spec.Initrd) 77 spec.StoreDisk = resolveImageSpecPath(base, spec.StoreDisk) 78 79 if err := spec.Validate(); err != nil { 80 return ImageSpec{}, err 81 } 82 return spec, nil 83} 84 85func (s ImageSpec) Validate() error { 86 if s.Kernel == "" { 87 return fmt.Errorf("microvm image spec missing kernel") 88 } 89 if s.Initrd == "" { 90 return fmt.Errorf("microvm image spec missing initrd") 91 } 92 if s.StoreDisk == "" { 93 return fmt.Errorf("microvm image spec missing storeDisk") 94 } 95 if s.BootArgs == "" { 96 return fmt.Errorf("microvm image spec missing bootArgs") 97 } 98 if s.Shell == "" { 99 return fmt.Errorf("microvm image spec missing shell") 100 } 101 if s.RunnerType == "qemu" || s.RunnerType == "" { 102 if s.RunnerConfig.Machine == "" { 103 return fmt.Errorf("microvm image spec missing runnerConfig.machine for qemu runner") 104 } 105 } 106 if s.MemoryMiB <= 0 { 107 return fmt.Errorf("microvm image spec memoryMiB must be positive") 108 } 109 if s.VCPUs <= 0 { 110 return fmt.Errorf("microvm image spec vcpus must be positive") 111 } 112 for _, networkInterface := range s.NetworkInterfaces { 113 if networkInterface.Type == "" { 114 return fmt.Errorf("microvm image spec network interface missing type") 115 } 116 if networkInterface.ID == "" { 117 return fmt.Errorf("microvm image spec network interface missing id") 118 } 119 if networkInterface.MAC == "" { 120 return fmt.Errorf("microvm image spec network interface %q missing mac", networkInterface.ID) 121 } 122 } 123 for _, volume := range s.Volumes { 124 if volume.Image == "" { 125 return fmt.Errorf("microvm image spec volume missing image") 126 } 127 if volume.FSType == "" { 128 return fmt.Errorf("microvm image spec volume %q missing fsType", volume.Image) 129 } 130 if volume.SizeMiB <= 0 { 131 return fmt.Errorf("microvm image spec volume %q sizeMiB must be positive", volume.Image) 132 } 133 } 134 return nil 135} 136 137func (s ImageSpec) RunnerCmd() string { 138 switch s.RunnerType { 139 case "qemu", "": 140 return "qemu-system-" + s.Arch 141 case "firecracker": 142 return "firecracker" 143 default: 144 return "" 145 } 146} 147 148// also see Runner.Validate for where Runner specific files are validated 149func (s ImageSpec) validateImageFiles() error { 150 required := map[string]string{ 151 "kernel": s.Kernel, 152 "initrd": s.Initrd, 153 "storeDisk": s.StoreDisk, 154 } 155 for name, path := range required { 156 if !filepath.IsAbs(path) { 157 continue 158 } 159 if _, err := os.Stat(path); err != nil { 160 return fmt.Errorf("required image spec file %s not found at %q: %w", name, path, err) 161 } 162 } 163 164 return nil 165} 166 167func resolveImageSpecPath(base, path string) string { 168 if path == "" || filepath.IsAbs(path) { 169 return path 170 } 171 return filepath.Join(base, path) 172} 173 174func (e *Engine) resolveImage(name string) (ImageSpec, string, string, error) { 175 name = strings.TrimSpace(name) 176 if name == "" { 177 name = strings.TrimSpace(e.cfg.MicroVMPipelines.DefaultImage) 178 } 179 if name == "" { 180 return ImageSpec{}, "", "", fmt.Errorf("no image specified in workflow and SPINDLE_MICROVM_PIPELINES_DEFAULT_IMAGE is not set") 181 } 182 if !isPlainImageName(name) { 183 return ImageSpec{}, "", "", fmt.Errorf("invalid microVM image name %q: must be a plain name, not a path", name) 184 } 185 186 imageDir := strings.TrimSpace(e.cfg.MicroVMPipelines.ImageDir) 187 if imageDir == "" { 188 return ImageSpec{}, "", "", fmt.Errorf("microVM workflows require SPINDLE_MICROVM_PIPELINES_IMAGE_DIR") 189 } 190 191 candidates := imageCandidates(imageDir, name) 192 for _, candidate := range candidates { 193 path, ok, err := imageSpecPath(candidate) 194 if err != nil { 195 return ImageSpec{}, "", "", err 196 } 197 if !ok { 198 continue 199 } 200 imageSpec, err := LoadImageSpec(path) 201 if err != nil { 202 return ImageSpec{}, "", "", err 203 } 204 return imageSpec, path, name, nil 205 } 206 207 return ImageSpec{}, "", "", fmt.Errorf("microVM image %q was not found; looked in: %s", name, strings.Join(candidates, ", ")) 208} 209 210// check if image name is not a path 211func isPlainImageName(name string) bool { 212 if name == "" || name == "." || name == ".." { 213 return false 214 } 215 if filepath.IsAbs(name) || strings.ContainsRune(name, '/') || strings.ContainsRune(name, filepath.Separator) { 216 return false 217 } 218 return true 219} 220 221// returns candidates, which is either a directory or spec file itself 222func imageCandidates(imageDir, name string) []string { 223 if imageDir == "" { 224 return nil 225 } 226 return []string{ 227 filepath.Join(imageDir, name), 228 filepath.Join(imageDir, name+".json"), 229 } 230} 231 232// resolve the candidate to a spec: 233// - first check if its a file, if yes, return 234// - otherwise assume its a directory and check and return `/spec.json` 235func imageSpecPath(candidate string) (string, bool, error) { 236 info, err := os.Stat(candidate) 237 if err != nil { 238 if errors.Is(err, os.ErrNotExist) { 239 return "", false, nil 240 } 241 return "", false, err 242 } 243 if info.IsDir() { 244 candidate = filepath.Join(candidate, imageSpecFileName) 245 info, err = os.Stat(candidate) 246 if err != nil { 247 if errors.Is(err, os.ErrNotExist) { 248 return "", false, fmt.Errorf("microVM image directory %q does not contain %s", filepath.Dir(candidate), imageSpecFileName) 249 } 250 return "", false, err 251 } 252 if info.IsDir() { 253 return "", false, fmt.Errorf("microVM image spec %q is a directory", candidate) 254 } 255 } 256 257 path, err := filepath.EvalSymlinks(candidate) 258 if err != nil { 259 return "", false, err 260 } 261 return path, true, nil 262}