github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/statemachine/state_machine.go (about) 1 // Package statemachine provides the functions and structs to set up and 2 // execute a state machine based ubuntu-image build 3 package statemachine 4 5 import ( 6 "crypto/rand" 7 "encoding/json" 8 "fmt" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "sort" 13 "strconv" 14 "strings" 15 "time" 16 17 diskfs "github.com/diskfs/go-diskfs" 18 "github.com/google/uuid" 19 "github.com/snapcore/snapd/gadget" 20 "github.com/snapcore/snapd/gadget/quantity" 21 "github.com/snapcore/snapd/image" 22 "github.com/snapcore/snapd/osutil" 23 "github.com/snapcore/snapd/osutil/mkfs" 24 "github.com/snapcore/snapd/seed" 25 "github.com/xeipuuv/gojsonschema" 26 27 "github.com/canonical/ubuntu-image/internal/commands" 28 "github.com/canonical/ubuntu-image/internal/helper" 29 ) 30 31 const ( 32 metadataStateFile = "ubuntu-image.json" 33 ) 34 35 var gadgetYamlPathInTree = filepath.Join("meta", "gadget.yaml") 36 37 // define some functions that can be mocked by test cases 38 var gadgetLayoutVolume = gadget.LayoutVolume 39 var gadgetNewMountedFilesystemWriter = gadget.NewMountedFilesystemWriter 40 var helperCopyBlob = helper.CopyBlob 41 var helperSetDefaults = helper.SetDefaults 42 var helperCheckEmptyFields = helper.CheckEmptyFields 43 var helperCheckTags = helper.CheckTags 44 var helperBackupAndCopyResolvConf = helper.BackupAndCopyResolvConf 45 var helperRestoreResolvConf = helper.RestoreResolvConf 46 var osReadDir = os.ReadDir 47 var osReadFile = os.ReadFile 48 var osWriteFile = os.WriteFile 49 var osMkdir = os.Mkdir 50 var osMkdirAll = os.MkdirAll 51 var osMkdirTemp = os.MkdirTemp 52 var osOpen = os.Open 53 var osOpenFile = os.OpenFile 54 var osRemoveAll = os.RemoveAll 55 var osRemove = os.Remove 56 var osRename = os.Rename 57 var osCreate = os.Create 58 var osTruncate = os.Truncate 59 var osutilCopyFile = osutil.CopyFile 60 var osutilCopySpecialFile = osutil.CopySpecialFile 61 var execCommand = exec.Command 62 var mkfsMakeWithContent = mkfs.MakeWithContent 63 var mkfsMake = mkfs.Make 64 var diskfsCreate = diskfs.Create 65 var randRead = rand.Read 66 var seedOpen = seed.Open 67 var imagePrepare = image.Prepare 68 var gojsonschemaValidate = gojsonschema.Validate 69 var filepathRel = filepath.Rel 70 71 // SmInterface allows different image types to implement their own setup/run/teardown functions 72 type SmInterface interface { 73 Setup() error 74 Run() error 75 Teardown() error 76 SetCommonOpts(commonOpts *commands.CommonOpts, stateMachineOpts *commands.StateMachineOpts) 77 } 78 79 // stateFunc allows us easy access to the function names, which will help with --resume and debug statements 80 type stateFunc struct { 81 name string 82 function func(*StateMachine) error 83 } 84 85 // temporaryDirectories organizes the state machines, rootfs, unpack, and volumes dirs 86 type temporaryDirectories struct { 87 rootfs string 88 unpack string 89 volumes string 90 chroot string 91 scratch string 92 } 93 94 // StateMachine will hold the command line data, track the current state, and handle all function calls 95 type StateMachine struct { 96 cleanWorkDir bool // whether or not to clean up the workDir 97 CurrentStep string // tracks the current progress of the state machine 98 StepsTaken int // counts the number of steps taken 99 ConfDefPath string // directory holding the model assertion / image definition file 100 YamlFilePath string // the location for the gadget yaml file 101 IsSeeded bool // core 20 images are seeded 102 RootfsVolName string // volume on which the rootfs is located 103 RootfsPartNum int // rootfs partition number 104 SectorSize quantity.Size // parsed (converted) sector size 105 RootfsSize quantity.Size 106 tempDirs temporaryDirectories 107 108 // The flags that were passed in on the command line 109 commonFlags *commands.CommonOpts 110 stateMachineFlags *commands.StateMachineOpts 111 112 states []stateFunc // the state functions 113 114 // used to access image type specific variables from state functions 115 parent SmInterface 116 117 // imported from snapd, the info parsed from gadget.yaml 118 GadgetInfo *gadget.Info 119 120 // image sizes for parsing the --image-size flags 121 ImageSizes map[string]quantity.Size 122 VolumeOrder []string 123 124 // names of images for each volume 125 VolumeNames map[string]string 126 127 Packages []string 128 Snaps []string 129 } 130 131 // SetCommonOpts stores the common options for all image types in the struct 132 func (stateMachine *StateMachine) SetCommonOpts(commonOpts *commands.CommonOpts, 133 stateMachineOpts *commands.StateMachineOpts) { 134 stateMachine.commonFlags = commonOpts 135 stateMachine.stateMachineFlags = stateMachineOpts 136 } 137 138 // parseImageSizes handles the flag --image-size, which is a string in the format 139 // <volumeName>:<volumeSize>,<volumeName2>:<volumeSize2>. It can also be in the 140 // format <volumeSize> to signify one size to rule them all 141 func (stateMachine *StateMachine) parseImageSizes() error { 142 stateMachine.ImageSizes = make(map[string]quantity.Size) 143 144 if stateMachine.commonFlags.Size == "" { 145 return nil 146 } 147 148 if stateMachine.hasSingleImageSizeValue() { 149 err := stateMachine.handleSingleImageSize() 150 if err != nil { 151 return err 152 } 153 } else { 154 err := stateMachine.handleMultipleImageSizes() 155 if err != nil { 156 return err 157 } 158 } 159 return nil 160 } 161 162 // hasSingleImageSizeValue determines if the provided --image-size flags contains 163 // a single value to use for every volumes or a list of values for some volumes 164 func (stateMachine *StateMachine) hasSingleImageSizeValue() bool { 165 return !strings.Contains(stateMachine.commonFlags.Size, ":") 166 } 167 168 // getSuggestedImageSize returns the suggested size for the given volume 169 func (stateMachine *StateMachine) getSuggestedImageSize(volumeName string) quantity.Size { 170 var parsedSize quantity.Size 171 if stateMachine.hasSingleImageSizeValue() { 172 // this scenario has just one size for each volume 173 // no need to check error as it has already been done by 174 // the parseImageSizes function 175 parsedSize, _ = quantity.ParseSize(stateMachine.commonFlags.Size) // nolint: errcheck 176 } else { 177 parsedSize = stateMachine.ImageSizes[volumeName] 178 } 179 return parsedSize 180 } 181 182 // handleSingleImageSize parses as a single value and applies the image size given in 183 // the flag --image-size 184 func (stateMachine *StateMachine) handleSingleImageSize() error { 185 parsedSize, err := quantity.ParseSize(stateMachine.commonFlags.Size) 186 if err != nil { 187 return fmt.Errorf("Failed to parse argument to --image-size: %s", err.Error()) 188 } 189 for volumeName := range stateMachine.GadgetInfo.Volumes { 190 stateMachine.ImageSizes[volumeName] = parsedSize 191 } 192 return nil 193 } 194 195 // handleMultipleImageSizes parses and applies the image size given in 196 // the flag --image-size in the format <volumeName>:<volumeSize>,<volumeName2>:<volumeSize2> 197 func (stateMachine *StateMachine) handleMultipleImageSizes() error { 198 allSizes := strings.Split(stateMachine.commonFlags.Size, ",") 199 for _, size := range allSizes { 200 // each of these should be of the form "<name|number>:<size>" 201 splitSize := strings.Split(size, ":") 202 if len(splitSize) != 2 { 203 return fmt.Errorf("Argument to --image-size %s is not "+ 204 "in the correct format", size) 205 } 206 parsedSize, err := quantity.ParseSize(splitSize[1]) 207 if err != nil { 208 return fmt.Errorf("Failed to parse argument to --image-size: %s", 209 err.Error()) 210 } 211 // the image size parsed successfully, now find which volume to associate it with 212 volumeNumber, err := strconv.Atoi(splitSize[0]) 213 if err == nil { 214 // argument passed was numeric. 215 if volumeNumber < len(stateMachine.VolumeOrder) { 216 stateName := stateMachine.VolumeOrder[volumeNumber] 217 stateMachine.ImageSizes[stateName] = parsedSize 218 } else { 219 return fmt.Errorf("Volume index %d is out of range", volumeNumber) 220 } 221 } else { 222 if _, found := stateMachine.GadgetInfo.Volumes[splitSize[0]]; !found { 223 return fmt.Errorf("Volume %s does not exist in gadget.yaml", 224 splitSize[0]) 225 } 226 stateMachine.ImageSizes[splitSize[0]] = parsedSize 227 } 228 } 229 230 return nil 231 } 232 233 // saveVolumeOrder records the order that the volumes appear in gadget.yaml. This is necessary 234 // to preserve backwards compatibility of the command line syntax --image-size <volume_number>:<size> 235 func (stateMachine *StateMachine) saveVolumeOrder(gadgetYamlContents string) { 236 indexMap := make(map[string]int) 237 for volumeName := range stateMachine.GadgetInfo.Volumes { 238 searchString := volumeName + ":" 239 index := strings.Index(gadgetYamlContents, searchString) 240 indexMap[volumeName] = index 241 } 242 243 // now sort based on the index 244 type volumeNameIndex struct { 245 VolumeName string 246 Index int 247 } 248 249 var sortable []volumeNameIndex 250 for volumeName, volumeIndex := range indexMap { 251 sortable = append(sortable, volumeNameIndex{volumeName, volumeIndex}) 252 } 253 254 sort.Slice(sortable, func(i, j int) bool { 255 return sortable[i].Index < sortable[j].Index 256 }) 257 258 var sortedVolumes []string 259 for _, volume := range sortable { 260 sortedVolumes = append(sortedVolumes, volume.VolumeName) 261 } 262 263 stateMachine.VolumeOrder = sortedVolumes 264 } 265 266 // postProcessGadgetYaml adds the rootfs to the partitions list if needed 267 func (stateMachine *StateMachine) postProcessGadgetYaml() error { 268 var rootfsSeen bool = false 269 var farthestOffset quantity.Offset 270 var lastOffset quantity.Offset 271 farthestOffsetUnknown := false 272 var volume *gadget.Volume 273 274 for _, volumeName := range stateMachine.VolumeOrder { 275 volume = stateMachine.GadgetInfo.Volumes[volumeName] 276 volumeBaseDir := filepath.Join(stateMachine.tempDirs.volumes, volumeName) 277 if err := osMkdirAll(volumeBaseDir, 0755); err != nil { 278 return fmt.Errorf("Error creating volume dir: %s", err.Error()) 279 } 280 // look for the rootfs and check if the image is seeded 281 for i := range volume.Structure { 282 structure := &volume.Structure[i] 283 stateMachine.warnUsageOfSystemLabel(volumeName, structure, i) 284 285 if structure.Role == gadget.SystemData { 286 rootfsSeen = true 287 } 288 289 stateMachine.checkSystemSeed(volume, structure, i) 290 291 err := checkStructureContent(structure) 292 if err != nil { 293 return err 294 } 295 296 err = stateMachine.handleRootfsScheme(structure, volume, i) 297 if err != nil { 298 return err 299 } 300 301 // update farthestOffset if needed 302 if structure.Offset == nil { 303 farthestOffsetUnknown = true 304 } else { 305 offset := *structure.Offset 306 lastOffset = offset + quantity.Offset(structure.Size) 307 farthestOffset = maxOffset(lastOffset, farthestOffset) 308 } 309 310 fixMissingContent(volume, structure, i) 311 } 312 } 313 314 fixMissingSystemData(volume, farthestOffset, farthestOffsetUnknown, rootfsSeen, stateMachine.GadgetInfo.Volumes) 315 316 return nil 317 } 318 319 func (stateMachine *StateMachine) warnUsageOfSystemLabel(volumeName string, structure *gadget.VolumeStructure, structIndex int) { 320 if structure.Role == "" && structure.Label == gadget.SystemBoot && !stateMachine.commonFlags.Quiet { 321 fmt.Printf("WARNING: volumes:%s:structure:%d:filesystem_label "+ 322 "used for defining partition roles; use role instead\n", 323 volumeName, structIndex) 324 } 325 } 326 327 // checkSystemSeed checks if the struture is a system-seed one and fixes the Label if needed 328 func (stateMachine *StateMachine) checkSystemSeed(volume *gadget.Volume, structure *gadget.VolumeStructure, structIndex int) { 329 if structure.Role == gadget.SystemSeed { 330 stateMachine.IsSeeded = true 331 if structure.Label == "" { 332 structure.Label = structure.Name 333 volume.Structure[structIndex] = *structure 334 } 335 } 336 } 337 338 // checkStructureContent makes sure there are no "../" paths in the structure's contents 339 func checkStructureContent(structure *gadget.VolumeStructure) error { 340 for _, content := range structure.Content { 341 if strings.Contains(content.UnresolvedSource, "../") { 342 return fmt.Errorf("filesystem content source \"%s\" contains \"../\". "+ 343 "This is disallowed for security purposes", 344 content.UnresolvedSource) 345 } 346 } 347 return nil 348 } 349 350 // handleRootfsScheme handles special syntax of rootfs:/<file path> in structure 351 // content. This is needed to allow images such as raspberry pi to source their 352 // kernel and initrd from the staged rootfs later in the build process. 353 func (stateMachine *StateMachine) handleRootfsScheme(structure *gadget.VolumeStructure, volume *gadget.Volume, structIndex int) error { 354 if structure.Role == gadget.SystemBoot || structure.Label == gadget.SystemBoot { 355 relativeRootfsPath, err := filepathRel( 356 filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 357 stateMachine.tempDirs.rootfs, 358 ) 359 if err != nil { 360 return fmt.Errorf("Error creating relative path from unpack/gadget to rootfs: \"%s\"", err.Error()) 361 } 362 for j, content := range structure.Content { 363 content.UnresolvedSource = strings.ReplaceAll(content.UnresolvedSource, 364 "rootfs:", 365 relativeRootfsPath, 366 ) 367 volume.Structure[structIndex].Content[j] = content 368 } 369 } 370 return nil 371 } 372 373 // fixMissingContent adds Content to system-data and system-seed. 374 // It may not be defined for these roles, so Content is a nil slice leading 375 // copyStructureContent() skip the rootfs copying later. 376 // So we need to make an empty slice here to avoid this situation. 377 func fixMissingContent(volume *gadget.Volume, structure *gadget.VolumeStructure, structIndex int) { 378 if (structure.Role == gadget.SystemData || structure.Role == gadget.SystemSeed) && structure.Content == nil { 379 structure.Content = make([]gadget.VolumeContent, 0) 380 } 381 382 volume.Structure[structIndex] = *structure 383 } 384 385 // fixMissingSystemData handles the case of unspecified system-data 386 // partition where we simply attach the rootfs at the end of the 387 // partition list. 388 // Since so far we have no knowledge of the rootfs contents, the 389 // size is set to 0, and will be calculated later 390 // Note that there is only one volume, so "volume" points to it 391 func fixMissingSystemData(volume *gadget.Volume, farthestOffset quantity.Offset, farthestOffsetUnknown bool, rootfsSeen bool, volumes map[string]*gadget.Volume) { 392 if !farthestOffsetUnknown && !rootfsSeen && len(volumes) == 1 { 393 rootfsStructure := gadget.VolumeStructure{ 394 Name: "", 395 Label: "writable", 396 Offset: &farthestOffset, 397 Size: quantity.Size(0), 398 Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", 399 Role: gadget.SystemData, 400 ID: "", 401 Filesystem: "ext4", 402 Content: []gadget.VolumeContent{}, 403 Update: gadget.VolumeUpdate{}, 404 // "virtual" yaml index for the new structure (it would 405 // be the last one in gadget.yaml) 406 YamlIndex: len(volume.Structure), 407 } 408 409 volume.Structure = append(volume.Structure, rootfsStructure) 410 } 411 } 412 413 // readMetadata reads info about a partial state machine encoded as JSON from disk 414 func (stateMachine *StateMachine) readMetadata(metadataFile string) error { 415 if !stateMachine.stateMachineFlags.Resume { 416 return nil 417 } 418 // open the ubuntu-image.json file and load the state 419 var partialStateMachine = &StateMachine{} 420 jsonfilePath := filepath.Join(stateMachine.stateMachineFlags.WorkDir, metadataFile) 421 jsonfile, err := os.ReadFile(jsonfilePath) 422 if err != nil { 423 return fmt.Errorf("error reading metadata file: %s", err.Error()) 424 } 425 426 err = json.Unmarshal(jsonfile, partialStateMachine) 427 if err != nil { 428 return fmt.Errorf("failed to parse metadata file: %s", err.Error()) 429 } 430 431 return stateMachine.loadState(partialStateMachine) 432 } 433 434 func (stateMachine *StateMachine) loadState(partialStateMachine *StateMachine) error { 435 stateMachine.StepsTaken = partialStateMachine.StepsTaken 436 437 if stateMachine.StepsTaken > len(stateMachine.states) { 438 return fmt.Errorf("invalid steps taken count (%d). The state machine only have %d steps", stateMachine.StepsTaken, len(stateMachine.states)) 439 } 440 441 // delete all of the stateFuncs that have already run 442 stateMachine.states = stateMachine.states[stateMachine.StepsTaken:] 443 444 stateMachine.CurrentStep = partialStateMachine.CurrentStep 445 stateMachine.YamlFilePath = partialStateMachine.YamlFilePath 446 stateMachine.IsSeeded = partialStateMachine.IsSeeded 447 stateMachine.RootfsVolName = partialStateMachine.RootfsVolName 448 stateMachine.RootfsPartNum = partialStateMachine.RootfsPartNum 449 450 stateMachine.SectorSize = partialStateMachine.SectorSize 451 stateMachine.RootfsSize = partialStateMachine.RootfsSize 452 453 stateMachine.tempDirs.rootfs = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "root") 454 stateMachine.tempDirs.unpack = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "unpack") 455 stateMachine.tempDirs.volumes = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "volumes") 456 stateMachine.tempDirs.chroot = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "chroot") 457 stateMachine.tempDirs.scratch = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "scratch") 458 459 stateMachine.GadgetInfo = partialStateMachine.GadgetInfo 460 stateMachine.ImageSizes = partialStateMachine.ImageSizes 461 stateMachine.VolumeOrder = partialStateMachine.VolumeOrder 462 stateMachine.VolumeNames = partialStateMachine.VolumeNames 463 464 stateMachine.Packages = partialStateMachine.Packages 465 stateMachine.Snaps = partialStateMachine.Snaps 466 467 if stateMachine.GadgetInfo != nil { 468 // Due to https://github.com/golang/go/issues/10415 we need to set back the volume 469 // structs we reset before encoding (see writeMetadata()) 470 gadget.SetEnclosingVolumeInStructs(stateMachine.GadgetInfo.Volumes) 471 472 rebuildYamlIndex(stateMachine.GadgetInfo) 473 } 474 475 return nil 476 } 477 478 // rebuildYamlIndex reset the YamlIndex field in VolumeStructure 479 // This field is not serialized (for a good reason) so it is lost when saving the metadata 480 // We consider here the JSON serialization keeps the struct order and we can naively 481 // consider the YamlIndex value is the same as the index of the structure in the structure slice. 482 func rebuildYamlIndex(info *gadget.Info) { 483 for _, v := range info.Volumes { 484 for i, s := range v.Structure { 485 s.YamlIndex = i 486 v.Structure[i] = s 487 } 488 } 489 } 490 491 // displayStates print the calculated states 492 func (s *StateMachine) displayStates() { 493 if !s.commonFlags.Debug && !s.commonFlags.DryRun { 494 return 495 } 496 497 verb := "will" 498 if s.commonFlags.DryRun { 499 verb = "would" 500 } 501 fmt.Printf("\nFollowing states %s be executed:\n", verb) 502 503 for i, state := range s.states { 504 if state.name == s.stateMachineFlags.Until { 505 break 506 } 507 fmt.Printf("[%d] %s\n", i, state.name) 508 509 if state.name == s.stateMachineFlags.Thru { 510 break 511 } 512 } 513 514 if s.commonFlags.DryRun { 515 return 516 } 517 fmt.Println("\nContinuing") 518 } 519 520 // writeMetadata writes the state machine info to disk, encoded as JSON. This will be used when resuming a 521 // partial state machine run 522 func (stateMachine *StateMachine) writeMetadata(metadataFile string) error { 523 jsonfilePath := filepath.Join(stateMachine.stateMachineFlags.WorkDir, metadataFile) 524 jsonfile, err := os.OpenFile(jsonfilePath, os.O_CREATE|os.O_WRONLY, 0644) 525 if err != nil && !os.IsExist(err) { 526 return fmt.Errorf("error opening JSON metadata file for writing: %s", jsonfilePath) 527 } 528 defer jsonfile.Close() 529 530 b, err := json.Marshal(stateMachine) 531 if err != nil { 532 return fmt.Errorf("failed to JSON encode metadata: %w", err) 533 } 534 535 _, err = jsonfile.Write(b) 536 if err != nil { 537 return fmt.Errorf("failed to write metadata to file: %w", err) 538 } 539 return nil 540 } 541 542 // handleContentSizes ensures that the sizes of the partitions are large enough and stores 543 // safe values in the stateMachine struct for use during make_image 544 func (stateMachine *StateMachine) handleContentSizes(farthestOffset quantity.Offset, volumeName string) { 545 // store volume sizes in the stateMachine Struct. These will be used during 546 // the make_image step 547 calculated := quantity.Size((farthestOffset/quantity.OffsetMiB + 17) * quantity.OffsetMiB) 548 volumeSize, found := stateMachine.ImageSizes[volumeName] 549 if !found { 550 stateMachine.ImageSizes[volumeName] = calculated 551 } else { 552 if volumeSize < calculated { 553 fmt.Printf("WARNING: ignoring image size smaller than "+ 554 "minimum required size: vol:%s %d < %d\n", 555 volumeName, uint64(volumeSize), uint64(calculated)) 556 stateMachine.ImageSizes[volumeName] = calculated 557 } else { 558 stateMachine.ImageSizes[volumeName] = volumeSize 559 } 560 } 561 } 562 563 // generate work directory file structure 564 func (stateMachine *StateMachine) makeTemporaryDirectories() error { 565 // if no workdir was specified, open a /tmp dir 566 if stateMachine.stateMachineFlags.WorkDir == "" { 567 stateMachine.stateMachineFlags.WorkDir = filepath.Join("/tmp", "ubuntu-image-"+uuid.NewString()) 568 if err := osMkdir(stateMachine.stateMachineFlags.WorkDir, 0755); err != nil { 569 return fmt.Errorf("Failed to create temporary directory: %s", err.Error()) 570 } 571 stateMachine.cleanWorkDir = true 572 } else { 573 err := osMkdirAll(stateMachine.stateMachineFlags.WorkDir, 0755) 574 if err != nil && !os.IsExist(err) { 575 return fmt.Errorf("Error creating work directory: %s", err.Error()) 576 } 577 } 578 579 stateMachine.tempDirs.rootfs = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "root") 580 stateMachine.tempDirs.unpack = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "unpack") 581 stateMachine.tempDirs.volumes = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "volumes") 582 stateMachine.tempDirs.chroot = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "chroot") 583 stateMachine.tempDirs.scratch = filepath.Join(stateMachine.stateMachineFlags.WorkDir, "scratch") 584 585 tempDirs := []string{stateMachine.tempDirs.scratch, stateMachine.tempDirs.rootfs, stateMachine.tempDirs.unpack} 586 for _, tempDir := range tempDirs { 587 err := osMkdir(tempDir, 0755) 588 if err != nil && !os.IsExist(err) { 589 return fmt.Errorf("Error creating temporary directory \"%s\": \"%s\"", tempDir, err.Error()) 590 } 591 } 592 593 return nil 594 } 595 596 // determineOutputDirectory sets the directory in which to place artifacts 597 // and creates it if it doesn't already exist 598 func (stateMachine *StateMachine) determineOutputDirectory() error { 599 if stateMachine.commonFlags.OutputDir == "" { 600 if stateMachine.cleanWorkDir { // no workdir specified, so create the image in the pwd 601 var err error 602 stateMachine.commonFlags.OutputDir, err = os.Getwd() 603 if err != nil { 604 return fmt.Errorf("Error creating OutputDir: %s", err.Error()) 605 } 606 } else { 607 stateMachine.commonFlags.OutputDir = stateMachine.stateMachineFlags.WorkDir 608 } 609 } else { 610 err := osMkdirAll(stateMachine.commonFlags.OutputDir, 0755) 611 if err != nil && !os.IsExist(err) { 612 return fmt.Errorf("Error creating OutputDir: %s", err.Error()) 613 } 614 } 615 return nil 616 } 617 618 // Run iterates through the state functions, stopping when appropriate based on --until and --thru 619 func (stateMachine *StateMachine) Run() error { 620 if stateMachine.commonFlags.DryRun { 621 return nil 622 } 623 // iterate through the states 624 for i := 0; i < len(stateMachine.states); i++ { 625 stateFunc := stateMachine.states[i] 626 stateMachine.CurrentStep = stateFunc.name 627 if stateFunc.name == stateMachine.stateMachineFlags.Until { 628 break 629 } 630 if !stateMachine.commonFlags.Quiet { 631 fmt.Printf("[%d] %s\n", stateMachine.StepsTaken, stateFunc.name) 632 } 633 start := time.Now() 634 err := stateFunc.function(stateMachine) 635 if stateMachine.commonFlags.Debug { 636 fmt.Printf("duration: %v\n", time.Since(start)) 637 } 638 if err != nil { 639 // clean up work dir on error 640 cleanupErr := stateMachine.cleanup() 641 if cleanupErr != nil { 642 return fmt.Errorf("error during cleanup: %s while cleaning after stateFunc error: %w", cleanupErr.Error(), err) 643 } 644 return err 645 } 646 stateMachine.StepsTaken++ 647 if stateFunc.name == stateMachine.stateMachineFlags.Thru { 648 break 649 } 650 } 651 fmt.Println("Build successful") 652 return nil 653 } 654 655 // Teardown handles anything else that needs to happen after the states have finished running 656 func (stateMachine *StateMachine) Teardown() error { 657 if stateMachine.commonFlags.DryRun { 658 return nil 659 } 660 if stateMachine.cleanWorkDir { 661 return stateMachine.cleanup() 662 } 663 return stateMachine.writeMetadata(metadataStateFile) 664 }