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 }