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  }