Monorepo for Tangled tangled.org
6

Configure Feed

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

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