github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/statemachine/pack_test.go (about)

     1  package statemachine
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"testing"
     9  
    10  	"github.com/snapcore/snapd/osutil"
    11  
    12  	"github.com/canonical/ubuntu-image/internal/commands"
    13  	"github.com/canonical/ubuntu-image/internal/helper"
    14  	"github.com/canonical/ubuntu-image/internal/testhelper"
    15  )
    16  
    17  func TestPack_Setup(t *testing.T) {
    18  	asserter := helper.Asserter{T: t}
    19  	restoreCWD := testhelper.SaveCWD()
    20  	defer restoreCWD()
    21  
    22  	var stateMachine PackStateMachine
    23  	stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts()
    24  	stateMachine.parent = &stateMachine
    25  
    26  	err := stateMachine.Setup()
    27  	asserter.AssertErrNil(err, true)
    28  }
    29  
    30  // TestPack_validateInput_fail tests a failure in the Setup() function when validating common input
    31  func TestPack_validateInput_fail(t *testing.T) {
    32  	testCases := []struct {
    33  		name   string
    34  		until  string
    35  		thru   string
    36  		errMsg string
    37  	}{
    38  		{
    39  			name:   "invalid_until_name",
    40  			until:  "fake step",
    41  			thru:   "",
    42  			errMsg: "not a valid state name",
    43  		},
    44  		{
    45  			name:   "invalid_thru_name",
    46  			until:  "",
    47  			thru:   "fake step",
    48  			errMsg: "not a valid state name",
    49  		},
    50  		{
    51  			name:   "both_until_and_thru",
    52  			until:  "make_temporary_directories",
    53  			thru:   "calculate_rootfs_size",
    54  			errMsg: "cannot specify both --until and --thru",
    55  		},
    56  	}
    57  	for _, tc := range testCases {
    58  		t.Run("test_failed_snap_setup_"+tc.name, func(t *testing.T) {
    59  			asserter := helper.Asserter{T: t}
    60  			restoreCWD := testhelper.SaveCWD()
    61  			defer restoreCWD()
    62  
    63  			var stateMachine PackStateMachine
    64  			stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts()
    65  			stateMachine.parent = &stateMachine
    66  			stateMachine.stateMachineFlags.Until = tc.until
    67  			stateMachine.stateMachineFlags.Thru = tc.thru
    68  
    69  			err := stateMachine.Setup()
    70  			asserter.AssertErrContains(err, tc.errMsg)
    71  		})
    72  	}
    73  }
    74  
    75  // TestPack_readMetadata_fail tests a failed metadata read by passing --resume with no previous partial state machine run
    76  func TestPack_readMetadata_fail(t *testing.T) {
    77  	asserter := helper.Asserter{T: t}
    78  	restoreCWD := testhelper.SaveCWD()
    79  	defer restoreCWD()
    80  
    81  	// start a --resume with no previous SM run
    82  	var stateMachine PackStateMachine
    83  	stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts()
    84  	stateMachine.stateMachineFlags.Resume = true
    85  	stateMachine.stateMachineFlags.WorkDir = testDir
    86  
    87  	err := stateMachine.Setup()
    88  	asserter.AssertErrContains(err, "error reading metadata file")
    89  	os.RemoveAll(stateMachine.stateMachineFlags.WorkDir)
    90  }
    91  
    92  // TestPack_makeTemporaryDirectories_fail tests the Setup function with makeTemporaryDirectories failing
    93  func TestPack_makeTemporaryDirectories_fail(t *testing.T) {
    94  	asserter := helper.Asserter{T: t}
    95  	restoreCWD := testhelper.SaveCWD()
    96  	defer restoreCWD()
    97  
    98  	var stateMachine PackStateMachine
    99  	stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts()
   100  	stateMachine.parent = &stateMachine
   101  
   102  	stateMachine.stateMachineFlags.WorkDir = testDir
   103  
   104  	// mock os.MkdirAll
   105  	osMkdirAll = mockMkdirAll
   106  	t.Cleanup(func() {
   107  		osMkdirAll = os.MkdirAll
   108  	})
   109  	err := stateMachine.Setup()
   110  	asserter.AssertErrContains(err, "Error creating work directory")
   111  }
   112  
   113  // TestPackStateMachine_DryRun tests a successful dry-run execution
   114  func TestPackStateMachine_DryRun(t *testing.T) {
   115  	asserter := helper.Asserter{T: t}
   116  	restoreCWD := testhelper.SaveCWD()
   117  	defer restoreCWD()
   118  
   119  	workDir := "ubuntu-image-test-dry-run"
   120  	err := os.Mkdir(workDir, 0755)
   121  	asserter.AssertErrNil(err, true)
   122  
   123  	t.Cleanup(func() { os.RemoveAll(workDir) })
   124  
   125  	var stateMachine PackStateMachine
   126  	stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts()
   127  	stateMachine.parent = &stateMachine
   128  	stateMachine.stateMachineFlags.WorkDir = workDir
   129  	stateMachine.commonFlags.DryRun = true
   130  
   131  	err = stateMachine.Setup()
   132  	asserter.AssertErrNil(err, true)
   133  
   134  	files, err := osReadDir(workDir)
   135  	asserter.AssertErrNil(err, true)
   136  
   137  	if len(files) != 0 {
   138  		t.Errorf("Some files were created in the workdir but should not. Created files: %s", files)
   139  	}
   140  
   141  	err = stateMachine.Run()
   142  	asserter.AssertErrNil(err, true)
   143  
   144  	err = stateMachine.Teardown()
   145  	asserter.AssertErrNil(err, true)
   146  }
   147  
   148  func TestPack_populateTemporaryDirectories(t *testing.T) {
   149  	testCases := []struct {
   150  		name        string
   151  		mockFuncs   func() func()
   152  		expectedErr string
   153  		opts        commands.PackOpts
   154  	}{
   155  		{
   156  			name: "success",
   157  			opts: commands.PackOpts{
   158  				RootfsDir: filepath.Join("testdata", "filesystem"),
   159  				GadgetDir: filepath.Join("testdata", "gadget_dir"),
   160  			},
   161  		},
   162  		{
   163  			name:        "fail to read files from rootfs",
   164  			expectedErr: "Error reading rootfs dir",
   165  			opts: commands.PackOpts{
   166  				RootfsDir: filepath.Join("inexistent"),
   167  				GadgetDir: filepath.Join("testdata", "gadget_dir"),
   168  			},
   169  		},
   170  		{
   171  			name: "fail to copy files to rootfs",
   172  			mockFuncs: func() func() {
   173  				osutilCopySpecialFile = mockCopySpecialFile
   174  				return func() { osutilCopySpecialFile = osutil.CopySpecialFile }
   175  			},
   176  			expectedErr: "Error copying rootfs",
   177  			opts: commands.PackOpts{
   178  				RootfsDir: filepath.Join("testdata", "filesystem"),
   179  				GadgetDir: filepath.Join("testdata", "gadget_dir"),
   180  			},
   181  		},
   182  		{
   183  			name: "fail to create needed gadget dir",
   184  			mockFuncs: func() func() {
   185  				osMkdir = mockMkdir
   186  				return func() { osMkdir = os.Mkdir }
   187  			},
   188  			expectedErr: "Error creating scratch/gadget directory",
   189  			opts: commands.PackOpts{
   190  				RootfsDir: filepath.Join("testdata", "filesystem"),
   191  				GadgetDir: filepath.Join("testdata", "gadget_dir"),
   192  			},
   193  		},
   194  		{
   195  			name:        "fail to copy to inexistent rootfs",
   196  			expectedErr: "Error copying rootfs",
   197  			opts: commands.PackOpts{
   198  				RootfsDir: filepath.Join("testdata", "filesystem"),
   199  				GadgetDir: filepath.Join("testdata", "gadget_dir"),
   200  			},
   201  			mockFuncs: func() func() {
   202  				mock := testhelper.NewOSMock(
   203  					&testhelper.OSMockConf{
   204  						OsutilCopySpecialFileThreshold: 1,
   205  					},
   206  				)
   207  
   208  				osutilCopySpecialFile = mock.CopySpecialFile
   209  				return func() { osutilCopySpecialFile = osutil.CopySpecialFile }
   210  			},
   211  		},
   212  		{
   213  			name:        "fail to read gadget dir",
   214  			expectedErr: "Error reading gadget dir",
   215  			opts: commands.PackOpts{
   216  				RootfsDir: filepath.Join("testdata", "filesystem"),
   217  				GadgetDir: filepath.Join("testdata", "gadget_dir"),
   218  			},
   219  			mockFuncs: func() func() {
   220  				mock := testhelper.NewOSMock(
   221  					&testhelper.OSMockConf{
   222  						ReadDirThreshold: 1,
   223  					},
   224  				)
   225  
   226  				osReadDir = mock.ReadDir
   227  				return func() { osReadDir = os.ReadDir }
   228  			},
   229  		},
   230  		{
   231  			name:        "fail to copy gadget destination",
   232  			expectedErr: "Error copying gadget",
   233  			opts: commands.PackOpts{
   234  				RootfsDir: filepath.Join("testdata", "filesystem"),
   235  				GadgetDir: filepath.Join("testdata", "gadget_dir"),
   236  			},
   237  			mockFuncs: func() func() {
   238  				mock := testhelper.NewOSMock(
   239  					&testhelper.OSMockConf{
   240  						OsutilCopySpecialFileThreshold: 2,
   241  					},
   242  				)
   243  
   244  				osutilCopySpecialFile = mock.CopySpecialFile
   245  				return func() { osutilCopySpecialFile = osutil.CopySpecialFile }
   246  			},
   247  		},
   248  	}
   249  
   250  	for _, tc := range testCases {
   251  		t.Run(tc.name, func(t *testing.T) {
   252  			asserter := helper.Asserter{T: t}
   253  			stateMachine := &PackStateMachine{
   254  				Opts: tc.opts,
   255  			}
   256  			stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts()
   257  			stateMachine.parent = stateMachine
   258  
   259  			err := stateMachine.makeTemporaryDirectories()
   260  			asserter.AssertErrNil(err, true)
   261  
   262  			t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) })
   263  
   264  			if tc.mockFuncs != nil {
   265  				restoreMock := tc.mockFuncs()
   266  				t.Cleanup(restoreMock)
   267  			}
   268  
   269  			err = stateMachine.populateTemporaryDirectories()
   270  			if err != nil || len(tc.expectedErr) != 0 {
   271  				asserter.AssertErrContains(err, tc.expectedErr)
   272  			}
   273  		})
   274  	}
   275  }
   276  
   277  // TestPackStateMachine_SuccessfulRun runs through a full pack state machine run and ensures
   278  // it is successful. It creates a .img file and ensures they are the
   279  // correct file types it also mounts the resulting .img and ensures grub was updated
   280  func TestPackStateMachine_SuccessfulRun(t *testing.T) {
   281  	if testing.Short() {
   282  		t.Skip("skipping test in short mode.")
   283  	}
   284  
   285  	asserter := helper.Asserter{T: t}
   286  	restoreCWD := testhelper.SaveCWD()
   287  	t.Cleanup(restoreCWD)
   288  
   289  	// We need the output directory set for this
   290  	outputDir, err := os.MkdirTemp("/tmp", "ubuntu-image-")
   291  	asserter.AssertErrNil(err, true)
   292  	t.Cleanup(func() { os.RemoveAll(outputDir) })
   293  
   294  	gadgetDir, err := os.MkdirTemp("/tmp", "ubuntu-image-gadget-")
   295  	asserter.AssertErrNil(err, true)
   296  	t.Cleanup(func() { os.RemoveAll(gadgetDir) })
   297  
   298  	rootfsDir, err := os.MkdirTemp("/tmp", "ubuntu-image-rootfs-")
   299  	asserter.AssertErrNil(err, true)
   300  	t.Cleanup(func() { os.RemoveAll(rootfsDir) })
   301  
   302  	var stateMachine PackStateMachine
   303  	stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts()
   304  	stateMachine.parent = &stateMachine
   305  	stateMachine.commonFlags.Debug = true
   306  	stateMachine.commonFlags.Size = "5G"
   307  	stateMachine.commonFlags.OutputDir = outputDir
   308  
   309  	stateMachine.Opts = commands.PackOpts{
   310  		RootfsDir: rootfsDir,
   311  		GadgetDir: filepath.Join(gadgetDir, "gadget"),
   312  	}
   313  
   314  	gadgetSource := filepath.Join("testdata", "gadget_tree")
   315  	err = osutil.CopySpecialFile(gadgetSource, gadgetDir)
   316  	asserter.AssertErrNil(err, true)
   317  
   318  	err = os.Rename(filepath.Join(gadgetDir, "gadget_tree"), filepath.Join(gadgetDir, "gadget"))
   319  	asserter.AssertErrNil(err, true)
   320  
   321  	// also copy gadget.yaml to the root of the gadget dir
   322  	err = osutil.CopyFile(
   323  		filepath.Join("testdata", "gadget_dir", "gadget.yaml"),
   324  		filepath.Join(gadgetDir, "gadget", "gadget.yaml"),
   325  		osutil.CopyFlagDefault,
   326  	)
   327  	asserter.AssertErrNil(err, true)
   328  
   329  	debootstrapCmd := execCommand("debootstrap",
   330  		"--arch", "amd64",
   331  		"--variant=minbase",
   332  		"--include=grub2-common",
   333  		"jammy",
   334  		stateMachine.Opts.RootfsDir,
   335  		"http://archive.ubuntu.com/ubuntu/",
   336  	)
   337  
   338  	debootstrapOutput := helper.SetCommandOutput(debootstrapCmd, true)
   339  
   340  	err = debootstrapCmd.Run()
   341  	if err != nil {
   342  		t.Errorf("Error running debootstrap command \"%s\". Error is \"%s\". Output is: \n%s",
   343  			debootstrapCmd.String(), err.Error(), debootstrapOutput.String())
   344  	}
   345  	asserter.AssertErrNil(err, true)
   346  
   347  	err = os.Mkdir(filepath.Join(stateMachine.Opts.RootfsDir, "boot", "grub"), 0755)
   348  	asserter.AssertErrNil(err, true)
   349  
   350  	err = stateMachine.Setup()
   351  	asserter.AssertErrNil(err, true)
   352  
   353  	t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) })
   354  
   355  	err = stateMachine.Run()
   356  	asserter.AssertErrNil(err, true)
   357  
   358  	t.Cleanup(func() {
   359  		err = stateMachine.Teardown()
   360  		asserter.AssertErrNil(err, true)
   361  	})
   362  
   363  	artifacts := map[string]string{"pc.img": "DOS/MBR boot sector"}
   364  	testHelperCheckArtifacts(t, &asserter, stateMachine.commonFlags.OutputDir, artifacts)
   365  
   366  	// create a directory in which to mount the rootfs
   367  	mountDir := filepath.Join(stateMachine.tempDirs.scratch, "loopback")
   368  	var mountImageCmds []*exec.Cmd
   369  	var umountImageCmds []*exec.Cmd
   370  
   371  	t.Cleanup(func() {
   372  		for _, teardownCmd := range umountImageCmds {
   373  			if tmpErr := teardownCmd.Run(); tmpErr != nil {
   374  				if err != nil {
   375  					err = fmt.Errorf("%s after previous error: %w", tmpErr, err)
   376  				} else {
   377  					err = tmpErr
   378  				}
   379  			}
   380  		}
   381  	})
   382  
   383  	// set up the loopback
   384  	mountImageCmds = append(mountImageCmds,
   385  		//nolint:gosec,G204
   386  		exec.Command("losetup",
   387  			filepath.Join("/dev", "loop99"),
   388  			filepath.Join(stateMachine.commonFlags.OutputDir, "pc.img"),
   389  		),
   390  	)
   391  
   392  	// unset the loopback
   393  	umountImageCmds = append(umountImageCmds,
   394  		//nolint:gosec,G204
   395  		exec.Command("losetup", "--detach", filepath.Join("/dev", "loop99")),
   396  	)
   397  
   398  	mountImageCmds = append(mountImageCmds,
   399  		//nolint:gosec,G204
   400  		exec.Command("kpartx", "-a", filepath.Join("/dev", "loop99")),
   401  	)
   402  
   403  	umountImageCmds = append([]*exec.Cmd{
   404  		//nolint:gosec,G204
   405  		exec.Command("kpartx", "-d", filepath.Join("/dev", "loop99")),
   406  	}, umountImageCmds...,
   407  	)
   408  
   409  	mountImageCmds = append(mountImageCmds,
   410  		//nolint:gosec,G204
   411  		exec.Command("mount", filepath.Join("/dev", "mapper", "loop99p3"), mountDir), // with this example the rootfs is partition 3 mountDir
   412  	)
   413  
   414  	umountImageCmds = append([]*exec.Cmd{
   415  		//nolint:gosec,G204
   416  		exec.Command("mount", "--make-rprivate", filepath.Join("/dev", "mapper", "loop99p3")),
   417  		//nolint:gosec,G204
   418  		exec.Command("umount", "--recursive", filepath.Join("/dev", "mapper", "loop99p3")),
   419  	}, umountImageCmds...,
   420  	)
   421  
   422  	// set up the mountpoints
   423  	mountPoints := []mountPoint{
   424  		{
   425  			src:      "devtmpfs-build",
   426  			basePath: mountDir,
   427  			relpath:  "/dev",
   428  			typ:      "devtmpfs",
   429  		},
   430  		{
   431  			src:      "devpts-build",
   432  			basePath: mountDir,
   433  			relpath:  "/dev/pts",
   434  			typ:      "devpts",
   435  			opts:     []string{"nodev", "nosuid"},
   436  		},
   437  		{
   438  			src:      "proc-build",
   439  			basePath: mountDir,
   440  			relpath:  "/proc",
   441  			typ:      "proc",
   442  		},
   443  		{
   444  			src:      "sysfs-build",
   445  			basePath: mountDir,
   446  			relpath:  "/sys",
   447  			typ:      "sysfs",
   448  		},
   449  	}
   450  	for _, mp := range mountPoints {
   451  		mountCmds, umountCmds, err := mp.getMountCmd()
   452  		if err != nil {
   453  			t.Errorf("Error preparing mountpoint \"%s\": \"%s\"",
   454  				mp.relpath,
   455  				err.Error(),
   456  			)
   457  		}
   458  		mountImageCmds = append(mountImageCmds, mountCmds...)
   459  		umountImageCmds = append(umountCmds, umountImageCmds...)
   460  	}
   461  	// make sure to unmount the disk too
   462  	umountImageCmds = append([]*exec.Cmd{exec.Command("umount", "--recursive", mountDir)}, umountImageCmds...)
   463  
   464  	// now run all the commands to mount the image
   465  	for _, cmd := range mountImageCmds {
   466  		outPut := helper.SetCommandOutput(cmd, true)
   467  		err := cmd.Run()
   468  		if err != nil {
   469  			t.Errorf("Error running command \"%s\". Error is \"%s\". Output is: \n%s",
   470  				cmd.String(), err.Error(), outPut.String())
   471  		}
   472  	}
   473  
   474  	testHelperCheckGrubConfig(t, mountDir)
   475  }