github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/statemachine/classic_test.go (about) 1 // This test file tests a successful classic run and success/error scenarios for all states 2 // that are specific to the classic builds 3 package statemachine 4 5 import ( 6 "bufio" 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "path" 14 "path/filepath" 15 "reflect" 16 "regexp" 17 "runtime" 18 "strconv" 19 "strings" 20 "testing" 21 22 "github.com/pkg/xattr" 23 "github.com/snapcore/snapd/image" 24 "github.com/snapcore/snapd/osutil" 25 "github.com/snapcore/snapd/seed" 26 "github.com/snapcore/snapd/store" 27 "github.com/xeipuuv/gojsonschema" 28 "gopkg.in/yaml.v2" 29 30 "github.com/canonical/ubuntu-image/internal/helper" 31 "github.com/canonical/ubuntu-image/internal/imagedefinition" 32 "github.com/canonical/ubuntu-image/internal/testhelper" 33 ) 34 35 var yamlMarshal = yaml.Marshal 36 37 func TestMain(m *testing.M) { 38 basicChroot = NewBasicChroot() 39 code := m.Run() 40 basicChroot.Clean() 41 os.Exit(code) 42 } 43 44 // TestClassicSetup tests a successful run of the polymorphed Setup function 45 func TestClassicSetup(t *testing.T) { 46 asserter := helper.Asserter{T: t} 47 restoreCWD := testhelper.SaveCWD() 48 defer restoreCWD() 49 50 var stateMachine ClassicStateMachine 51 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 52 stateMachine.parent = &stateMachine 53 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 54 "test_amd64.yaml") 55 56 err := stateMachine.Setup() 57 asserter.AssertErrNil(err, true) 58 } 59 60 // TestYAMLSchemaParsing attempts to parse a variety of image definition files, both 61 // valid and invalid, and ensures the correct result/errors are returned 62 func TestYAMLSchemaParsing(t *testing.T) { 63 t.Parallel() 64 testCases := []struct { 65 name string 66 imageDefinition string 67 shouldPass bool 68 expectedError string 69 }{ 70 {"valid_image_definition", "test_raspi.yaml", true, ""}, 71 {"invalid_class", "test_bad_class.yaml", false, "Class must be one of the following"}, 72 {"invalid_url", "test_bad_url.yaml", false, "Does not match format 'uri'"}, 73 {"invalid_model_assertion_url", "test_invalid_model_assertion_url.yaml", false, "Does not match format 'uri'"}, 74 {"invalid_ppa_name", "test_bad_ppa_name.yaml", false, "PPAName: Does not match pattern"}, 75 {"invalid_ppa_auth", "test_bad_ppa_name.yaml", false, "Auth: Does not match pattern"}, 76 {"both_seed_and_tasks", "test_both_seed_and_tasks.yaml", false, "Must validate one and only one schema"}, 77 {"git_gadget_without_url", "test_git_gadget_without_url.yaml", false, "When key gadget:type is specified as git, a URL must be provided"}, 78 {"file_doesnt_exist", "test_not_exist.yaml", false, "no such file or directory"}, 79 {"not_valid_yaml", "test_invalid_yaml.yaml", false, "yaml: unmarshal errors"}, 80 {"missing_yaml_fields", "test_missing_name.yaml", false, "Key \"name\" is required in struct \"ImageDefinition\", but is not in the YAML file!"}, 81 {"private_ppa_without_fingerprint", "test_private_ppa_without_fingerprint.yaml", false, "Fingerprint is required for private PPAs"}, 82 {"invalid_paths_in_manual_copy", "test_invalid_paths_in_manual_copy.yaml", false, "needs to be an absolute path (../../malicious)"}, 83 {"invalid_paths_in_manual_copy_bug", "test_invalid_paths_in_manual_copy.yaml", false, "needs to be an absolute path (/../../malicious)"}, 84 {"invalid_paths_in_manual_mkdir", "test_invalid_paths_in_manual_mkdir.yaml", false, "needs to be an absolute path (../../malicious)"}, 85 {"invalid_paths_in_manual_mkdir_bug", "test_invalid_paths_in_manual_mkdir.yaml", false, "needs to be an absolute path (/../../malicious)"}, 86 {"invalid_paths_in_manual_touch_file", "test_invalid_paths_in_manual_touch_file.yaml", false, "needs to be an absolute path (../../malicious)"}, 87 {"invalid_paths_in_manual_touch_file_bug", "test_invalid_paths_in_manual_touch_file.yaml", false, "needs to be an absolute path (/../../malicious)"}, 88 {"img_specified_without_gadget", "test_image_without_gadget.yaml", false, "Key img cannot be used without key gadget:"}, 89 } 90 for _, tc := range testCases { 91 t.Run(tc.name, func(t *testing.T) { 92 asserter := helper.Asserter{T: t} 93 restoreCWD := testhelper.SaveCWD() 94 defer restoreCWD() 95 96 var stateMachine ClassicStateMachine 97 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 98 stateMachine.parent = &stateMachine 99 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 100 tc.imageDefinition) 101 err := stateMachine.parseImageDefinition() 102 103 if tc.shouldPass { 104 asserter.AssertErrNil(err, false) 105 } else { 106 asserter.AssertErrContains(err, tc.expectedError) 107 } 108 }) 109 } 110 } 111 112 // TestFailedParseImageDefinition mocks function calls to test 113 // failure cases in the parseImageDefinition state 114 func TestFailedParseImageDefinition(t *testing.T) { 115 asserter := helper.Asserter{T: t} 116 restoreCWD := testhelper.SaveCWD() 117 defer restoreCWD() 118 119 var stateMachine ClassicStateMachine 120 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 121 stateMachine.parent = &stateMachine 122 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 123 "test_raspi.yaml") 124 125 // mock helper.SetDefaults 126 helperSetDefaults = mockSetDefaults 127 t.Cleanup(func() { 128 helperSetDefaults = helper.SetDefaults 129 }) 130 err := stateMachine.parseImageDefinition() 131 asserter.AssertErrContains(err, "Test Error") 132 helperSetDefaults = helper.SetDefaults 133 134 // mock helper.CheckEmptyFields 135 helperCheckEmptyFields = mockCheckEmptyFields 136 t.Cleanup(func() { 137 helperCheckEmptyFields = helper.CheckEmptyFields 138 }) 139 err = stateMachine.parseImageDefinition() 140 asserter.AssertErrContains(err, "Test Error") 141 helperCheckEmptyFields = helper.CheckEmptyFields 142 143 // mock gojsonschema.Validate 144 gojsonschemaValidate = mockGojsonschemaValidateError 145 t.Cleanup(func() { 146 gojsonschemaValidate = gojsonschema.Validate 147 }) 148 err = stateMachine.parseImageDefinition() 149 asserter.AssertErrContains(err, "Schema validation returned an error") 150 gojsonschemaValidate = gojsonschema.Validate 151 152 // mock helper.CheckTags 153 // the gadget must be set to nil for this test to work 154 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 155 "test_image_without_gadget.yaml") 156 helperCheckTags = mockCheckTags 157 t.Cleanup(func() { 158 helperCheckTags = helper.CheckTags 159 }) 160 err = stateMachine.parseImageDefinition() 161 asserter.AssertErrContains(err, "Test Error") 162 helperCheckTags = helper.CheckTags 163 } 164 165 // TestClassicStateMachine_calculateStates reads in a variety of yaml files and ensures 166 // that the correct states are added to the state machine 167 // TODO: manually assemble the image definitions instead of relying on the parseImageDefinition() function to make this more of a unit test 168 func TestClassicStateMachine_calculateStates(t *testing.T) { 169 t.Parallel() 170 testCases := []struct { 171 name string 172 imageDefinition string 173 expectedStates []string 174 }{ 175 { 176 name: "state_build_gadget", 177 imageDefinition: "test_build_gadget.yaml", 178 expectedStates: []string{ 179 "build_gadget_tree", 180 "prepare_gadget_tree", 181 "load_gadget_yaml", 182 "verify_artifact_names", 183 "germinate", 184 "create_chroot", 185 "install_packages", 186 "prepare_image", 187 "preseed_image", 188 "clean_rootfs", 189 "customize_sources_list", 190 "customize_cloud_init", 191 "set_default_locale", 192 "populate_rootfs_contents", 193 "calculate_rootfs_size", 194 "populate_bootfs_contents", 195 "populate_prepare_partitions", 196 "make_disk", 197 "update_bootloader", 198 "generate_package_manifest", 199 }, 200 }, 201 { 202 name: "state_prebuilt_gadget", 203 imageDefinition: "test_prebuilt_gadget.yaml", 204 expectedStates: []string{ 205 "build_gadget_tree", 206 "prepare_gadget_tree", 207 "load_gadget_yaml", 208 "verify_artifact_names", 209 "germinate", 210 "create_chroot", 211 "install_packages", 212 "prepare_image", 213 "preseed_image", 214 "clean_rootfs", 215 "customize_sources_list", 216 "customize_cloud_init", 217 "set_default_locale", 218 "populate_rootfs_contents", 219 "calculate_rootfs_size", 220 "populate_bootfs_contents", 221 "populate_prepare_partitions", 222 "make_disk", 223 "update_bootloader", 224 "generate_package_manifest", 225 }, 226 }, 227 { 228 name: "state_prebuilt_rootfs_extras", 229 imageDefinition: "test_prebuilt_rootfs_extras.yaml", 230 expectedStates: []string{ 231 "build_gadget_tree", 232 "prepare_gadget_tree", 233 "load_gadget_yaml", 234 "verify_artifact_names", 235 "extract_rootfs_tar", 236 "add_extra_ppas", 237 "install_packages", 238 "clean_extra_ppas", 239 "prepare_image", 240 "preseed_image", 241 "clean_rootfs", 242 "customize_sources_list", 243 "customize_cloud_init", 244 "set_default_locale", 245 "populate_rootfs_contents", 246 "calculate_rootfs_size", 247 "populate_bootfs_contents", 248 "populate_prepare_partitions", 249 "make_disk", 250 "update_bootloader", 251 "generate_package_manifest", 252 }, 253 }, 254 { 255 name: "state_ppa", 256 imageDefinition: "test_amd64.yaml", 257 expectedStates: []string{ 258 "build_gadget_tree", 259 "prepare_gadget_tree", 260 "load_gadget_yaml", 261 "verify_artifact_names", 262 "germinate", 263 "create_chroot", 264 "add_extra_ppas", 265 "install_packages", 266 "clean_extra_ppas", 267 "prepare_image", 268 "preseed_image", 269 "clean_rootfs", 270 "customize_sources_list", 271 "customize_cloud_init", 272 "perform_manual_customization", 273 "set_default_locale", 274 "populate_rootfs_contents", 275 "calculate_rootfs_size", 276 "populate_bootfs_contents", 277 "populate_prepare_partitions", 278 "make_disk", 279 "update_bootloader", 280 "make_qcow2_image", 281 "generate_package_manifest", 282 "generate_filelist", 283 }, 284 }, 285 { 286 name: "extract_rootfs_tar", 287 imageDefinition: "test_extract_rootfs_tar.yaml", 288 expectedStates: []string{ 289 "build_gadget_tree", 290 "prepare_gadget_tree", 291 "load_gadget_yaml", 292 "verify_artifact_names", 293 "extract_rootfs_tar", 294 "install_packages", 295 "clean_rootfs", 296 "customize_sources_list", 297 "customize_cloud_init", 298 "set_default_locale", 299 "populate_rootfs_contents", 300 "calculate_rootfs_size", 301 "populate_bootfs_contents", 302 "populate_prepare_partitions", 303 "make_disk", 304 "update_bootloader", 305 "generate_package_manifest", 306 }, 307 }, 308 { 309 name: "extract_rootfs_tar_no_customization", 310 imageDefinition: "test_extract_rootfs_tar_no_customization.yaml", 311 expectedStates: []string{ 312 "build_gadget_tree", 313 "prepare_gadget_tree", 314 "load_gadget_yaml", 315 "verify_artifact_names", 316 "extract_rootfs_tar", 317 "clean_rootfs", 318 "customize_sources_list", 319 "set_default_locale", 320 "populate_rootfs_contents", 321 "calculate_rootfs_size", 322 "populate_bootfs_contents", 323 "populate_prepare_partitions", 324 "make_disk", 325 "update_bootloader", 326 "generate_package_manifest", 327 }, 328 }, 329 { 330 name: "build_rootfs_from_seed", 331 imageDefinition: "test_rootfs_seed.yaml", 332 expectedStates: []string{ 333 "build_gadget_tree", 334 "prepare_gadget_tree", 335 "load_gadget_yaml", 336 "verify_artifact_names", 337 "germinate", 338 "create_chroot", 339 "install_packages", 340 "prepare_image", 341 "preseed_image", 342 "clean_rootfs", 343 "customize_sources_list", 344 "customize_cloud_init", 345 "set_default_locale", 346 "populate_rootfs_contents", 347 "calculate_rootfs_size", 348 "populate_bootfs_contents", 349 "populate_prepare_partitions", 350 "make_disk", 351 "update_bootloader", 352 "generate_package_manifest", 353 }, 354 }, 355 { 356 name: "build_rootfs_from_tasks", 357 imageDefinition: "test_rootfs_tasks.yaml", 358 expectedStates: []string{ 359 "build_gadget_tree", 360 "prepare_gadget_tree", 361 "load_gadget_yaml", 362 "verify_artifact_names", 363 "build_rootfs_from_tasks", 364 "clean_rootfs", 365 "customize_sources_list", 366 "customize_cloud_init", 367 "set_default_locale", 368 "populate_rootfs_contents", 369 "calculate_rootfs_size", 370 "populate_bootfs_contents", 371 "populate_prepare_partitions", 372 "make_disk", 373 "update_bootloader", 374 "generate_package_manifest", 375 }, 376 }, 377 { 378 name: "customization_states", 379 imageDefinition: "test_customization.yaml", 380 expectedStates: []string{ 381 "build_gadget_tree", 382 "prepare_gadget_tree", 383 "load_gadget_yaml", 384 "verify_artifact_names", 385 "germinate", 386 "create_chroot", 387 "add_extra_ppas", 388 "install_packages", 389 "clean_extra_ppas", 390 "prepare_image", 391 "preseed_image", 392 "clean_rootfs", 393 "customize_sources_list", 394 "customize_cloud_init", 395 "perform_manual_customization", 396 "set_default_locale", 397 "populate_rootfs_contents", 398 "calculate_rootfs_size", 399 "populate_bootfs_contents", 400 "populate_prepare_partitions", 401 "make_disk", 402 "update_bootloader", 403 "generate_package_manifest", 404 }, 405 }, 406 { 407 name: "qcow2", 408 imageDefinition: "test_qcow2.yaml", 409 expectedStates: []string{ 410 "build_gadget_tree", 411 "prepare_gadget_tree", 412 "load_gadget_yaml", 413 "verify_artifact_names", 414 "germinate", 415 "create_chroot", 416 "add_extra_ppas", 417 "install_packages", 418 "clean_extra_ppas", 419 "prepare_image", 420 "preseed_image", 421 "clean_rootfs", 422 "customize_sources_list", 423 "customize_cloud_init", 424 "set_default_locale", 425 "populate_rootfs_contents", 426 "calculate_rootfs_size", 427 "populate_bootfs_contents", 428 "populate_prepare_partitions", 429 "make_disk", 430 "update_bootloader", 431 "make_qcow2_image", 432 }, 433 }, 434 { 435 name: "no artifact", 436 imageDefinition: "test_no_artifact.yaml", 437 expectedStates: []string{ 438 "build_gadget_tree", 439 "prepare_gadget_tree", 440 "load_gadget_yaml", 441 "germinate", 442 "create_chroot", 443 "add_extra_ppas", 444 "install_packages", 445 "clean_extra_ppas", 446 "prepare_image", 447 "preseed_image", 448 "clean_rootfs", 449 "customize_sources_list", 450 "customize_cloud_init", 451 "perform_manual_customization", 452 "set_default_locale", 453 "populate_rootfs_contents", 454 }, 455 }, 456 } 457 for _, tc := range testCases { 458 t.Run(tc.name, func(t *testing.T) { 459 asserter := helper.Asserter{T: t} 460 restoreCWD := testhelper.SaveCWD() 461 defer restoreCWD() 462 463 var stateMachine ClassicStateMachine 464 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 465 stateMachine.parent = &stateMachine 466 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", tc.imageDefinition) 467 err := stateMachine.parseImageDefinition() 468 asserter.AssertErrNil(err, true) 469 470 err = stateMachine.calculateStates() 471 asserter.AssertErrNil(err, true) 472 473 stateNames := make([]string, 0) 474 for _, f := range stateMachine.states { 475 stateNames = append(stateNames, f.name) 476 } 477 478 asserter.AssertEqual(tc.expectedStates, stateNames) 479 }) 480 } 481 } 482 483 // TestFailedCalculateStates tests failure scenarios in the 484 // calculateStates function 485 func TestFailedCalculateStates(t *testing.T) { 486 asserter := helper.Asserter{T: t} 487 restoreCWD := testhelper.SaveCWD() 488 defer restoreCWD() 489 490 var stateMachine ClassicStateMachine 491 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 492 stateMachine.parent = &stateMachine 493 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 494 Gadget: &imagedefinition.Gadget{ 495 GadgetType: "git", 496 }, 497 Rootfs: &imagedefinition.Rootfs{ 498 ArchiveTasks: []string{"test"}, 499 }, 500 Customization: &imagedefinition.Customization{}, 501 Artifacts: &imagedefinition.Artifact{}, 502 } 503 504 // mock helper.CheckTags 505 // the gadget must be set to nil for this test to work 506 helperCheckTags = mockCheckTags 507 t.Cleanup(func() { 508 helperCheckTags = helper.CheckTags 509 }) 510 err := stateMachine.calculateStates() 511 asserter.AssertErrContains(err, "Test Error") 512 } 513 514 // TestDisplayStates ensures the states are printed to stdout when the --debug flag is set 515 func TestDisplayStates(t *testing.T) { 516 asserter := helper.Asserter{T: t} 517 restoreCWD := testhelper.SaveCWD() 518 defer restoreCWD() 519 520 var stateMachine ClassicStateMachine 521 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 522 stateMachine.parent = &stateMachine 523 stateMachine.commonFlags.Debug = true 524 stateMachine.commonFlags.DiskInfo = "test" // for coverage! 525 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", "test_raspi.yaml") 526 err := stateMachine.parseImageDefinition() 527 asserter.AssertErrNil(err, true) 528 529 // capture stdout, calculate the states, and ensure they were printed 530 stdout, restoreStdout, err := helper.CaptureStd(&os.Stdout) 531 defer restoreStdout() 532 asserter.AssertErrNil(err, true) 533 534 err = stateMachine.calculateStates() 535 asserter.AssertErrNil(err, true) 536 537 stateMachine.displayStates() 538 asserter.AssertErrNil(err, true) 539 540 // restore stdout and examine what was printed 541 restoreStdout() 542 readStdout, err := io.ReadAll(stdout) 543 asserter.AssertErrNil(err, true) 544 545 expectedStates := `Following states will be executed: 546 [0] build_gadget_tree 547 [1] prepare_gadget_tree 548 [2] load_gadget_yaml 549 [3] verify_artifact_names 550 [4] germinate 551 [5] create_chroot 552 [6] install_packages 553 [7] prepare_image 554 [8] preseed_image 555 [9] clean_rootfs 556 [10] customize_sources_list 557 [11] customize_fstab 558 [12] perform_manual_customization 559 [13] set_default_locale 560 [14] populate_rootfs_contents 561 [15] generate_disk_info 562 [16] calculate_rootfs_size 563 [17] populate_bootfs_contents 564 [18] populate_prepare_partitions 565 [19] make_disk 566 [20] update_bootloader 567 [21] generate_package_manifest 568 ` 569 if !strings.Contains(string(readStdout), expectedStates) { 570 t.Errorf("Expected states to be printed in output:\n\"%s\"\n but got \n\"%s\"\n instead", 571 expectedStates, string(readStdout)) 572 } 573 } 574 575 // TestClassicStateMachine_Setup_Fail_setConfDefDir tests a failure in the Setup() function when setting the configuration definition directory 576 func TestClassicStateMachine_Setup_Fail_setConfDefDir(t *testing.T) { 577 asserter := helper.Asserter{T: t} 578 restoreCWD := testhelper.SaveCWD() 579 defer restoreCWD() 580 581 var stateMachine ClassicStateMachine 582 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 583 584 tmpDirPath := filepath.Join("/tmp", "test_failed_set_conf_dir") 585 err := os.Mkdir(tmpDirPath, 0755) 586 t.Cleanup(func() { 587 os.RemoveAll(tmpDirPath) 588 }) 589 asserter.AssertErrNil(err, true) 590 591 err = os.Chdir(tmpDirPath) 592 asserter.AssertErrNil(err, true) 593 594 _ = os.RemoveAll(tmpDirPath) 595 596 err = stateMachine.Setup() 597 asserter.AssertErrContains(err, "unable to determine the configuration definition directory") 598 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 599 } 600 601 // TestFailedValidateInputClassic tests a failure in the Setup() function when validating common input 602 func TestFailedValidateInputClassic(t *testing.T) { 603 asserter := helper.Asserter{T: t} 604 restoreCWD := testhelper.SaveCWD() 605 defer restoreCWD() 606 607 // use both --until and --thru to trigger this failure 608 var stateMachine ClassicStateMachine 609 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 610 stateMachine.stateMachineFlags.Until = "until-test" 611 stateMachine.stateMachineFlags.Thru = "thru-test" 612 613 err := stateMachine.Setup() 614 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 615 asserter.AssertErrContains(err, "cannot specify both --until and --thru") 616 } 617 618 // TestFailedReadMetadataClassic tests a failed metadata read by passing --resume with no previous partial state machine run 619 func TestFailedReadMetadataClassic(t *testing.T) { 620 asserter := helper.Asserter{T: t} 621 restoreCWD := testhelper.SaveCWD() 622 defer restoreCWD() 623 624 // start a --resume with no previous SM run 625 var stateMachine ClassicStateMachine 626 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 627 stateMachine.stateMachineFlags.Resume = true 628 stateMachine.stateMachineFlags.WorkDir = testDir 629 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 630 "test_amd64.yaml") 631 632 err := stateMachine.Setup() 633 asserter.AssertErrContains(err, "error reading metadata file") 634 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 635 } 636 637 // TestClassicStateMachine_Setup_Fail_makeTemporaryDirectories tests the Setup function 638 // with makeTemporaryDirectories failing 639 func TestClassicStateMachine_Setup_Fail_makeTemporaryDirectories(t *testing.T) { 640 asserter := helper.Asserter{T: t} 641 restoreCWD := testhelper.SaveCWD() 642 defer restoreCWD() 643 644 var stateMachine ClassicStateMachine 645 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 646 stateMachine.stateMachineFlags.WorkDir = testDir 647 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 648 "test_amd64.yaml") 649 650 // mock os.MkdirAll 651 osMkdirAll = mockMkdirAll 652 t.Cleanup(func() { 653 osMkdirAll = os.MkdirAll 654 }) 655 err := stateMachine.Setup() 656 asserter.AssertErrContains(err, "Error creating work directory") 657 } 658 659 // TestClassicStateMachine_Setup_Fail_determineOutputDirectory tests the Setup function 660 // with determineOutputDirectory failing 661 func TestClassicStateMachine_Setup_Fail_determineOutputDirectory(t *testing.T) { 662 asserter := helper.Asserter{T: t} 663 restoreCWD := testhelper.SaveCWD() 664 defer restoreCWD() 665 666 var stateMachine ClassicStateMachine 667 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 668 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 669 "test_amd64.yaml") 670 stateMachine.commonFlags.OutputDir = "/tmp/test" 671 672 // mock os.MkdirAll 673 osMkdirAll = mockMkdirAll 674 t.Cleanup(func() { 675 osMkdirAll = os.MkdirAll 676 }) 677 err := stateMachine.Setup() 678 asserter.AssertErrContains(err, "Error creating OutputDir") 679 } 680 681 // TestClassicStateMachine_DryRun tests a successful dry-run execution 682 func TestClassicStateMachine_DryRun(t *testing.T) { 683 asserter := helper.Asserter{T: t} 684 restoreCWD := testhelper.SaveCWD() 685 defer restoreCWD() 686 687 workDir := "ubuntu-image-test-dry-run" 688 err := os.Mkdir(workDir, 0755) 689 asserter.AssertErrNil(err, true) 690 691 t.Cleanup(func() { os.RemoveAll(workDir) }) 692 693 var stateMachine ClassicStateMachine 694 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 695 stateMachine.parent = &stateMachine 696 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 697 "test_amd64.yaml") 698 stateMachine.stateMachineFlags.WorkDir = workDir 699 stateMachine.commonFlags.DryRun = true 700 701 err = stateMachine.Setup() 702 asserter.AssertErrNil(err, true) 703 704 files, err := osReadDir(workDir) 705 asserter.AssertErrNil(err, true) 706 707 if len(files) != 0 { 708 t.Errorf("Some files were created in the workdir but should not. Created files: %s", files) 709 } 710 711 err = stateMachine.Run() 712 asserter.AssertErrNil(err, true) 713 714 err = stateMachine.Teardown() 715 asserter.AssertErrNil(err, true) 716 } 717 718 // TestPrepareGadgetTree runs prepareGadgetTree() and ensures the gadget_tree files 719 // are placed in the correct locations 720 func TestPrepareGadgetTree(t *testing.T) { 721 t.Parallel() 722 asserter := helper.Asserter{T: t} 723 restoreCWD := testhelper.SaveCWD() 724 defer restoreCWD() 725 726 var stateMachine ClassicStateMachine 727 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 728 stateMachine.parent = &stateMachine 729 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 730 Architecture: getHostArch(), 731 Series: getHostSuite(), 732 Gadget: &imagedefinition.Gadget{}, 733 } 734 735 err := stateMachine.makeTemporaryDirectories() 736 asserter.AssertErrNil(err, true) 737 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 738 739 // place a test gadget tree in the scratch directory so we don't have to build one 740 gadgetDir := filepath.Join(stateMachine.tempDirs.scratch, "gadget") 741 err = os.MkdirAll(gadgetDir, 0755) 742 asserter.AssertErrNil(err, true) 743 744 gadgetSource := filepath.Join("testdata", "gadget_tree") 745 err = osutil.CopySpecialFile(gadgetSource, filepath.Join(gadgetDir, "install")) 746 asserter.AssertErrNil(err, true) 747 748 err = stateMachine.prepareGadgetTree() 749 asserter.AssertErrNil(err, true) 750 751 gadgetTreeFiles := []string{"grub.conf", "pc-boot.img", "meta/gadget.yaml"} 752 for _, file := range gadgetTreeFiles { 753 _, err := os.Stat(filepath.Join(stateMachine.tempDirs.unpack, "gadget", file)) 754 if err != nil { 755 t.Errorf("File %s should be in unpack, but is missing", file) 756 } 757 } 758 } 759 760 // TestPrepareGadgetTreePrebuilt tests the prepareGadgetTree function with prebuilt gadgets 761 func TestPrepareGadgetTreePrebuilt(t *testing.T) { 762 t.Parallel() 763 asserter := helper.Asserter{T: t} 764 restoreCWD := testhelper.SaveCWD() 765 defer restoreCWD() 766 767 var stateMachine ClassicStateMachine 768 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 769 stateMachine.parent = &stateMachine 770 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 771 Architecture: getHostArch(), 772 Series: getHostSuite(), 773 Gadget: &imagedefinition.Gadget{ 774 GadgetType: "prebuilt", 775 GadgetURL: "testdata/gadget_tree/", 776 }, 777 } 778 779 err := stateMachine.makeTemporaryDirectories() 780 asserter.AssertErrNil(err, true) 781 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 782 783 err = stateMachine.prepareGadgetTree() 784 asserter.AssertErrNil(err, true) 785 786 gadgetTreeFiles := []string{"grub.conf", "pc-boot.img", "meta/gadget.yaml"} 787 for _, file := range gadgetTreeFiles { 788 _, err := os.Stat(filepath.Join(stateMachine.tempDirs.unpack, "gadget", file)) 789 if err != nil { 790 t.Errorf("File %s should be in unpack, but is missing", file) 791 } 792 } 793 } 794 795 // TestFailedPrepareGadgetTree tests failures in the prepareGadgetTree function 796 func TestFailedPrepareGadgetTree(t *testing.T) { 797 asserter := helper.Asserter{T: t} 798 var stateMachine ClassicStateMachine 799 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 800 stateMachine.parent = &stateMachine 801 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 802 Architecture: getHostArch(), 803 Series: getHostSuite(), 804 Gadget: &imagedefinition.Gadget{}, 805 } 806 807 err := stateMachine.makeTemporaryDirectories() 808 asserter.AssertErrNil(err, true) 809 810 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 811 812 // place a test gadget tree in the scratch directory so we don't have to build one 813 gadgetDir := filepath.Join(stateMachine.tempDirs.scratch, "gadget") 814 err = os.MkdirAll(gadgetDir, 0755) 815 asserter.AssertErrNil(err, true) 816 817 gadgetSource := filepath.Join("testdata", "gadget_tree") 818 err = osutil.CopySpecialFile(gadgetSource, filepath.Join(gadgetDir, "install")) 819 asserter.AssertErrNil(err, true) 820 821 // mock os.Mkdir 822 osMkdirAll = mockMkdirAll 823 t.Cleanup(func() { 824 osMkdirAll = os.MkdirAll 825 }) 826 err = stateMachine.prepareGadgetTree() 827 asserter.AssertErrContains(err, "Error creating unpack directory") 828 osMkdirAll = os.MkdirAll 829 830 // mock os.ReadDir 831 osReadDir = mockReadDir 832 t.Cleanup(func() { 833 osReadDir = os.ReadDir 834 }) 835 err = stateMachine.prepareGadgetTree() 836 asserter.AssertErrContains(err, "Error reading gadget tree") 837 osReadDir = os.ReadDir 838 839 // mock osutil.CopySpecialFile 840 osutilCopySpecialFile = mockCopySpecialFile 841 t.Cleanup(func() { 842 osutilCopySpecialFile = osutil.CopySpecialFile 843 }) 844 err = stateMachine.prepareGadgetTree() 845 asserter.AssertErrContains(err, "Error copying gadget tree") 846 osutilCopySpecialFile = osutil.CopySpecialFile 847 } 848 849 // TestVerifyArtifactNames unit tests the verifyArtifactNames function 850 func TestVerifyArtifactNames(t *testing.T) { 851 t.Parallel() 852 testCases := []struct { 853 name string 854 gadgetYAML string 855 artifacts *imagedefinition.Artifact 856 img *[]imagedefinition.Img 857 qcow2 *[]imagedefinition.Qcow2 858 expectedVolNames map[string]string 859 shouldPass bool 860 }{ 861 { 862 name: "no artifact ", 863 gadgetYAML: "gadget_tree/meta/gadget.yaml", 864 artifacts: nil, 865 expectedVolNames: nil, 866 shouldPass: true, 867 }, 868 { 869 name: "single_volume_specified", 870 gadgetYAML: "gadget_tree/meta/gadget.yaml", 871 artifacts: &imagedefinition.Artifact{ 872 Img: &[]imagedefinition.Img{ 873 { 874 ImgName: "test1.img", 875 ImgVolume: "pc", 876 }, 877 }, 878 }, 879 expectedVolNames: map[string]string{ 880 "pc": "test1.img", 881 }, 882 shouldPass: true, 883 }, 884 { 885 name: "single_volume_not_specified", 886 gadgetYAML: "gadget_tree/meta/gadget.yaml", 887 artifacts: &imagedefinition.Artifact{ 888 Img: &[]imagedefinition.Img{ 889 { 890 ImgName: "test-single.img", 891 }, 892 }, 893 }, 894 expectedVolNames: map[string]string{ 895 "pc": "test-single.img", 896 }, 897 shouldPass: true, 898 }, 899 { 900 name: "mutli_volume_specified", 901 gadgetYAML: "gadget-multi.yaml", 902 artifacts: &imagedefinition.Artifact{ 903 Img: &[]imagedefinition.Img{ 904 { 905 ImgName: "test1.img", 906 ImgVolume: "first", 907 }, 908 { 909 ImgName: "test2.img", 910 ImgVolume: "second", 911 }, 912 { 913 ImgName: "test3.img", 914 ImgVolume: "third", 915 }, 916 { 917 ImgName: "test4.img", 918 ImgVolume: "fourth", 919 }, 920 }, 921 }, 922 expectedVolNames: map[string]string{ 923 "first": "test1.img", 924 "second": "test2.img", 925 "third": "test3.img", 926 "fourth": "test4.img", 927 }, 928 shouldPass: true, 929 }, 930 { 931 name: "mutli_volume_not_specified", 932 gadgetYAML: "gadget-multi.yaml", 933 artifacts: &imagedefinition.Artifact{ 934 Img: &[]imagedefinition.Img{ 935 { 936 ImgName: "test1.img", 937 }, 938 { 939 ImgName: "test2.img", 940 }, 941 { 942 ImgName: "test3.img", 943 }, 944 { 945 ImgName: "test4.img", 946 }, 947 }, 948 }, 949 expectedVolNames: map[string]string{}, 950 shouldPass: false, 951 }, 952 { 953 name: "mutli_volume_some_specified", 954 gadgetYAML: "gadget-multi.yaml", 955 artifacts: &imagedefinition.Artifact{ 956 Img: &[]imagedefinition.Img{ 957 { 958 ImgName: "test1.img", 959 ImgVolume: "first", 960 }, 961 { 962 ImgName: "test2.img", 963 ImgVolume: "second", 964 }, 965 { 966 ImgName: "test3.img", 967 }, 968 { 969 ImgName: "test4.img", 970 }, 971 }, 972 }, 973 expectedVolNames: map[string]string{}, 974 shouldPass: false, 975 }, 976 { 977 name: "mutli_volume_only_create_some_images", 978 gadgetYAML: "gadget-multi.yaml", 979 artifacts: &imagedefinition.Artifact{ 980 Img: &[]imagedefinition.Img{ 981 { 982 ImgName: "test1.img", 983 ImgVolume: "first", 984 }, 985 { 986 ImgName: "test2.img", 987 ImgVolume: "second", 988 }, 989 }, 990 }, 991 expectedVolNames: map[string]string{ 992 "first": "test1.img", 993 "second": "test2.img", 994 }, 995 shouldPass: true, 996 }, 997 { 998 name: "qcow2_single_volume_no_img", 999 gadgetYAML: "gadget_tree/meta/gadget.yaml", 1000 artifacts: &imagedefinition.Artifact{ 1001 Qcow2: &[]imagedefinition.Qcow2{ 1002 { 1003 Qcow2Name: "test1.qcow2", 1004 Qcow2Volume: "pc", 1005 }, 1006 }, 1007 }, 1008 expectedVolNames: map[string]string{ 1009 "pc": "test1.qcow2.img", 1010 }, 1011 shouldPass: true, 1012 }, 1013 { 1014 name: "qcow2_single_volume_not_specified_no_img", 1015 gadgetYAML: "gadget_tree/meta/gadget.yaml", 1016 artifacts: &imagedefinition.Artifact{ 1017 Qcow2: &[]imagedefinition.Qcow2{ 1018 { 1019 Qcow2Name: "test1.qcow2", 1020 }, 1021 }, 1022 }, 1023 expectedVolNames: map[string]string{ 1024 "pc": "test1.qcow2.img", 1025 }, 1026 shouldPass: true, 1027 }, 1028 { 1029 name: "qcow2_single_volume_yes_img", 1030 gadgetYAML: "gadget_tree/meta/gadget.yaml", 1031 artifacts: &imagedefinition.Artifact{ 1032 Img: &[]imagedefinition.Img{ 1033 { 1034 ImgName: "test1.img", 1035 ImgVolume: "pc", 1036 }, 1037 }, 1038 Qcow2: &[]imagedefinition.Qcow2{ 1039 { 1040 Qcow2Name: "test1.img", 1041 Qcow2Volume: "pc", 1042 }, 1043 }, 1044 }, 1045 expectedVolNames: map[string]string{ 1046 "pc": "test1.img", 1047 }, 1048 shouldPass: true, 1049 }, 1050 { 1051 name: "qcow2_mutli_volume_not_specified", 1052 gadgetYAML: "gadget-multi.yaml", 1053 artifacts: &imagedefinition.Artifact{ 1054 Qcow2: &[]imagedefinition.Qcow2{ 1055 { 1056 Qcow2Name: "test1.img", 1057 }, 1058 { 1059 Qcow2Name: "test2.img", 1060 }, 1061 { 1062 Qcow2Name: "test3.img", 1063 }, 1064 { 1065 Qcow2Name: "test4.img", 1066 }, 1067 }, 1068 }, 1069 expectedVolNames: map[string]string{}, 1070 shouldPass: false, 1071 }, 1072 { 1073 name: "qcow2_mutli_volume_no_img", 1074 gadgetYAML: "gadget-multi.yaml", 1075 artifacts: &imagedefinition.Artifact{ 1076 Qcow2: &[]imagedefinition.Qcow2{ 1077 { 1078 Qcow2Name: "test1.qcow2", 1079 Qcow2Volume: "first", 1080 }, 1081 { 1082 Qcow2Name: "test2.qcow2", 1083 Qcow2Volume: "second", 1084 }, 1085 { 1086 Qcow2Name: "test3.qcow2", 1087 Qcow2Volume: "third", 1088 }, 1089 { 1090 Qcow2Name: "test4.qcow2", 1091 Qcow2Volume: "fourth", 1092 }, 1093 }, 1094 }, 1095 expectedVolNames: map[string]string{ 1096 "first": "test1.qcow2.img", 1097 "second": "test2.qcow2.img", 1098 "third": "test3.qcow2.img", 1099 "fourth": "test4.qcow2.img", 1100 }, 1101 shouldPass: true, 1102 }, 1103 { 1104 name: "qcow2_mutli_volume_yes_img", 1105 gadgetYAML: "gadget-multi.yaml", 1106 artifacts: &imagedefinition.Artifact{ 1107 Img: &[]imagedefinition.Img{ 1108 { 1109 ImgName: "test1.img", 1110 ImgVolume: "first", 1111 }, 1112 { 1113 ImgName: "test2.img", 1114 ImgVolume: "second", 1115 }, 1116 { 1117 ImgName: "test3.img", 1118 ImgVolume: "third", 1119 }, 1120 { 1121 ImgName: "test4.img", 1122 ImgVolume: "fourth", 1123 }, 1124 }, 1125 Qcow2: &[]imagedefinition.Qcow2{ 1126 { 1127 Qcow2Name: "test1.img", 1128 Qcow2Volume: "first", 1129 }, 1130 { 1131 Qcow2Name: "test2.img", 1132 Qcow2Volume: "second", 1133 }, 1134 { 1135 Qcow2Name: "test3.img", 1136 Qcow2Volume: "third", 1137 }, 1138 { 1139 Qcow2Name: "test4.img", 1140 Qcow2Volume: "fourth", 1141 }, 1142 }, 1143 }, 1144 expectedVolNames: map[string]string{ 1145 "first": "test1.img", 1146 "second": "test2.img", 1147 "third": "test3.img", 1148 "fourth": "test4.img", 1149 }, 1150 shouldPass: true, 1151 }, 1152 { 1153 name: "qcow2_mutli_volume_img_for_different_volume", 1154 gadgetYAML: "gadget-multi.yaml", 1155 artifacts: &imagedefinition.Artifact{ 1156 Img: &[]imagedefinition.Img{ 1157 { 1158 ImgName: "test1.img", 1159 ImgVolume: "first", 1160 }, 1161 { 1162 ImgName: "test2.img", 1163 ImgVolume: "second", 1164 }, 1165 }, 1166 Qcow2: &[]imagedefinition.Qcow2{ 1167 { 1168 Qcow2Name: "test3.qcow2", 1169 Qcow2Volume: "third", 1170 }, 1171 { 1172 Qcow2Name: "test4.qcow2", 1173 Qcow2Volume: "fourth", 1174 }, 1175 }, 1176 }, 1177 expectedVolNames: map[string]string{ 1178 "first": "test1.img", 1179 "second": "test2.img", 1180 "third": "test3.qcow2.img", 1181 "fourth": "test4.qcow2.img", 1182 }, 1183 shouldPass: true, 1184 }, 1185 } 1186 for _, tc := range testCases { 1187 t.Run(tc.name, func(t *testing.T) { 1188 asserter := helper.Asserter{T: t} 1189 restoreCWD := testhelper.SaveCWD() 1190 defer restoreCWD() 1191 1192 var stateMachine ClassicStateMachine 1193 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1194 stateMachine.parent = &stateMachine 1195 1196 stateMachine.YamlFilePath = filepath.Join("testdata", tc.gadgetYAML) 1197 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 1198 Architecture: getHostArch(), 1199 Series: getHostSuite(), 1200 Rootfs: &imagedefinition.Rootfs{ 1201 Archive: "ubuntu", 1202 }, 1203 Customization: &imagedefinition.Customization{}, 1204 Artifacts: tc.artifacts, 1205 } 1206 1207 err := stateMachine.makeTemporaryDirectories() 1208 asserter.AssertErrNil(err, true) 1209 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 1210 1211 // load gadget yaml 1212 err = stateMachine.loadGadgetYaml() 1213 asserter.AssertErrNil(err, true) 1214 1215 // verify artifact names 1216 err = stateMachine.verifyArtifactNames() 1217 if tc.shouldPass { 1218 asserter.AssertErrNil(err, true) 1219 if !reflect.DeepEqual(tc.expectedVolNames, stateMachine.VolumeNames) { 1220 fmt.Println(tc.expectedVolNames) 1221 fmt.Println(stateMachine.VolumeNames) 1222 t.Errorf("Expected volume names does not match calculated volume names") 1223 } 1224 } else { 1225 asserter.AssertErrContains(err, "Volume names must be specified for each image") 1226 } 1227 }) 1228 } 1229 } 1230 1231 // TestBuildRootfsFromTasks unit tests the buildRootfsFromTasks function 1232 func TestBuildRootfsFromTasks(t *testing.T) { 1233 t.Parallel() 1234 asserter := helper.Asserter{T: t} 1235 restoreCWD := testhelper.SaveCWD() 1236 defer restoreCWD() 1237 1238 var stateMachine ClassicStateMachine 1239 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1240 1241 err := stateMachine.buildRootfsFromTasks() 1242 asserter.AssertErrNil(err, true) 1243 1244 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 1245 } 1246 1247 // TestExtractRootfsTar unit tests the extractRootfsTar function 1248 func TestExtractRootfsTar(t *testing.T) { 1249 t.Parallel() 1250 wd, _ := os.Getwd() // nolint: errcheck 1251 testCases := []struct { 1252 name string 1253 rootfsTar string 1254 SHA256sum string 1255 expectedFiles []string 1256 }{ 1257 { 1258 name: "vanilla_tar", 1259 rootfsTar: filepath.Join("testdata", "rootfs_tarballs", "rootfs.tar"), 1260 SHA256sum: "ec01fd8488b0f35d2ca69e6f82edfaecef5725da70913bab61240419ce574918", 1261 expectedFiles: []string{ 1262 "test_tar1", 1263 "test_tar2", 1264 }, 1265 }, 1266 { 1267 name: "vanilla_tar respecting absolute path", 1268 rootfsTar: filepath.Join(wd, "testdata", "rootfs_tarballs", "rootfs.tar"), 1269 SHA256sum: "ec01fd8488b0f35d2ca69e6f82edfaecef5725da70913bab61240419ce574918", 1270 expectedFiles: []string{ 1271 "test_tar1", 1272 "test_tar2", 1273 }, 1274 }, 1275 { 1276 name: "vanilla_tar relative path even with dot dot", 1277 rootfsTar: filepath.Join("testdata", "../..", filepath.Base(wd), "testdata", "rootfs_tarballs", "rootfs.tar"), 1278 SHA256sum: "ec01fd8488b0f35d2ca69e6f82edfaecef5725da70913bab61240419ce574918", 1279 expectedFiles: []string{ 1280 "test_tar1", 1281 "test_tar2", 1282 }, 1283 }, 1284 { 1285 name: "gz", 1286 rootfsTar: filepath.Join("testdata", "rootfs_tarballs", "rootfs.tar.gz"), 1287 SHA256sum: "29152fd9cadbc92f174815ec642ab3aea98f08f902a4f317ec037f8fe60e40c3", 1288 expectedFiles: []string{ 1289 "test_tar_gz1", 1290 "test_tar_gz2", 1291 }, 1292 }, 1293 { 1294 name: "xz", 1295 rootfsTar: filepath.Join("testdata", "rootfs_tarballs", "rootfs.tar.xz"), 1296 SHA256sum: "e3708f1d98ccea0e0c36843d9576580505ee36d523bfcf78b0f73a035ae9a14e", 1297 expectedFiles: []string{ 1298 "test_tar_xz1", 1299 "test_tar_xz2", 1300 }, 1301 }, 1302 { 1303 name: "bz2", 1304 rootfsTar: filepath.Join("testdata", "rootfs_tarballs", "rootfs.tar.bz2"), 1305 SHA256sum: "a1180a73b652d85d7330ef21d433b095363664f2f808363e67f798fae15abf0c", 1306 expectedFiles: []string{ 1307 "test_tar_bz1", 1308 "test_tar_bz2", 1309 }, 1310 }, 1311 { 1312 name: "zst", 1313 rootfsTar: filepath.Join("testdata", "rootfs_tarballs", "rootfs.tar.zst"), 1314 SHA256sum: "5fb00513f84e28225a3155fd78c59a6a923b222e1c125aab35bbfd4091281829", 1315 expectedFiles: []string{ 1316 "test_tar_zstd1", 1317 "test_tar_zstd2", 1318 }, 1319 }, 1320 } 1321 for _, tc := range testCases { 1322 t.Run(tc.name, func(t *testing.T) { 1323 asserter := helper.Asserter{T: t} 1324 restoreCWD := testhelper.SaveCWD() 1325 defer restoreCWD() 1326 1327 var stateMachine ClassicStateMachine 1328 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1329 stateMachine.parent = &stateMachine 1330 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 1331 Architecture: getHostArch(), 1332 Series: getHostSuite(), 1333 Rootfs: &imagedefinition.Rootfs{ 1334 Tarball: &imagedefinition.Tarball{ 1335 TarballURL: fmt.Sprintf("file://%s", tc.rootfsTar), 1336 }, 1337 }, 1338 } 1339 1340 err := stateMachine.setConfDefDir(filepath.Join(wd, "image_definition.yaml")) 1341 asserter.AssertErrNil(err, true) 1342 1343 err = stateMachine.makeTemporaryDirectories() 1344 asserter.AssertErrNil(err, true) 1345 1346 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 1347 1348 err = stateMachine.extractRootfsTar() 1349 asserter.AssertErrNil(err, true) 1350 1351 for _, testFile := range tc.expectedFiles { 1352 _, err := os.Stat(filepath.Join(stateMachine.tempDirs.chroot, testFile)) 1353 if err != nil { 1354 t.Errorf("File %s should be in chroot, but is missing", testFile) 1355 } 1356 } 1357 }) 1358 } 1359 } 1360 1361 // TestFailedExtractRootfsTar tests failures in the extractRootfsTar function 1362 func TestFailedExtractRootfsTar(t *testing.T) { 1363 asserter := helper.Asserter{T: t} 1364 restoreCWD := testhelper.SaveCWD() 1365 defer restoreCWD() 1366 1367 var stateMachine ClassicStateMachine 1368 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1369 stateMachine.parent = &stateMachine 1370 tarPath := filepath.Join("testdata", "rootfs_tarballs", "rootfs.tar") 1371 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 1372 Architecture: getHostArch(), 1373 Series: getHostSuite(), 1374 Rootfs: &imagedefinition.Rootfs{ 1375 Tarball: &imagedefinition.Tarball{ 1376 TarballURL: fmt.Sprintf("file://%s", tarPath), 1377 SHA256sum: "fail", 1378 }, 1379 }, 1380 } 1381 1382 err := stateMachine.makeTemporaryDirectories() 1383 asserter.AssertErrNil(err, true) 1384 1385 // mock os.Mkdir 1386 osMkdir = mockMkdir 1387 t.Cleanup(func() { 1388 osMkdir = os.Mkdir 1389 }) 1390 err = stateMachine.extractRootfsTar() 1391 asserter.AssertErrContains(err, "Failed to create chroot directory") 1392 osMkdir = os.Mkdir 1393 1394 // clean up chroot directory 1395 os.RemoveAll(stateMachine.tempDirs.chroot) 1396 1397 // now test with the incorrect SHA256sum 1398 err = stateMachine.extractRootfsTar() 1399 asserter.AssertErrContains(err, "Calculated SHA256 sum of rootfs tarball") 1400 1401 // clean up chroot directory 1402 os.RemoveAll(stateMachine.tempDirs.chroot) 1403 1404 // use a tarball that doesn't exist to trigger a failure in computing 1405 // the SHA256 sum 1406 stateMachine.ImageDef.Rootfs.Tarball.TarballURL = "file:///fakefile" 1407 err = stateMachine.extractRootfsTar() 1408 asserter.AssertErrContains(err, "Error opening file \"/fakefile\" to calculate SHA256 sum") 1409 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 1410 } 1411 1412 // TestStateMachine_customizeCloudInit unit tests the customizeCloudInit method 1413 func TestStateMachine_customizeCloudInit(t *testing.T) { 1414 testCases := []struct { 1415 name string 1416 cloudInitCustomization imagedefinition.CloudInit 1417 wantMetaData string 1418 wantUserData string 1419 wantNetworkConfig string 1420 }{ 1421 { 1422 name: "full cloudinit conf", 1423 cloudInitCustomization: imagedefinition.CloudInit{ 1424 MetaData: `#cloud-config 1425 1426 foo: bar`, 1427 UserData: `#cloud-config 1428 1429 foo: baz`, 1430 NetworkConfig: `#cloud-config 1431 1432 foobar: foobar`, 1433 }, 1434 wantMetaData: `#cloud-config 1435 1436 foo: bar`, 1437 wantUserData: `#cloud-config 1438 1439 foo: baz`, 1440 wantNetworkConfig: `#cloud-config 1441 1442 foobar: foobar`, 1443 }, 1444 { 1445 name: "empty user data", 1446 cloudInitCustomization: imagedefinition.CloudInit{ 1447 MetaData: `#cloud-config 1448 1449 foo: bar`, 1450 UserData: "", 1451 NetworkConfig: `#cloud-config 1452 1453 foobar: foobar`, 1454 }, 1455 wantMetaData: `#cloud-config 1456 1457 foo: bar`, 1458 wantUserData: "", 1459 wantNetworkConfig: `#cloud-config 1460 1461 foobar: foobar`, 1462 }, 1463 { 1464 name: "empty metadata", 1465 cloudInitCustomization: imagedefinition.CloudInit{ 1466 UserData: "", 1467 NetworkConfig: `#cloud-config 1468 1469 foobar: foobar`, 1470 }, 1471 wantMetaData: "", 1472 wantUserData: "", 1473 wantNetworkConfig: `#cloud-config 1474 1475 foobar: foobar`, 1476 }, 1477 { 1478 name: "multiline user data", 1479 cloudInitCustomization: imagedefinition.CloudInit{ 1480 UserData: `#cloud-config 1481 1482 chpasswd: 1483 expire: true 1484 users: 1485 - name: ubuntu 1486 password: ubuntu 1487 type: text 1488 `, 1489 }, 1490 wantMetaData: "", 1491 wantUserData: `#cloud-config 1492 1493 chpasswd: 1494 expire: true 1495 users: 1496 - name: ubuntu 1497 password: ubuntu 1498 type: text 1499 `, 1500 wantNetworkConfig: "", 1501 }, 1502 } 1503 1504 for i, tc := range testCases { 1505 t.Run("test_customize_cloud_init_"+tc.name, func(t *testing.T) { 1506 // Test setup 1507 asserter := helper.Asserter{T: t} 1508 restoreCWD := testhelper.SaveCWD() 1509 defer restoreCWD() 1510 1511 var stateMachine ClassicStateMachine 1512 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1513 stateMachine.parent = &stateMachine 1514 tmpDir, err := os.MkdirTemp("", "") 1515 asserter.AssertErrNil(err, true) 1516 t.Cleanup(func() { 1517 if tmpErr := osRemoveAll(tmpDir); tmpErr != nil { 1518 if err != nil { 1519 err = fmt.Errorf("%s after previous error: %w", tmpErr, err) 1520 } else { 1521 err = tmpErr 1522 } 1523 } 1524 }) 1525 stateMachine.tempDirs.chroot = tmpDir 1526 1527 // this directory is expected to be present as it is installed by cloud-init 1528 err = os.MkdirAll(path.Join(tmpDir, "etc/cloud/cloud.cfg.d"), 0777) 1529 asserter.AssertErrNil(err, true) 1530 1531 stateMachine.ImageDef.Customization = &imagedefinition.Customization{ 1532 CloudInit: &testCases[i].cloudInitCustomization, 1533 } 1534 1535 // Running function to test 1536 err = stateMachine.customizeCloudInit() 1537 asserter.AssertErrNil(err, true) 1538 1539 // Validation 1540 seedPath := path.Join(tmpDir, "var/lib/cloud/seed/nocloud") 1541 1542 metaDataFile, err := os.Open(path.Join(seedPath, "meta-data")) 1543 if tc.cloudInitCustomization.MetaData != "" { 1544 asserter.AssertErrNil(err, false) 1545 1546 metaDataFileContent, err := io.ReadAll(metaDataFile) 1547 asserter.AssertErrNil(err, false) 1548 1549 if string(metaDataFileContent) != tc.wantMetaData { 1550 t.Errorf("un-expected meta-data content found: expected:\n%v\ngot:%v", tc.wantMetaData, string(metaDataFileContent)) 1551 } 1552 } else { 1553 asserter.AssertErrContains(err, "no such file or directory") 1554 } 1555 1556 networkConfigFile, err := os.Open(path.Join(seedPath, "network-config")) 1557 if tc.cloudInitCustomization.NetworkConfig != "" { 1558 asserter.AssertErrNil(err, false) 1559 1560 networkConfigFileContent, err := io.ReadAll(networkConfigFile) 1561 asserter.AssertErrNil(err, false) 1562 if string(networkConfigFileContent) != tc.wantNetworkConfig { 1563 t.Errorf("un-expected network-config found: expected:\n%v\ngot:%v", tc.wantNetworkConfig, string(networkConfigFileContent)) 1564 } 1565 } else { 1566 asserter.AssertErrContains(err, "no such file or directory") 1567 } 1568 1569 userDataFile, err := os.Open(path.Join(seedPath, "user-data")) 1570 if tc.cloudInitCustomization.UserData != "" { 1571 asserter.AssertErrNil(err, false) 1572 1573 userDataFileContent, err := io.ReadAll(userDataFile) 1574 asserter.AssertErrNil(err, false) 1575 1576 if string(userDataFileContent) != tc.wantUserData { 1577 t.Errorf("un-expected user-data content found: expected:\n%v\ngot:%v", tc.wantUserData, string(userDataFileContent)) 1578 } 1579 } else { 1580 asserter.AssertErrContains(err, "no such file or directory") 1581 } 1582 1583 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 1584 }) 1585 } 1586 } 1587 1588 // TestStatemachine_customizeCloudInit_failed tests failure modes of customizeCloudInit method 1589 func TestStatemachine_customizeCloudInit_failed(t *testing.T) { 1590 asserter := helper.Asserter{T: t} 1591 restoreCWD := testhelper.SaveCWD() 1592 defer restoreCWD() 1593 1594 var stateMachine ClassicStateMachine 1595 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1596 stateMachine.parent = &stateMachine 1597 tmpDir, err := os.MkdirTemp("", "") 1598 asserter.AssertErrNil(err, true) 1599 t.Cleanup(func() { os.RemoveAll(tmpDir) }) 1600 stateMachine.tempDirs.chroot = tmpDir 1601 1602 stateMachine.ImageDef.Customization = &imagedefinition.Customization{ 1603 CloudInit: &imagedefinition.CloudInit{ 1604 MetaData: `foo: bar`, 1605 NetworkConfig: `foobar: foobar`, 1606 UserData: `#cloud-config 1607 1608 chpasswd: 1609 expire: true 1610 users: 1611 - name: ubuntu 1612 password: ubuntu 1613 type: text 1614 `, 1615 }, 1616 } 1617 1618 // Test if osCreate fails 1619 fileList := []string{"meta-data", "user-data", "network-config", "90_dpkg.cfg"} 1620 for _, file := range fileList { 1621 t.Run("test_failed_customize_cloud_init_"+file, func(t *testing.T) { 1622 // this directory is expected to be present as it is installed by cloud-init 1623 cloudInitConfigDirPath := path.Join(tmpDir, "etc/cloud/cloud.cfg.d") 1624 err = os.MkdirAll(cloudInitConfigDirPath, 0777) 1625 asserter.AssertErrNil(err, true) 1626 t.Cleanup(func() { 1627 os.RemoveAll(cloudInitConfigDirPath) 1628 }) 1629 1630 osCreate = func(name string) (*os.File, error) { 1631 if strings.Contains(name, file) { 1632 return nil, errors.New("test error: failed to create file") 1633 } 1634 return os.Create(name) 1635 } 1636 1637 err := stateMachine.customizeCloudInit() 1638 asserter.AssertErrContains(err, "test error: failed to create file") 1639 }) 1640 } 1641 1642 // Test if Write fails (file is read only) 1643 for _, file := range fileList { 1644 t.Run("test_failed_customize_cloud_init_"+file, func(t *testing.T) { 1645 // this directory is expected to be present as it is installed by cloud-init 1646 cloudInitConfigDirPath := path.Join(tmpDir, "etc/cloud/cloud.cfg.d") 1647 err = os.MkdirAll(cloudInitConfigDirPath, 0777) 1648 asserter.AssertErrNil(err, true) 1649 t.Cleanup(func() { 1650 os.RemoveAll(cloudInitConfigDirPath) 1651 }) 1652 1653 osCreate = func(name string) (*os.File, error) { 1654 if strings.Contains(name, file) { 1655 fileReadWrite, err := os.Create(name) 1656 asserter.AssertErrNil(err, true) 1657 fileReadWrite.Close() 1658 return os.Open(name) 1659 } 1660 return os.Create(name) 1661 } 1662 1663 err := stateMachine.customizeCloudInit() 1664 if err == nil { 1665 t.Errorf("expected error but got nil") 1666 } 1667 }) 1668 } 1669 1670 // Test if os.MkdirAll fails 1671 t.Run("test_failed_customize_cloud_init_mkdir", func(t *testing.T) { 1672 // this directory is expected to be present as it is installed by cloud-init 1673 cloudInitConfigDirPath := path.Join(tmpDir, "etc/cloud/cloud.cfg.d") 1674 err = os.MkdirAll(cloudInitConfigDirPath, 0777) 1675 asserter.AssertErrNil(err, true) 1676 t.Cleanup(func() { 1677 os.RemoveAll(cloudInitConfigDirPath) 1678 }) 1679 1680 osMkdirAll = mockMkdirAll 1681 t.Cleanup(func() { 1682 osMkdirAll = os.MkdirAll 1683 }) 1684 1685 err := stateMachine.customizeCloudInit() 1686 if err == nil { 1687 t.Error() 1688 } 1689 }) 1690 1691 // Test if yaml.Marshal fails 1692 t.Run("test_failed_customize_cloud_init_yaml_marshal", func(t *testing.T) { 1693 // this directory is expected to be present as it is installed by cloud-init 1694 cloudInitConfigDirPath := path.Join(tmpDir, "etc/cloud/cloud.cfg.d") 1695 err = os.MkdirAll(cloudInitConfigDirPath, 0777) 1696 asserter.AssertErrNil(err, true) 1697 t.Cleanup(func() { 1698 os.RemoveAll(cloudInitConfigDirPath) 1699 }) 1700 1701 yamlMarshal = mockMarshal 1702 defer func() { 1703 yamlMarshal = yaml.Marshal 1704 }() 1705 1706 err := stateMachine.customizeCloudInit() 1707 if err == nil { 1708 t.Error() 1709 } 1710 }) 1711 1712 // Test cloud-init customization is invalid 1713 testCases := []struct { 1714 name string 1715 cloudInitCustomization imagedefinition.CloudInit 1716 }{ 1717 { 1718 name: "invalid userdata", 1719 cloudInitCustomization: imagedefinition.CloudInit{ 1720 UserData: "foo: bar", 1721 }, 1722 }, 1723 } 1724 1725 for i, tc := range testCases { 1726 t.Run("test_failed_customize_cloud_init_invalid_config_"+tc.name, func(t *testing.T) { 1727 // this directory is expected to be present as it is installed by cloud-init 1728 cloudInitConfigDirPath := path.Join(tmpDir, "etc/cloud/cloud.cfg.d") 1729 err = os.MkdirAll(cloudInitConfigDirPath, 0777) 1730 asserter.AssertErrNil(err, true) 1731 t.Cleanup(func() { 1732 os.RemoveAll(cloudInitConfigDirPath) 1733 }) 1734 1735 stateMachine.ImageDef.Customization.CloudInit = &testCases[i].cloudInitCustomization 1736 1737 err := stateMachine.customizeCloudInit() 1738 asserter.AssertErrContains(err, "is missing proper header") 1739 }) 1740 } 1741 } 1742 1743 // TestStateMachine_manualCustomization unit tests the manualCustomization function 1744 func TestStateMachine_manualCustomization(t *testing.T) { 1745 t.Parallel() 1746 if testing.Short() { 1747 t.Skip("skipping test in short mode.") 1748 } 1749 1750 asserter := helper.Asserter{T: t} 1751 restoreCWD := testhelper.SaveCWD() 1752 defer restoreCWD() 1753 1754 var stateMachine ClassicStateMachine 1755 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1756 stateMachine.parent = &stateMachine 1757 1758 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 1759 Architecture: getHostArch(), 1760 Series: getHostSuite(), 1761 Rootfs: &imagedefinition.Rootfs{ 1762 Archive: "ubuntu", 1763 }, 1764 Customization: &imagedefinition.Customization{ 1765 Manual: &imagedefinition.Manual{ 1766 MakeDirs: []*imagedefinition.MakeDirs{ 1767 { 1768 Path: "/etc/foo/bar", 1769 Permissions: 0755, 1770 }, 1771 { 1772 Path: "/etc/baz/test", 1773 Permissions: 0644, 1774 }, 1775 }, 1776 CopyFile: []*imagedefinition.CopyFile{ 1777 { 1778 Source: filepath.Join("testdata", "test_script"), 1779 Dest: "/test_copy_file", 1780 }, 1781 }, 1782 TouchFile: []*imagedefinition.TouchFile{ 1783 { 1784 TouchPath: "/test_touch_file", 1785 }, 1786 }, 1787 Execute: []*imagedefinition.Execute{ 1788 { 1789 // the file we already copied creates a file /test_execute 1790 ExecutePath: "/test_copy_file", 1791 }, 1792 }, 1793 AddUser: []*imagedefinition.AddUser{ 1794 { 1795 UserName: "testuser", 1796 UserID: "123456", 1797 }, 1798 }, 1799 AddGroup: []*imagedefinition.AddGroup{ 1800 { 1801 GroupName: "testgroup", 1802 GroupID: "456789", 1803 }, 1804 }, 1805 }, 1806 }, 1807 } 1808 1809 d, err := os.Getwd() 1810 asserter.AssertErrNil(err, true) 1811 err = stateMachine.setConfDefDir(filepath.Join(d, "image_definition.yaml")) 1812 asserter.AssertErrNil(err, true) 1813 1814 err = stateMachine.makeTemporaryDirectories() 1815 asserter.AssertErrNil(err, true) 1816 1817 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 1818 1819 err = getBasicChroot(stateMachine.StateMachine) 1820 asserter.AssertErrNil(err, true) 1821 1822 err = stateMachine.manualCustomization() 1823 asserter.AssertErrNil(err, true) 1824 1825 // Check that the correct directories exist 1826 testDirectories := []string{"/etc/foo/bar", "/etc/baz/test"} 1827 for _, dirName := range testDirectories { 1828 _, err := os.Stat(filepath.Join(stateMachine.tempDirs.chroot, dirName)) 1829 if err != nil { 1830 t.Errorf("directory %s should exist, but it does not", dirName) 1831 } 1832 } 1833 1834 // Check that the correct files exist 1835 testFiles := []string{"test_copy_file", "test_touch_file", "test_execute"} 1836 for _, fileName := range testFiles { 1837 _, err := os.Stat(filepath.Join(stateMachine.tempDirs.chroot, fileName)) 1838 if err != nil { 1839 t.Errorf("file %s should exist, but it does not", fileName) 1840 } 1841 } 1842 1843 // Check that the test user exists with the correct uid 1844 passwdFile := filepath.Join(stateMachine.tempDirs.chroot, "etc", "passwd") 1845 passwdContents, err := os.ReadFile(passwdFile) 1846 asserter.AssertErrNil(err, true) 1847 if !strings.Contains(string(passwdContents), "testuser:x:123456") { 1848 t.Errorf("Test user was not created in the chroot") 1849 } 1850 1851 // Check that the test group exists with the correct gid 1852 groupFile := filepath.Join(stateMachine.tempDirs.chroot, "etc", "group") 1853 groupContents, err := os.ReadFile(groupFile) 1854 asserter.AssertErrNil(err, true) 1855 if !strings.Contains(string(groupContents), "testgroup:x:456789") { 1856 t.Errorf("Test group was not created in the chroot") 1857 } 1858 } 1859 1860 // TestStateMachine_manualCustomization_fail tests failures in the manualCustomization function 1861 func TestStateMachine_manualCustomization_fail(t *testing.T) { 1862 if testing.Short() { 1863 t.Skip("skipping test in short mode.") 1864 } 1865 1866 t.Run("test_failed_manual_customization", func(t *testing.T) { 1867 asserter := helper.Asserter{T: t} 1868 restoreCWD := testhelper.SaveCWD() 1869 t.Cleanup(restoreCWD) 1870 1871 var stateMachine ClassicStateMachine 1872 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1873 stateMachine.parent = &stateMachine 1874 1875 err := stateMachine.makeTemporaryDirectories() 1876 asserter.AssertErrNil(err, true) 1877 1878 // mock helper.BackupAndCopyResolvConf 1879 helperBackupAndCopyResolvConf = mockBackupAndCopyResolvConfFail 1880 t.Cleanup(func() { 1881 helperBackupAndCopyResolvConf = helper.BackupAndCopyResolvConf 1882 }) 1883 err = stateMachine.manualCustomization() 1884 asserter.AssertErrContains(err, "Error setting up /etc/resolv.conf") 1885 }) 1886 1887 tests := []struct { 1888 name string 1889 expectedErr string 1890 manualCustomizations *imagedefinition.Manual 1891 }{ 1892 { 1893 name: "failing manualMakeDirs", 1894 expectedErr: "not a directory", 1895 manualCustomizations: &imagedefinition.Manual{ 1896 MakeDirs: []*imagedefinition.MakeDirs{ 1897 { 1898 Path: filepath.Join("/etc", "resolv.conf"), 1899 Permissions: 0755, 1900 }, 1901 }, 1902 }, 1903 }, 1904 { 1905 name: "failing manualCopyFile", 1906 expectedErr: "cp: cannot stat 'this/path/does/not/exist'", 1907 manualCustomizations: &imagedefinition.Manual{ 1908 CopyFile: []*imagedefinition.CopyFile{ 1909 { 1910 Source: filepath.Join("this", "path", "does", "not", "exist"), 1911 Dest: filepath.Join("this", "path", "does", "not", "exist"), 1912 }, 1913 }, 1914 }, 1915 }, 1916 { 1917 name: "failing manualExecute", 1918 expectedErr: "chroot: failed to run command", 1919 manualCustomizations: &imagedefinition.Manual{ 1920 Execute: []*imagedefinition.Execute{ 1921 { 1922 ExecutePath: filepath.Join("this", "path", "does", "not", "exist"), 1923 }, 1924 }, 1925 }, 1926 }, 1927 { 1928 name: "failing manualTouchFile", 1929 expectedErr: "no such file or directory", 1930 manualCustomizations: &imagedefinition.Manual{ 1931 TouchFile: []*imagedefinition.TouchFile{ 1932 { 1933 TouchPath: filepath.Join("this", "path", "does", "not", "exist"), 1934 }, 1935 }, 1936 }, 1937 }, 1938 { 1939 name: "failing manualAddGroup", 1940 expectedErr: "group 'root' already exists", 1941 manualCustomizations: &imagedefinition.Manual{ 1942 AddGroup: []*imagedefinition.AddGroup{ 1943 { 1944 GroupName: "root", 1945 GroupID: "0", 1946 }, 1947 }, 1948 }, 1949 }, 1950 { 1951 name: "failing manualAddUser", 1952 expectedErr: "user 'root' already exists", 1953 manualCustomizations: &imagedefinition.Manual{ 1954 AddUser: []*imagedefinition.AddUser{ 1955 { 1956 UserName: "root", 1957 UserID: "0", 1958 }, 1959 }, 1960 }, 1961 }, 1962 } 1963 asserter := helper.Asserter{T: t} 1964 1965 var stateMachine ClassicStateMachine 1966 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1967 stateMachine.parent = &stateMachine 1968 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 1969 Architecture: getHostArch(), 1970 Series: getHostSuite(), 1971 Rootfs: &imagedefinition.Rootfs{ 1972 Archive: "ubuntu", 1973 }, 1974 } 1975 1976 err := stateMachine.makeTemporaryDirectories() 1977 asserter.AssertErrNil(err, true) 1978 1979 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 1980 1981 err = getBasicChroot(stateMachine.StateMachine) 1982 asserter.AssertErrNil(err, true) 1983 1984 // create an /etc/resolv.conf in the chroot 1985 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "etc"), 0755) 1986 asserter.AssertErrNil(err, true) 1987 _, err = os.Create(filepath.Join(stateMachine.tempDirs.chroot, "etc", "resolv.conf")) 1988 asserter.AssertErrNil(err, true) 1989 1990 for _, tc := range tests { 1991 t.Run(tc.name, func(t *testing.T) { 1992 asserter := helper.Asserter{T: t} 1993 restoreCWD := testhelper.SaveCWD() 1994 t.Cleanup(restoreCWD) 1995 1996 stateMachine.ImageDef.Customization = &imagedefinition.Customization{ 1997 Manual: tc.manualCustomizations, 1998 } 1999 2000 err = stateMachine.manualCustomization() 2001 2002 if len(tc.expectedErr) == 0 { 2003 asserter.AssertErrNil(err, true) 2004 } else { 2005 asserter.AssertErrContains(err, tc.expectedErr) 2006 } 2007 }) 2008 } 2009 } 2010 2011 // TestPrepareClassicImage unit tests the prepareClassicImage function 2012 func TestPrepareClassicImage(t *testing.T) { 2013 t.Parallel() 2014 if testing.Short() { 2015 t.Skip("skipping test in short mode.") 2016 } 2017 2018 asserter := helper.Asserter{T: t} 2019 restoreCWD := testhelper.SaveCWD() 2020 defer restoreCWD() 2021 2022 var stateMachine ClassicStateMachine 2023 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2024 stateMachine.parent = &stateMachine 2025 stateMachine.Snaps = []string{"core20"} 2026 stateMachine.commonFlags.Channel = "stable" 2027 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2028 Architecture: getHostArch(), 2029 Customization: &imagedefinition.Customization{ 2030 ExtraSnaps: []*imagedefinition.Snap{ 2031 { 2032 SnapName: "hello", 2033 Channel: "candidate", 2034 }, 2035 { 2036 SnapName: "lxd", 2037 Channel: "latest/stable", 2038 }, 2039 { 2040 SnapName: "core22", 2041 }, 2042 }, 2043 }, 2044 } 2045 2046 err := stateMachine.makeTemporaryDirectories() 2047 asserter.AssertErrNil(err, true) 2048 2049 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 2050 2051 err = stateMachine.prepareClassicImage() 2052 asserter.AssertErrNil(err, true) 2053 2054 // check that the lxd and hello snaps, as well as lxd's base, core20 2055 // were prepared in the correct location 2056 snaps := map[string]string{"lxd": "stable", "hello": "candidate", "core20": "stable", "core22": "stable"} 2057 for snapName, snapChannel := range snaps { 2058 // reach out to the snap store to find the revision 2059 // of the snap for the specified channel 2060 snapStore := store.New(nil, nil) 2061 snapSpec := store.SnapSpec{Name: snapName} 2062 context := context.TODO() 2063 snapInfo, err := snapStore.SnapInfo(context, snapSpec, nil) 2064 asserter.AssertErrNil(err, true) 2065 2066 storeRevision := snapInfo.Channels["latest/"+snapChannel].Revision.N 2067 snapFileName := fmt.Sprintf("%s_%d.snap", snapName, storeRevision) 2068 2069 snapPath := filepath.Join(stateMachine.tempDirs.chroot, 2070 "var", "lib", "snapd", "seed", "snaps", snapFileName) 2071 _, err = os.Stat(snapPath) 2072 if err != nil { 2073 if os.IsNotExist(err) { 2074 t.Errorf("File %s should exist, but does not", snapPath) 2075 } 2076 } 2077 } 2078 } 2079 2080 // TestClassicSnapRevisions tests that if revisions are specified in the image definition 2081 // that the corresponding revisions are staged in the chroot 2082 func TestClassicSnapRevisions(t *testing.T) { 2083 t.Parallel() 2084 if testing.Short() { 2085 t.Skip("skipping test in short mode.") 2086 } 2087 if runtime.GOARCH != "amd64" { 2088 t.Skip("Test for amd64 only") 2089 } 2090 asserter := helper.Asserter{T: t} 2091 restoreCWD := testhelper.SaveCWD() 2092 defer restoreCWD() 2093 2094 var stateMachine ClassicStateMachine 2095 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2096 stateMachine.parent = &stateMachine 2097 stateMachine.Snaps = []string{"lxd"} 2098 stateMachine.commonFlags.Channel = "stable" 2099 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2100 Architecture: getHostArch(), 2101 Customization: &imagedefinition.Customization{ 2102 ExtraSnaps: []*imagedefinition.Snap{ 2103 { 2104 SnapName: "hello", 2105 SnapRevision: 38, 2106 }, 2107 { 2108 SnapName: "ubuntu-image", 2109 SnapRevision: 330, 2110 }, 2111 { 2112 SnapName: "core20", 2113 SnapRevision: 1852, 2114 }, 2115 }, 2116 }, 2117 } 2118 2119 err := stateMachine.makeTemporaryDirectories() 2120 asserter.AssertErrNil(err, true) 2121 2122 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 2123 2124 err = stateMachine.prepareClassicImage() 2125 asserter.AssertErrNil(err, true) 2126 2127 for _, snapInfo := range stateMachine.ImageDef.Customization.ExtraSnaps { 2128 // compile a regex used to get revision numbers from seed.manifest 2129 revRegex, err := regexp.Compile(fmt.Sprintf("%s_(.*?).snap\n", snapInfo.SnapName)) 2130 asserter.AssertErrNil(err, true) 2131 seedData, err := os.ReadFile(filepath.Join( 2132 stateMachine.tempDirs.chroot, 2133 "var", 2134 "lib", 2135 "snapd", 2136 "seed", 2137 "seed.yaml", 2138 )) 2139 asserter.AssertErrNil(err, true) 2140 revString := revRegex.FindStringSubmatch(string(seedData)) 2141 if len(revString) != 2 { 2142 t.Fatal("Error finding snap revision via regex") 2143 } 2144 seededRevision, err := strconv.Atoi(revString[1]) 2145 asserter.AssertErrNil(err, true) 2146 2147 if seededRevision != snapInfo.SnapRevision { 2148 t.Errorf("Error, expected snap %s to "+ 2149 "be revision %d, but it was %d", 2150 snapInfo.SnapName, snapInfo.SnapRevision, seededRevision) 2151 } 2152 } 2153 } 2154 2155 // TestFailedPrepareClassicImage tests failures in the prepareClassicImage function 2156 func TestFailedPrepareClassicImage(t *testing.T) { 2157 if testing.Short() { 2158 t.Skip("skipping test in short mode.") 2159 } 2160 asserter := helper.Asserter{T: t} 2161 restoreCWD := testhelper.SaveCWD() 2162 defer restoreCWD() 2163 2164 var stateMachine ClassicStateMachine 2165 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2166 stateMachine.parent = &stateMachine 2167 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2168 Architecture: getHostArch(), 2169 Customization: &imagedefinition.Customization{ 2170 ExtraSnaps: []*imagedefinition.Snap{}, 2171 }, 2172 } 2173 2174 err := stateMachine.makeTemporaryDirectories() 2175 asserter.AssertErrNil(err, true) 2176 2177 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 2178 2179 // include an invalid snap snap name to trigger a failure in 2180 // parseSnapsAndChannels 2181 stateMachine.Snaps = []string{"lxd=test=invalid=name"} 2182 err = stateMachine.prepareClassicImage() 2183 asserter.AssertErrContains(err, "Invalid syntax") 2184 2185 // try to include a nonexistent snap to trigger a failure 2186 // in snapStore.SnapInfo 2187 stateMachine.Snaps = []string{"test-this-snap-name-should-never-exist"} 2188 err = stateMachine.prepareClassicImage() 2189 asserter.AssertErrContains(err, "Error getting info for snap") 2190 2191 // mock image.Prepare 2192 stateMachine.Snaps = []string{"hello", "core"} 2193 imagePrepare = mockImagePrepare 2194 t.Cleanup(func() { 2195 imagePrepare = image.Prepare 2196 }) 2197 err = stateMachine.prepareClassicImage() 2198 asserter.AssertErrContains(err, "Error preparing image") 2199 imagePrepare = image.Prepare 2200 2201 // Test with a model assertion file 2202 stateMachine.ImageDef.ModelAssertion = filepath.Join("testdata", "modelAssertionClassic") 2203 err = stateMachine.prepareClassicImage() 2204 asserter.AssertErrNil(err, true) 2205 2206 path, err := filepath.Abs(filepath.Join("testdata", "modelAssertionClassic")) 2207 asserter.AssertErrNil(err, true) 2208 stateMachine.ImageDef.ModelAssertion = path 2209 err = stateMachine.prepareClassicImage() 2210 asserter.AssertErrNil(err, true) 2211 2212 stateMachine.ImageDef.ModelAssertion = "" 2213 // preseed the chroot, create a state.json file to trigger a reset, and mock some related functions 2214 err = stateMachine.prepareClassicImage() 2215 asserter.AssertErrNil(err, true) 2216 _, err = os.Create(filepath.Join(stateMachine.tempDirs.chroot, "var", "lib", "snapd", "state.json")) 2217 asserter.AssertErrNil(err, true) 2218 2219 seedOpen = mockSeedOpen 2220 t.Cleanup(func() { 2221 seedOpen = seed.Open 2222 }) 2223 err = stateMachine.prepareClassicImage() 2224 asserter.AssertErrContains(err, "Error getting list of preseeded snaps") 2225 seedOpen = seed.Open 2226 2227 // Setup the exec.Command mock 2228 testCaseName = "TestFailedPrepareClassicImage" 2229 execCommand = fakeExecCommand 2230 t.Cleanup(func() { 2231 execCommand = exec.Command 2232 }) 2233 err = stateMachine.prepareClassicImage() 2234 asserter.AssertErrContains(err, "Error resetting preseeding") 2235 } 2236 2237 // TestStateMachine_PopulateClassicRootfsContents runs the state machine through populate_rootfs_contents and examines 2238 // the rootfs to ensure at least some of the correct file are in place 2239 func TestStateMachine_PopulateClassicRootfsContents(t *testing.T) { 2240 t.Parallel() 2241 if testing.Short() { 2242 t.Skip("skipping test in short mode.") 2243 } 2244 2245 if runtime.GOARCH != "amd64" { 2246 t.Skip("Test for amd64 only") 2247 } 2248 asserter := helper.Asserter{T: t} 2249 restoreCWD := testhelper.SaveCWD() 2250 defer restoreCWD() 2251 2252 var stateMachine ClassicStateMachine 2253 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2254 stateMachine.parent = &stateMachine 2255 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2256 Architecture: getHostArch(), 2257 Series: getHostSuite(), 2258 Rootfs: &imagedefinition.Rootfs{ 2259 Archive: "ubuntu", 2260 }, 2261 Customization: &imagedefinition.Customization{}, 2262 } 2263 2264 err := stateMachine.makeTemporaryDirectories() 2265 asserter.AssertErrNil(err, true) 2266 2267 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 2268 2269 err = getBasicChroot(stateMachine.StateMachine) 2270 asserter.AssertErrNil(err, true) 2271 2272 err = stateMachine.populateClassicRootfsContents() 2273 asserter.AssertErrNil(err, true) 2274 2275 // check the files before Teardown 2276 fileList := []string{filepath.Join("etc", "shadow"), 2277 filepath.Join("etc", "systemd"), 2278 filepath.Join("usr", "lib")} 2279 for _, file := range fileList { 2280 _, err := os.Stat(filepath.Join(stateMachine.tempDirs.rootfs, file)) 2281 if err != nil { 2282 if os.IsNotExist(err) { 2283 t.Errorf("File %s should exist, but does not", file) 2284 } 2285 } 2286 } 2287 2288 // return when Customization.Fstab is not empty 2289 stateMachine.ImageDef.Customization.Fstab = []*imagedefinition.Fstab{ 2290 { 2291 Label: "writable", 2292 Mountpoint: "/", 2293 FSType: "ext4", 2294 MountOptions: "defaults", 2295 Dump: true, 2296 FsckOrder: 1, 2297 }, 2298 } 2299 2300 err = stateMachine.populateClassicRootfsContents() 2301 asserter.AssertErrNil(err, true) 2302 2303 // return when no Customization 2304 stateMachine.ImageDef.Customization = nil 2305 2306 err = stateMachine.populateClassicRootfsContents() 2307 asserter.AssertErrNil(err, true) 2308 } 2309 2310 // TestStateMachine_FailedPopulateClassicRootfsContents tests failed scenarios in populateClassicRootfsContents 2311 // this is accomplished by mocking functions 2312 func TestStateMachine_FailedPopulateClassicRootfsContents(t *testing.T) { 2313 if testing.Short() { 2314 t.Skip("skipping test in short mode.") 2315 } 2316 asserter := helper.Asserter{T: t} 2317 var stateMachine ClassicStateMachine 2318 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2319 stateMachine.parent = &stateMachine 2320 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2321 Architecture: getHostArch(), 2322 Series: getHostSuite(), 2323 Rootfs: &imagedefinition.Rootfs{ 2324 Archive: "ubuntu", 2325 }, 2326 Customization: &imagedefinition.Customization{}, 2327 } 2328 2329 err := stateMachine.makeTemporaryDirectories() 2330 asserter.AssertErrNil(err, true) 2331 2332 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 2333 2334 err = getBasicChroot(stateMachine.StateMachine) 2335 asserter.AssertErrNil(err, true) 2336 2337 // mock os.ReadDir 2338 osReadDir = mockReadDir 2339 t.Cleanup(func() { 2340 osReadDir = os.ReadDir 2341 }) 2342 err = stateMachine.populateClassicRootfsContents() 2343 asserter.AssertErrContains(err, "Error reading chroot dir") 2344 osReadDir = os.ReadDir 2345 2346 // mock osutil.CopySpecialFile 2347 osutilCopySpecialFile = mockCopySpecialFile 2348 t.Cleanup(func() { 2349 osutilCopySpecialFile = osutil.CopySpecialFile 2350 }) 2351 err = stateMachine.populateClassicRootfsContents() 2352 asserter.AssertErrContains(err, "Error copying rootfs") 2353 osutilCopySpecialFile = osutil.CopySpecialFile 2354 2355 // mock os.WriteFile 2356 osWriteFile = mockWriteFile 2357 t.Cleanup(func() { 2358 osWriteFile = os.WriteFile 2359 }) 2360 err = stateMachine.populateClassicRootfsContents() 2361 asserter.AssertErrContains(err, "Error writing to fstab") 2362 osWriteFile = os.WriteFile 2363 2364 // mock os.ReadFile 2365 osReadFile = mockReadFile 2366 t.Cleanup(func() { 2367 osReadFile = os.ReadFile 2368 }) 2369 err = stateMachine.populateClassicRootfsContents() 2370 asserter.AssertErrContains(err, "Error reading fstab") 2371 osReadFile = os.ReadFile 2372 2373 // return when existing fstab contains LABEL=writable 2374 //nolint:gosec,G306 2375 err = os.WriteFile(filepath.Join(stateMachine.tempDirs.chroot, "etc", "fstab"), 2376 []byte("LABEL=writable\n"), 2377 0644) 2378 asserter.AssertErrNil(err, true) 2379 err = stateMachine.populateClassicRootfsContents() 2380 asserter.AssertErrNil(err, true) 2381 2382 // create an /etc/resolv.conf.tmp in the chroot 2383 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "etc"), 0755) 2384 asserter.AssertErrNil(err, true) 2385 _, err = os.Create(filepath.Join(stateMachine.tempDirs.chroot, "etc", "resolv.conf.tmp")) 2386 asserter.AssertErrNil(err, true) 2387 2388 // mock helper.RestoreResolvConf 2389 helperRestoreResolvConf = mockRestoreResolvConf 2390 t.Cleanup(func() { 2391 helperRestoreResolvConf = helper.RestoreResolvConf 2392 }) 2393 err = stateMachine.populateClassicRootfsContents() 2394 asserter.AssertErrContains(err, "Error restoring /etc/resolv.conf") 2395 helperRestoreResolvConf = helper.RestoreResolvConf 2396 } 2397 2398 // TestSateMachine_customizeSourcesList tests functionality of the customizeSourcesList state function 2399 func TestSateMachine_customizeSourcesList(t *testing.T) { 2400 testCases := []struct { 2401 name string 2402 deb822Format bool 2403 existingSourcesList string 2404 existingDeb822SourcesList string 2405 customization *imagedefinition.Customization 2406 mockFuncs func() func() 2407 expectedErr string 2408 expectedSourcesList string 2409 expectedDeb822SourcesList string 2410 }{ 2411 { 2412 name: "set default sources.list", 2413 deb822Format: false, 2414 existingSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2415 customization: &imagedefinition.Customization{}, 2416 expectedSourcesList: `# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to 2417 # newer versions of the distribution. 2418 deb http://archive.ubuntu.com/ubuntu/ jammy main restricted universe 2419 `, 2420 }, 2421 { 2422 name: "set less components sources.list", 2423 deb822Format: false, 2424 existingSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2425 customization: &imagedefinition.Customization{ 2426 Components: []string{"main"}, 2427 }, 2428 expectedSourcesList: `# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to 2429 # newer versions of the distribution. 2430 deb http://archive.ubuntu.com/ubuntu/ jammy main 2431 `, 2432 }, 2433 { 2434 name: "set components and pocket sources.list", 2435 deb822Format: false, 2436 existingSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2437 customization: &imagedefinition.Customization{ 2438 Components: []string{"main"}, 2439 Pocket: "security", 2440 }, 2441 expectedSourcesList: `# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to 2442 # newer versions of the distribution. 2443 deb http://archive.ubuntu.com/ubuntu/ jammy main 2444 deb http://security.ubuntu.com/ubuntu/ jammy-security main 2445 `, 2446 }, 2447 { 2448 name: "fail to write sources.list", 2449 deb822Format: false, 2450 existingSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2451 customization: &imagedefinition.Customization{ 2452 Components: []string{"main"}, 2453 Pocket: "security", 2454 }, 2455 expectedSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2456 expectedErr: "unable to open sources.list file", 2457 mockFuncs: func() func() { 2458 mock := testhelper.NewOSMock( 2459 &testhelper.OSMockConf{ 2460 OpenFileThreshold: 0, 2461 }, 2462 ) 2463 2464 osOpenFile = mock.OpenFile 2465 return func() { osOpenFile = os.OpenFile } 2466 }, 2467 }, 2468 { 2469 name: "set default ubuntu.sources and commented sources.list", 2470 deb822Format: true, 2471 existingSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2472 existingDeb822SourcesList: `Types: deb 2473 URIs: http://archive.ubuntu.com/ 2474 Suites: jammy 2475 Components: main universe restricted multiverse 2476 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 2477 `, 2478 customization: &imagedefinition.Customization{}, 2479 expectedSourcesList: imagedefinition.LegacySourcesListComment, 2480 expectedDeb822SourcesList: `## Ubuntu distribution repository 2481 ## 2482 ## The following settings can be adjusted to configure which packages to use from Ubuntu. 2483 ## Mirror your choices (except for URIs and Suites) in the security section below to 2484 ## ensure timely security updates. 2485 ## 2486 ## Types: Append deb-src to enable the fetching of source package. 2487 ## URIs: A URL to the repository (you may add multiple URLs) 2488 ## Suites: The following additional suites can be configured 2489 ## <name>-updates - Major bug fix updates produced after the final release of the 2490 ## distribution. 2491 ## <name>-backports - software from this repository may not have been tested as 2492 ## extensively as that contained in the main release, although it includes 2493 ## newer versions of some applications which may provide useful features. 2494 ## Also, please note that software in backports WILL NOT receive any review 2495 ## or updates from the Ubuntu security team. 2496 ## Components: Aside from main, the following components can be added to the list 2497 ## restricted - Software that may not be under a free license, or protected by patents. 2498 ## universe - Community maintained packages. Software in this repository receives maintenance 2499 ## from volunteers in the Ubuntu community, or a 10 year security maintenance 2500 ## commitment from Canonical when an Ubuntu Pro subscription is attached. 2501 ## multiverse - Community maintained of restricted. Software from this repository is 2502 ## ENTIRELY UNSUPPORTED by the Ubuntu team, and may not be under a free 2503 ## licence. Please satisfy yourself as to your rights to use the software. 2504 ## Also, please note that software in multiverse WILL NOT receive any 2505 ## review or updates from the Ubuntu security team. 2506 ## 2507 ## See the sources.list(5) manual page for further settings. 2508 Types: deb 2509 URIs: http://archive.ubuntu.com/ubuntu/ 2510 Suites: jammy 2511 Components: main restricted 2512 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 2513 2514 ## Ubuntu security updates. Aside from URIs and Suites, 2515 ## this should mirror your choices in the previous section. 2516 Types: deb 2517 URIs: http://security.ubuntu.com/ubuntu/ 2518 Suites: jammy 2519 Components: main restricted 2520 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 2521 2522 `, 2523 }, 2524 { 2525 name: "fail to write ubuntu.sources and commented sources.list", 2526 deb822Format: true, 2527 existingSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2528 existingDeb822SourcesList: `Types: deb 2529 URIs: http://archive.ubuntu.com/ 2530 Suites: jammy 2531 Components: main universe restricted multiverse 2532 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 2533 `, 2534 customization: &imagedefinition.Customization{}, 2535 expectedSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2536 expectedDeb822SourcesList: `Types: deb 2537 URIs: http://archive.ubuntu.com/ 2538 Suites: jammy 2539 Components: main universe restricted multiverse 2540 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 2541 `, 2542 expectedErr: "unable to open ubuntu.sources file", 2543 mockFuncs: func() func() { 2544 mock := testhelper.NewOSMock( 2545 &testhelper.OSMockConf{ 2546 OpenFileThreshold: 0, 2547 }, 2548 ) 2549 2550 osOpenFile = mock.OpenFile 2551 return func() { osOpenFile = os.OpenFile } 2552 }, 2553 }, 2554 { 2555 name: "fail to create sources.list.d", 2556 deb822Format: true, 2557 existingSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2558 existingDeb822SourcesList: `Types: deb 2559 URIs: http://archive.ubuntu.com/ 2560 Suites: jammy 2561 Components: main universe restricted multiverse 2562 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 2563 `, 2564 customization: &imagedefinition.Customization{}, 2565 expectedSourcesList: "deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted", 2566 expectedDeb822SourcesList: `Types: deb 2567 URIs: http://archive.ubuntu.com/ 2568 Suites: jammy 2569 Components: main universe restricted multiverse 2570 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 2571 `, 2572 expectedErr: "Error /etc/apt/sources.list.d directory", 2573 mockFuncs: func() func() { 2574 mock := testhelper.NewOSMock( 2575 &testhelper.OSMockConf{ 2576 MkdirAllThreshold: 0, 2577 }, 2578 ) 2579 2580 osMkdirAll = mock.MkdirAll 2581 return func() { osMkdirAll = os.MkdirAll } 2582 }, 2583 }, 2584 } 2585 2586 for _, tc := range testCases { 2587 t.Run(tc.name, func(t *testing.T) { 2588 asserter := helper.Asserter{T: t} 2589 restoreCWD := testhelper.SaveCWD() 2590 defer restoreCWD() 2591 2592 var stateMachine ClassicStateMachine 2593 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2594 stateMachine.parent = &stateMachine 2595 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2596 Architecture: getHostArch(), 2597 Series: getHostSuite(), 2598 Rootfs: &imagedefinition.Rootfs{ 2599 SourcesListDeb822: helper.BoolPtr(tc.deb822Format), 2600 }, 2601 Customization: tc.customization, 2602 } 2603 2604 err := helper.SetDefaults(&stateMachine.ImageDef) 2605 asserter.AssertErrNil(err, true) 2606 2607 err = stateMachine.makeTemporaryDirectories() 2608 asserter.AssertErrNil(err, true) 2609 2610 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 2611 2612 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "etc", "apt", "sources.list.d"), 0644) 2613 asserter.AssertErrNil(err, true) 2614 2615 sourcesListPath := filepath.Join(stateMachine.tempDirs.chroot, "etc", "apt", "sources.list") 2616 deb822SourcesListPath := filepath.Join(stateMachine.tempDirs.chroot, "etc", "apt", "sources.list.d", "ubuntu.sources") 2617 2618 err = osWriteFile(sourcesListPath, []byte(tc.existingSourcesList), 0644) 2619 asserter.AssertErrNil(err, true) 2620 2621 err = osWriteFile(deb822SourcesListPath, []byte(tc.existingDeb822SourcesList), 0644) 2622 asserter.AssertErrNil(err, true) 2623 2624 if tc.mockFuncs != nil { 2625 restoreMock := tc.mockFuncs() 2626 t.Cleanup(restoreMock) 2627 } 2628 2629 err = stateMachine.customizeSourcesList() 2630 if err != nil || len(tc.expectedErr) != 0 { 2631 asserter.AssertErrContains(err, tc.expectedErr) 2632 } 2633 2634 sourcesListBytes, err := os.ReadFile(sourcesListPath) 2635 asserter.AssertErrNil(err, true) 2636 2637 asserter.AssertEqual(tc.expectedSourcesList, string(sourcesListBytes)) 2638 2639 deb822SourcesListBytes, err := os.ReadFile(deb822SourcesListPath) 2640 asserter.AssertErrNil(err, true) 2641 2642 asserter.AssertEqual(tc.expectedDeb822SourcesList, string(deb822SourcesListBytes)) 2643 2644 }) 2645 } 2646 } 2647 2648 // TestSateMachine_fixFstab tests functionality of the fixFstab function 2649 func TestSateMachine_fixFstab(t *testing.T) { 2650 t.Parallel() 2651 testCases := []struct { 2652 name string 2653 existingFstab string 2654 expectedFstab string 2655 }{ 2656 { 2657 name: "add entry to an existing but empty fstab", 2658 existingFstab: "# UNCONFIGURED FSTAB", 2659 expectedFstab: `LABEL=writable / ext4 discard,errors=remount-ro 0 1 2660 `, 2661 }, 2662 { 2663 name: "fix existing entry amongst several others", 2664 existingFstab: `# /etc/fstab: static file system information. 2665 UUID=1565-1398 / ext4 defaults 0 0 2666 #Here is another comment that should be left in place 2667 /dev/mapper/vgubuntu-swap_1 none swap sw 0 0 2668 `, 2669 expectedFstab: `# /etc/fstab: static file system information. 2670 LABEL=writable / ext4 discard,errors=remount-ro 0 1 2671 #Here is another comment that should be left in place 2672 /dev/mapper/vgubuntu-swap_1 none swap sw 0 0 2673 `, 2674 }, 2675 { 2676 name: "fix existing entry amongst several others (with spaces)", 2677 existingFstab: `# /etc/fstab: static file system information. 2678 UUID=1565-1398 / ext4 defaults 0 0 2679 /dev/mapper/vgubuntu-swap_1 none swap sw 0 0 2680 `, 2681 expectedFstab: `# /etc/fstab: static file system information. 2682 LABEL=writable / ext4 discard,errors=remount-ro 0 1 2683 /dev/mapper/vgubuntu-swap_1 none swap sw 0 0 2684 `, 2685 }, 2686 { 2687 name: "fix only one root mount point", 2688 existingFstab: `# /etc/fstab: static file system information. 2689 UUID=1565-1398 / ext4 defaults 0 0 2690 UUID=1234-5678 / ext4 defaults 0 0 2691 `, 2692 expectedFstab: `# /etc/fstab: static file system information. 2693 LABEL=writable / ext4 discard,errors=remount-ro 0 1 2694 UUID=1234-5678 / ext4 defaults 0 0 2695 `, 2696 }, 2697 } 2698 2699 for _, tc := range testCases { 2700 t.Run(tc.name, func(t *testing.T) { 2701 asserter := helper.Asserter{T: t} 2702 restoreCWD := testhelper.SaveCWD() 2703 defer restoreCWD() 2704 2705 var stateMachine ClassicStateMachine 2706 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2707 stateMachine.parent = &stateMachine 2708 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2709 Architecture: getHostArch(), 2710 Series: getHostSuite(), 2711 Rootfs: &imagedefinition.Rootfs{}, 2712 Customization: &imagedefinition.Customization{}, 2713 } 2714 2715 // set the defaults for the imageDef 2716 err := helper.SetDefaults(&stateMachine.ImageDef) 2717 asserter.AssertErrNil(err, true) 2718 2719 err = stateMachine.makeTemporaryDirectories() 2720 asserter.AssertErrNil(err, true) 2721 2722 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 2723 2724 // create the <chroot>/etc directory 2725 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.rootfs, "etc"), 0644) 2726 asserter.AssertErrNil(err, true) 2727 2728 fstabPath := filepath.Join(stateMachine.tempDirs.rootfs, "etc", "fstab") 2729 2730 // simulate an already existing fstab file 2731 if len(tc.existingFstab) != 0 { 2732 err = osWriteFile(fstabPath, []byte(tc.existingFstab), 0644) 2733 asserter.AssertErrNil(err, true) 2734 } 2735 2736 err = stateMachine.fixFstab() 2737 asserter.AssertErrNil(err, true) 2738 2739 fstabBytes, err := os.ReadFile(fstabPath) 2740 asserter.AssertErrNil(err, true) 2741 2742 if string(fstabBytes) != tc.expectedFstab { 2743 t.Errorf("Expected fstab content \"%s\", but got \"%s\"", 2744 tc.expectedFstab, string(fstabBytes)) 2745 } 2746 }) 2747 } 2748 } 2749 2750 // TestGeneratePackageManifest tests if classic image manifest generation works 2751 func TestGeneratePackageManifest(t *testing.T) { 2752 asserter := helper.Asserter{T: t} 2753 2754 // Setup the exec.Command mock 2755 testCaseName = "TestGeneratePackageManifest" 2756 execCommand = fakeExecCommand 2757 t.Cleanup(func() { 2758 execCommand = exec.Command 2759 }) 2760 // We need the output directory set for this 2761 outputDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 2762 asserter.AssertErrNil(err, true) 2763 t.Cleanup(func() { os.RemoveAll(outputDir) }) 2764 2765 var stateMachine ClassicStateMachine 2766 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2767 stateMachine.parent = &stateMachine 2768 stateMachine.commonFlags.OutputDir = outputDir 2769 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2770 Architecture: getHostArch(), 2771 Series: getHostSuite(), 2772 Rootfs: &imagedefinition.Rootfs{ 2773 Archive: "ubuntu", 2774 }, 2775 Customization: &imagedefinition.Customization{}, 2776 Artifacts: &imagedefinition.Artifact{ 2777 Manifest: &imagedefinition.Manifest{ 2778 ManifestName: "filesystem.manifest", 2779 }, 2780 }, 2781 } 2782 err = osMkdirAll(stateMachine.commonFlags.OutputDir, 0755) 2783 asserter.AssertErrNil(err, true) 2784 t.Cleanup(func() { os.RemoveAll(stateMachine.commonFlags.OutputDir) }) 2785 2786 err = stateMachine.generatePackageManifest() 2787 asserter.AssertErrNil(err, true) 2788 2789 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 2790 // Check if manifest file got generated and if it has expected contents 2791 manifestPath := filepath.Join(stateMachine.commonFlags.OutputDir, "filesystem.manifest") 2792 manifestBytes, err := os.ReadFile(manifestPath) 2793 asserter.AssertErrNil(err, true) 2794 // The order of packages shouldn't matter 2795 examplePackages := []string{"foo 1.2", "bar 1.4-1ubuntu4.1", "libbaz 0.1.3ubuntu2"} 2796 for _, pkg := range examplePackages { 2797 if !strings.Contains(string(manifestBytes), pkg) { 2798 t.Errorf("filesystem.manifest does not contain expected package: %s", pkg) 2799 } 2800 } 2801 } 2802 2803 // TestFailedGeneratePackageManifest tests if classic manifest generation failures are reported 2804 func TestFailedGeneratePackageManifest(t *testing.T) { 2805 asserter := helper.Asserter{T: t} 2806 var stateMachine ClassicStateMachine 2807 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2808 stateMachine.parent = &stateMachine 2809 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2810 Architecture: getHostArch(), 2811 Series: getHostSuite(), 2812 Rootfs: &imagedefinition.Rootfs{ 2813 Archive: "ubuntu", 2814 }, 2815 Customization: &imagedefinition.Customization{}, 2816 Artifacts: &imagedefinition.Artifact{ 2817 Manifest: &imagedefinition.Manifest{ 2818 ManifestName: "filesystem.manifest", 2819 }, 2820 }, 2821 } 2822 2823 // We need the output directory set for this 2824 outputDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 2825 asserter.AssertErrNil(err, true) 2826 t.Cleanup(func() { os.RemoveAll(outputDir) }) 2827 stateMachine.commonFlags.OutputDir = outputDir 2828 2829 // Setup the exec.Command mock - version from the success test 2830 testCaseName = "TestGeneratePackageManifest" 2831 execCommand = fakeExecCommand 2832 t.Cleanup(func() { 2833 execCommand = exec.Command 2834 }) 2835 2836 // Setup the mock for os.Create, making those fail 2837 osCreate = mockCreate 2838 t.Cleanup(func() { 2839 osCreate = os.Create 2840 }) 2841 2842 err = stateMachine.generatePackageManifest() 2843 asserter.AssertErrContains(err, "Error creating manifest file") 2844 osCreate = os.Create 2845 2846 // Setup the exec.Command mock - version from the fail test 2847 testCaseName = "TestFailedGeneratePackageManifest" 2848 execCommand = fakeExecCommand 2849 t.Cleanup(func() { 2850 execCommand = exec.Command 2851 }) 2852 err = stateMachine.generatePackageManifest() 2853 asserter.AssertErrContains(err, "Error generating package manifest with command") 2854 } 2855 2856 // TestGenerateFilelist tests if classic image filelist generation works 2857 func TestGenerateFilelist(t *testing.T) { 2858 asserter := helper.Asserter{T: t} 2859 2860 // Setup the exec.Command mock 2861 testCaseName = "TestGenerateFilelist" 2862 execCommand = fakeExecCommand 2863 t.Cleanup(func() { 2864 execCommand = exec.Command 2865 }) 2866 // We need the output directory set for this 2867 outputDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 2868 asserter.AssertErrNil(err, true) 2869 t.Cleanup(func() { os.RemoveAll(outputDir) }) 2870 2871 var stateMachine ClassicStateMachine 2872 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2873 stateMachine.parent = &stateMachine 2874 stateMachine.commonFlags.OutputDir = outputDir 2875 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2876 Architecture: getHostArch(), 2877 Series: getHostSuite(), 2878 Rootfs: &imagedefinition.Rootfs{ 2879 Archive: "ubuntu", 2880 }, 2881 Customization: &imagedefinition.Customization{}, 2882 Artifacts: &imagedefinition.Artifact{ 2883 Filelist: &imagedefinition.Filelist{ 2884 FilelistName: "filesystem.filelist", 2885 }, 2886 }, 2887 } 2888 err = osMkdirAll(stateMachine.commonFlags.OutputDir, 0755) 2889 asserter.AssertErrNil(err, true) 2890 t.Cleanup(func() { os.RemoveAll(stateMachine.commonFlags.OutputDir) }) 2891 2892 err = stateMachine.generateFilelist() 2893 asserter.AssertErrNil(err, true) 2894 2895 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 2896 // Check if filelist file got generated 2897 filelistPath := filepath.Join(stateMachine.commonFlags.OutputDir, "filesystem.filelist") 2898 _, err = os.Stat(filelistPath) 2899 asserter.AssertErrNil(err, true) 2900 } 2901 2902 // TestFailedGenerateFilelist tests if classic filelist generation failures are reported 2903 func TestFailedGenerateFilelist(t *testing.T) { 2904 asserter := helper.Asserter{T: t} 2905 2906 // Setup the exec.Command mock - version from the success test 2907 testCaseName = "TestGenerateFilelist" 2908 execCommand = fakeExecCommand 2909 defer func() { 2910 execCommand = exec.Command 2911 }() 2912 // Setup the mock for os.Create, making those fail 2913 osCreate = mockCreate 2914 defer func() { 2915 osCreate = os.Create 2916 }() 2917 2918 var stateMachine ClassicStateMachine 2919 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2920 stateMachine.parent = &stateMachine 2921 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 2922 Architecture: getHostArch(), 2923 Series: getHostSuite(), 2924 Rootfs: &imagedefinition.Rootfs{ 2925 Archive: "ubuntu", 2926 }, 2927 Customization: &imagedefinition.Customization{}, 2928 Artifacts: &imagedefinition.Artifact{ 2929 Filelist: &imagedefinition.Filelist{ 2930 FilelistName: "filesystem.filelist", 2931 }, 2932 }, 2933 } 2934 2935 // We need the output directory set for this 2936 outputDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 2937 asserter.AssertErrNil(err, true) 2938 t.Cleanup(func() { os.RemoveAll(outputDir) }) 2939 stateMachine.commonFlags.OutputDir = outputDir 2940 2941 // Setup the exec.Command mock - version from the success test 2942 testCaseName = "TestGenerateFilelist" 2943 execCommand = fakeExecCommand 2944 defer func() { 2945 execCommand = exec.Command 2946 }() 2947 2948 // Setup the mock for os.Create, making those fail 2949 osCreate = mockCreate 2950 defer func() { 2951 osCreate = os.Create 2952 }() 2953 2954 err = stateMachine.generateFilelist() 2955 asserter.AssertErrContains(err, "Error creating filelist") 2956 osCreate = os.Create 2957 2958 // Setup the exec.Command mock - version from the fail test 2959 testCaseName = "TestFailedGenerateFilelist" 2960 execCommand = fakeExecCommand 2961 defer func() { 2962 execCommand = exec.Command 2963 }() 2964 err = stateMachine.generateFilelist() 2965 asserter.AssertErrContains(err, "Error generating file list with command") 2966 } 2967 2968 // TestSuccessfulClassicRun runs through a full classic state machine run and ensures 2969 // it is successful. It creates a .img and a .qcow2 file and ensures they are the 2970 // correct file types it also mounts the resulting .img and ensures grub was updated 2971 func TestSuccessfulClassicRun(t *testing.T) { 2972 if testing.Short() { 2973 t.Skip("skipping test in short mode.") 2974 } 2975 2976 asserter := helper.Asserter{T: t} 2977 restoreCWD := testhelper.SaveCWD() 2978 t.Cleanup(restoreCWD) 2979 2980 // We need the output directory set for this 2981 outputDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 2982 asserter.AssertErrNil(err, true) 2983 t.Cleanup(func() { os.RemoveAll(outputDir) }) 2984 2985 var stateMachine ClassicStateMachine 2986 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 2987 stateMachine.parent = &stateMachine 2988 stateMachine.commonFlags.Debug = true 2989 stateMachine.commonFlags.Size = "5G" 2990 stateMachine.commonFlags.OutputDir = outputDir 2991 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 2992 "test_amd64.yaml") 2993 2994 err = stateMachine.Setup() 2995 asserter.AssertErrNil(err, true) 2996 2997 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 2998 2999 err = stateMachine.Run() 3000 asserter.AssertErrNil(err, true) 3001 3002 t.Cleanup(func() { 3003 err = stateMachine.Teardown() 3004 asserter.AssertErrNil(err, true) 3005 }) 3006 3007 testHelperCheckPPAInstalled(t, &asserter, stateMachine.tempDirs.chroot) 3008 testHelperCheckSnapInstalled(t, &asserter, stateMachine.tempDirs.chroot) 3009 3010 artifacts := map[string]string{ 3011 "pc-amd64.img": "DOS/MBR boot sector", 3012 "pc-amd64.qcow2": "QEMU QCOW", 3013 "filesystem-manifest.txt": "text", 3014 "filesystem-filelist.txt": "text", 3015 } 3016 testHelperCheckArtifacts(t, &asserter, stateMachine.commonFlags.OutputDir, artifacts) 3017 3018 // create a directory in which to mount the rootfs 3019 mountDir := filepath.Join(stateMachine.tempDirs.scratch, "loopback") 3020 var mountImageCmds []*exec.Cmd 3021 var umountImageCmds []*exec.Cmd 3022 3023 t.Cleanup(func() { 3024 for _, teardownCmd := range umountImageCmds { 3025 if tmpErr := teardownCmd.Run(); tmpErr != nil { 3026 if err != nil { 3027 err = fmt.Errorf("%s after previous error: %w", tmpErr, err) 3028 } else { 3029 err = tmpErr 3030 } 3031 } 3032 } 3033 }) 3034 3035 imgPath := filepath.Join(stateMachine.commonFlags.OutputDir, "pc-amd64.img") 3036 3037 // set up the loopback 3038 mountImageCmds = append(mountImageCmds, 3039 //nolint:gosec,G204 3040 exec.Command("losetup", 3041 filepath.Join("/dev", "loop99"), 3042 imgPath, 3043 ), 3044 ) 3045 3046 // unset the loopback 3047 umountImageCmds = append(umountImageCmds, 3048 //nolint:gosec,G204 3049 exec.Command("losetup", "--detach", filepath.Join("/dev", "loop99")), 3050 ) 3051 3052 mountImageCmds = append(mountImageCmds, 3053 //nolint:gosec,G204 3054 exec.Command("kpartx", "-a", filepath.Join("/dev", "loop99")), 3055 ) 3056 3057 umountImageCmds = append([]*exec.Cmd{ 3058 //nolint:gosec,G204 3059 exec.Command("kpartx", "-d", filepath.Join("/dev", "loop99")), 3060 }, umountImageCmds..., 3061 ) 3062 3063 mountImageCmds = append(mountImageCmds, 3064 //nolint:gosec,G204 3065 exec.Command("mount", filepath.Join("/dev", "mapper", "loop99p3"), mountDir), // with this example the rootfs is partition 3 mountDir 3066 ) 3067 3068 umountImageCmds = append([]*exec.Cmd{ 3069 //nolint:gosec,G204 3070 exec.Command("mount", "--make-rprivate", filepath.Join("/dev", "mapper", "loop99p3")), 3071 //nolint:gosec,G204 3072 exec.Command("umount", "--recursive", filepath.Join("/dev", "mapper", "loop99p3")), 3073 }, umountImageCmds..., 3074 ) 3075 3076 // set up the mountpoints 3077 mountPoints := []mountPoint{ 3078 { 3079 src: "devtmpfs-build", 3080 basePath: mountDir, 3081 relpath: "/dev", 3082 typ: "devtmpfs", 3083 }, 3084 { 3085 src: "devpts-build", 3086 basePath: mountDir, 3087 relpath: "/dev/pts", 3088 typ: "devpts", 3089 opts: []string{"nodev", "nosuid"}, 3090 }, 3091 { 3092 src: "proc-build", 3093 basePath: mountDir, 3094 relpath: "/proc", 3095 typ: "proc", 3096 }, 3097 { 3098 src: "sysfs-build", 3099 basePath: mountDir, 3100 relpath: "/sys", 3101 typ: "sysfs", 3102 }, 3103 } 3104 for _, mp := range mountPoints { 3105 mountCmds, umountCmds, err := mp.getMountCmd() 3106 if err != nil { 3107 t.Errorf("Error preparing mountpoint \"%s\": \"%s\"", 3108 mp.relpath, 3109 err.Error(), 3110 ) 3111 } 3112 mountImageCmds = append(mountImageCmds, mountCmds...) 3113 umountImageCmds = append(umountCmds, umountImageCmds...) 3114 } 3115 // make sure to unmount the disk too 3116 umountImageCmds = append([]*exec.Cmd{exec.Command("umount", "--recursive", mountDir)}, umountImageCmds...) 3117 3118 // now run all the commands to mount the image 3119 for _, cmd := range mountImageCmds { 3120 outPut := helper.SetCommandOutput(cmd, true) 3121 err := cmd.Run() 3122 if err != nil { 3123 t.Errorf("Error running command \"%s\". Error is \"%s\". Output is: \n%s", 3124 cmd.String(), err.Error(), outPut.String()) 3125 } 3126 } 3127 3128 testHelperCheckMakeDirs(t, mountDir) 3129 testHelperCheckAddUser(t, &asserter, mountDir) 3130 testHelperCheckGrubConfig(t, mountDir) 3131 testHelperCheckCleanedFiles(t, mountDir) 3132 testHelperCheckLocaleFile(t, &asserter, mountDir) 3133 testHelperCheckSourcesList(t, &asserter, mountDir) 3134 } 3135 3136 func testHelperCheckPPAInstalled(t *testing.T, asserter *helper.Asserter, chroot string) { 3137 t.Helper() 3138 files := []string{ 3139 filepath.Join(chroot, "usr", "bin", "hello-ubuntu-image-public"), 3140 filepath.Join(chroot, "usr", "bin", "hello-ubuntu-image-private"), 3141 } 3142 for _, file := range files { 3143 _, err := os.Stat(file) 3144 asserter.AssertErrNil(err, true) 3145 } 3146 } 3147 3148 func testHelperCheckSnapInstalled(t *testing.T, asserter *helper.Asserter, chroot string) { 3149 t.Helper() 3150 type snapList struct { 3151 Snaps []struct { 3152 Name string `yaml:"name"` 3153 Channel string `yaml:"channel"` 3154 } `yaml:"snaps"` 3155 } 3156 3157 seedYaml := filepath.Join(chroot, 3158 "var", "lib", "snapd", "seed", "seed.yaml") 3159 3160 seedFile, err := os.Open(seedYaml) 3161 asserter.AssertErrNil(err, true) 3162 defer seedFile.Close() 3163 3164 var seededSnaps snapList 3165 err = yaml.NewDecoder(seedFile).Decode(&seededSnaps) 3166 asserter.AssertErrNil(err, true) 3167 3168 expectedSnapChannels := map[string]string{ 3169 "hello": "candidate", 3170 "core20": "stable", 3171 } 3172 3173 for _, seededSnap := range seededSnaps.Snaps { 3174 channel, found := expectedSnapChannels[seededSnap.Name] 3175 if found { 3176 if channel != seededSnap.Channel { 3177 t.Errorf("Expected snap %s to be pre-seeded with channel %s, but got %s", 3178 seededSnap.Name, channel, seededSnap.Channel) 3179 } 3180 } 3181 } 3182 } 3183 3184 func testHelperCheckArtifacts(t *testing.T, asserter *helper.Asserter, outputDir string, artifacts map[string]string) { 3185 t.Helper() 3186 for artifact, fileType := range artifacts { 3187 fullPath := filepath.Join(outputDir, artifact) 3188 _, err := os.Stat(fullPath) 3189 if err != nil { 3190 if os.IsNotExist(err) { 3191 t.Errorf("File \"%s\" should exist, but does not", fullPath) 3192 } 3193 } 3194 3195 // check it is the expected file type 3196 fileCommand := *exec.Command("file", fullPath) 3197 cmdOutput, err := fileCommand.CombinedOutput() 3198 asserter.AssertErrNil(err, true) 3199 if !strings.Contains(string(cmdOutput), fileType) { 3200 t.Errorf("File \"%s\" is the wrong file type. Expected \"%s\" but got \"%s\"", 3201 fullPath, fileType, string(cmdOutput)) 3202 } 3203 } 3204 } 3205 3206 func testHelperCheckMakeDirs(t *testing.T, mountDir string) { 3207 t.Helper() 3208 addedDir := filepath.Join(mountDir, "etc", "foo", "bar") 3209 _, err := os.Stat(addedDir) 3210 if err != nil { 3211 if os.IsNotExist(err) { 3212 t.Errorf("Directory \"%s\" should exist, but does not", addedDir) 3213 } 3214 } 3215 } 3216 3217 func testHelperCheckAddUser(t *testing.T, asserter *helper.Asserter, mountDir string) { 3218 t.Helper() 3219 shadowPath := filepath.Join(mountDir, "etc", "shadow") 3220 shadowFile, err := os.Open(shadowPath) 3221 asserter.AssertErrNil(err, true) 3222 defer shadowFile.Close() 3223 ubuntu2Found := false 3224 ubuntu2Line := "" 3225 3226 scanner := bufio.NewScanner(shadowFile) 3227 3228 for scanner.Scan() { 3229 if strings.HasPrefix(scanner.Text(), "ubuntu2") { 3230 ubuntu2Line = scanner.Text() 3231 ubuntu2Found = true 3232 break 3233 } 3234 } 3235 3236 if !ubuntu2Found { 3237 t.Error("ubuntu2 user not created") 3238 } 3239 3240 expire := strings.Split(ubuntu2Line, ":")[2] 3241 3242 if expire != "0" { 3243 t.Error("ubuntu2 user password should be expired") 3244 } 3245 } 3246 3247 func testHelperCheckGrubConfig(t *testing.T, mountDir string) { 3248 t.Helper() 3249 grubCfg := filepath.Join(mountDir, "boot", "grub", "grub.cfg") 3250 _, err := os.Stat(grubCfg) 3251 if err != nil { 3252 if os.IsNotExist(err) { 3253 t.Errorf("File \"%s\" should exist, but does not", grubCfg) 3254 } 3255 } 3256 } 3257 3258 func testHelperCheckCleanedFiles(t *testing.T, mountDir string) { 3259 t.Helper() 3260 cleaned := []string{ 3261 filepath.Join(mountDir, "var", "lib", "dbus", "machine-id"), 3262 filepath.Join(mountDir, "etc", "ssh", "ssh_host_rsa_key"), 3263 filepath.Join(mountDir, "etc", "ssh", "ssh_host_rsa_key.pub"), 3264 filepath.Join(mountDir, "etc", "ssh", "ssh_host_ecdsa_key"), 3265 filepath.Join(mountDir, "etc", "ssh", "ssh_host_ecdsa_key.pub"), 3266 filepath.Join(mountDir, "usr", "sbin", "policy-rc.d"), 3267 filepath.Join(mountDir, "sbin", "start-stop-daemon.REAL"), 3268 filepath.Join(mountDir, "sbin", "initctl.REAL"), 3269 } 3270 for _, file := range cleaned { 3271 _, err := os.Stat(file) 3272 if !os.IsNotExist(err) { 3273 t.Errorf("File %s should not exist, but does", file) 3274 } 3275 } 3276 3277 truncated := []string{ 3278 filepath.Join(mountDir, "etc", "machine-id"), 3279 } 3280 for _, file := range truncated { 3281 fileInfo, err := os.Stat(file) 3282 if os.IsNotExist(err) { 3283 t.Errorf("File %s should exist, but does not", file) 3284 } 3285 3286 if fileInfo.Size() != 0 { 3287 t.Errorf("File %s should be empty, but it is not. Size: %v", file, fileInfo.Size()) 3288 } 3289 } 3290 } 3291 3292 func testHelperCheckLocaleFile(t *testing.T, asserter *helper.Asserter, mountDir string) { 3293 t.Helper() 3294 localeFile := filepath.Join(mountDir, "etc", "default", "locale") 3295 localeBytes, err := os.ReadFile(localeFile) 3296 asserter.AssertErrNil(err, true) 3297 if !strings.Contains(string(localeBytes), "LANG=C.UTF-8") { 3298 t.Errorf("Expected LANG=C.UTF-8 in %s, but got %s", localeFile, string(localeBytes)) 3299 } 3300 } 3301 3302 // testHelperCheckSourcesList checks if components and pocket correctly setup in /etc/apt/sources.list.d/ubuntu.sources 3303 func testHelperCheckSourcesList(t *testing.T, asserter *helper.Asserter, mountDir string) { 3304 t.Helper() 3305 aptDeb822SourcesListBytes, err := os.ReadFile(filepath.Join(mountDir, "etc", "apt", "sources.list.d", "ubuntu.sources")) 3306 asserter.AssertErrNil(err, true) 3307 wantAptDeb822SourcesList := `## Ubuntu distribution repository 3308 ## 3309 ## The following settings can be adjusted to configure which packages to use from Ubuntu. 3310 ## Mirror your choices (except for URIs and Suites) in the security section below to 3311 ## ensure timely security updates. 3312 ## 3313 ## Types: Append deb-src to enable the fetching of source package. 3314 ## URIs: A URL to the repository (you may add multiple URLs) 3315 ## Suites: The following additional suites can be configured 3316 ## <name>-updates - Major bug fix updates produced after the final release of the 3317 ## distribution. 3318 ## <name>-backports - software from this repository may not have been tested as 3319 ## extensively as that contained in the main release, although it includes 3320 ## newer versions of some applications which may provide useful features. 3321 ## Also, please note that software in backports WILL NOT receive any review 3322 ## or updates from the Ubuntu security team. 3323 ## Components: Aside from main, the following components can be added to the list 3324 ## restricted - Software that may not be under a free license, or protected by patents. 3325 ## universe - Community maintained packages. Software in this repository receives maintenance 3326 ## from volunteers in the Ubuntu community, or a 10 year security maintenance 3327 ## commitment from Canonical when an Ubuntu Pro subscription is attached. 3328 ## multiverse - Community maintained of restricted. Software from this repository is 3329 ## ENTIRELY UNSUPPORTED by the Ubuntu team, and may not be under a free 3330 ## licence. Please satisfy yourself as to your rights to use the software. 3331 ## Also, please note that software in multiverse WILL NOT receive any 3332 ## review or updates from the Ubuntu security team. 3333 ## 3334 ## See the sources.list(5) manual page for further settings. 3335 Types: deb 3336 URIs: http://archive.ubuntu.com/ubuntu/ 3337 Suites: jammy jammy-updates jammy-proposed 3338 Components: main universe restricted 3339 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 3340 3341 ## Ubuntu security updates. Aside from URIs and Suites, 3342 ## this should mirror your choices in the previous section. 3343 Types: deb 3344 URIs: http://security.ubuntu.com/ubuntu/ 3345 Suites: jammy jammy-updates jammy-proposed 3346 Components: main universe restricted 3347 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 3348 3349 ` 3350 asserter.AssertEqual(wantAptDeb822SourcesList, string(aptDeb822SourcesListBytes)) 3351 3352 // check if components and pocket correctly setup in /etc/apt/sources.list 3353 aptSourcesListBytes, err := os.ReadFile(filepath.Join(mountDir, "etc", "apt", "sources.list")) 3354 asserter.AssertErrNil(err, true) 3355 asserter.AssertEqual(imagedefinition.LegacySourcesListComment, string(aptSourcesListBytes)) 3356 } 3357 3358 // TestSuccessfulClassicRunNoArtifact runs through a full classic state machine run without artifact 3359 func TestSuccessfulClassicRunNoArtifact(t *testing.T) { 3360 if testing.Short() { 3361 t.Skip("skipping test in short mode.") 3362 } 3363 3364 asserter := helper.Asserter{T: t} 3365 restoreCWD := testhelper.SaveCWD() 3366 t.Cleanup(restoreCWD) 3367 3368 // We need the output directory set for this 3369 outputDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 3370 asserter.AssertErrNil(err, true) 3371 t.Cleanup(func() { os.RemoveAll(outputDir) }) 3372 3373 var stateMachine ClassicStateMachine 3374 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3375 stateMachine.parent = &stateMachine 3376 stateMachine.commonFlags.Debug = true 3377 stateMachine.commonFlags.Size = "5G" 3378 stateMachine.commonFlags.OutputDir = outputDir 3379 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 3380 "test_no_artifact.yaml") 3381 3382 err = stateMachine.Setup() 3383 asserter.AssertErrNil(err, true) 3384 3385 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 3386 3387 err = stateMachine.Run() 3388 asserter.AssertErrNil(err, true) 3389 3390 t.Cleanup(func() { 3391 err = stateMachine.Teardown() 3392 asserter.AssertErrNil(err, true) 3393 }) 3394 3395 // make sure packages were successfully installed from public and private ppas 3396 testHelperCheckPPAInstalled(t, &asserter, stateMachine.tempDirs.chroot) 3397 3398 // make sure snaps from the correct channel were installed 3399 testHelperCheckSnapInstalled(t, &asserter, stateMachine.tempDirs.chroot) 3400 } 3401 3402 func TestSuccessfulRootfsGeneration(t *testing.T) { 3403 if testing.Short() { 3404 t.Skip("skipping test in short mode.") 3405 } 3406 asserter := helper.Asserter{T: t} 3407 restoreCWD := testhelper.SaveCWD() 3408 t.Cleanup(restoreCWD) 3409 3410 // We need the output directory set for this 3411 outputDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 3412 asserter.AssertErrNil(err, true) 3413 t.Cleanup(func() { os.RemoveAll(outputDir) }) 3414 3415 var stateMachine ClassicStateMachine 3416 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3417 stateMachine.parent = &stateMachine 3418 stateMachine.commonFlags.Debug = true 3419 stateMachine.commonFlags.Size = "5G" 3420 stateMachine.commonFlags.OutputDir = outputDir 3421 stateMachine.Args.ImageDefinition = filepath.Join("testdata", "image_definitions", 3422 "test_rootfs_tarball.yaml") 3423 3424 err = stateMachine.Setup() 3425 asserter.AssertErrNil(err, true) 3426 3427 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 3428 3429 err = stateMachine.Run() 3430 asserter.AssertErrNil(err, true) 3431 3432 t.Cleanup(func() { 3433 err = stateMachine.Teardown() 3434 asserter.AssertErrNil(err, true) 3435 }) 3436 3437 // make sure all the artifacts were created and are the correct file types 3438 artifacts := map[string]string{ 3439 "rootfs.tar": "tar archive", 3440 } 3441 for artifact, fileType := range artifacts { 3442 fullPath := filepath.Join(stateMachine.commonFlags.OutputDir, artifact) 3443 _, err := os.Stat(fullPath) 3444 if err != nil { 3445 if os.IsNotExist(err) { 3446 t.Errorf("File \"%s\" should exist, but does not", fullPath) 3447 } 3448 } 3449 3450 // check it is the expected file type 3451 fileCommand := *exec.Command("file", fullPath) 3452 cmdOutput, err := fileCommand.CombinedOutput() 3453 asserter.AssertErrNil(err, true) 3454 if !strings.Contains(string(cmdOutput), fileType) { 3455 t.Errorf("File \"%s\" is the wrong file type. Expected \"%s\" but got \"%s\"", 3456 fullPath, fileType, string(cmdOutput)) 3457 } 3458 } 3459 } 3460 3461 // TestGerminate tests the germinate state and ensures some necessary packages are included 3462 func TestGerminate(t *testing.T) { 3463 if testing.Short() { 3464 t.Skip("skipping test in short mode.") 3465 } 3466 testCases := []struct { 3467 name string 3468 flavor string 3469 seedURLs []string 3470 seedNames []string 3471 expectedPackages []string 3472 expectedSnaps []string 3473 vcs bool 3474 }{ 3475 { 3476 "git", 3477 "ubuntu", 3478 []string{"git://git.launchpad.net/~ubuntu-core-dev/ubuntu-seeds/+git/"}, 3479 []string{"server", "minimal", "standard", "cloud-image"}, 3480 []string{"python3", "sudo", "cloud-init", "ubuntu-server"}, 3481 []string{"lxd"}, 3482 true, 3483 }, 3484 { 3485 "http", 3486 "ubuntu", 3487 []string{"https://people.canonical.com/~ubuntu-archive/seeds/"}, 3488 []string{"server", "minimal", "standard", "cloud-image"}, 3489 []string{"python3", "sudo", "cloud-init", "ubuntu-server"}, 3490 []string{"lxd"}, 3491 false, 3492 }, 3493 { 3494 "bzr+git", 3495 "ubuntu", 3496 []string{"http://bazaar.launchpad.net/~ubuntu-mate-dev/ubuntu-seeds/", 3497 "git://git.launchpad.net/~ubuntu-core-dev/ubuntu-seeds/+git/", 3498 "https://people.canonical.com/~ubuntu-archive/seeds/", 3499 }, 3500 []string{"desktop", "desktop-common", "standard", "minimal"}, 3501 []string{"xorg", "wget", "ubuntu-minimal"}, 3502 []string{}, 3503 true, 3504 }, 3505 } 3506 for _, tc := range testCases { 3507 t.Run("test_germinate_"+tc.name, func(t *testing.T) { 3508 asserter := helper.Asserter{T: t} 3509 restoreCWD := testhelper.SaveCWD() 3510 defer restoreCWD() 3511 3512 var stateMachine ClassicStateMachine 3513 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3514 stateMachine.parent = &stateMachine 3515 3516 err := stateMachine.makeTemporaryDirectories() 3517 asserter.AssertErrNil(err, true) 3518 3519 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 3520 3521 hostArch := getHostArch() 3522 hostSuite := getHostSuite() 3523 imageDef := imagedefinition.ImageDefinition{ 3524 Architecture: hostArch, 3525 Series: hostSuite, 3526 Rootfs: &imagedefinition.Rootfs{ 3527 Flavor: tc.flavor, 3528 Mirror: "http://archive.ubuntu.com/ubuntu/", 3529 Seed: &imagedefinition.Seed{ 3530 SeedURLs: tc.seedURLs, 3531 SeedBranch: hostSuite, 3532 Names: tc.seedNames, 3533 Vcs: helper.BoolPtr(tc.vcs), 3534 }, 3535 }, 3536 } 3537 3538 stateMachine.ImageDef = imageDef 3539 3540 err = stateMachine.germinate() 3541 asserter.AssertErrNil(err, true) 3542 3543 // spot check some packages that should remain seeded for a long time 3544 testHelperCheckGerminatedPackages(t, tc.expectedPackages, stateMachine.Packages) 3545 // spot check some snaps that should remain seeded for a long time 3546 testHelperCheckGerminatedSnaps(t, tc.expectedSnaps, stateMachine.Snaps) 3547 }) 3548 } 3549 } 3550 3551 func testHelperCheckGerminatedPackages(t *testing.T, expectedPackages []string, gotPackages []string) { 3552 for _, expectedPackage := range expectedPackages { 3553 found := false 3554 for _, seedPackage := range gotPackages { 3555 if expectedPackage == seedPackage { 3556 found = true 3557 } 3558 } 3559 if !found { 3560 t.Errorf("Expected to find %s in list of packages: %v", 3561 expectedPackage, gotPackages) 3562 } 3563 } 3564 } 3565 3566 func testHelperCheckGerminatedSnaps(t *testing.T, expectedSnaps []string, gotSnaps []string) { 3567 for _, expectedSnap := range expectedSnaps { 3568 found := false 3569 for _, seedSnap := range gotSnaps { 3570 snapName := strings.Split(seedSnap, "=")[0] 3571 if expectedSnap == snapName { 3572 found = true 3573 } 3574 } 3575 if !found { 3576 t.Errorf("Expected to find %s in list of snaps: %v", 3577 expectedSnap, gotSnaps) 3578 } 3579 } 3580 } 3581 3582 // TestFailedGerminate mocks function calls to test 3583 // failure cases in the germinate state 3584 func TestFailedGerminate(t *testing.T) { 3585 if testing.Short() { 3586 t.Skip("skipping test in short mode.") 3587 } 3588 asserter := helper.Asserter{T: t} 3589 restoreCWD := testhelper.SaveCWD() 3590 defer restoreCWD() 3591 3592 var stateMachine ClassicStateMachine 3593 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3594 stateMachine.parent = &stateMachine 3595 3596 err := stateMachine.makeTemporaryDirectories() 3597 asserter.AssertErrNil(err, true) 3598 3599 // create a valid imageDefinition 3600 hostArch := getHostArch() 3601 hostSuite := getHostSuite() 3602 imageDef := imagedefinition.ImageDefinition{ 3603 Architecture: hostArch, 3604 Series: hostSuite, 3605 Rootfs: &imagedefinition.Rootfs{ 3606 Flavor: "ubuntu", 3607 Mirror: "http://archive.ubuntu.com/ubuntu/", 3608 Seed: &imagedefinition.Seed{ 3609 SeedURLs: []string{"git://git.launchpad.net/~ubuntu-core-dev/ubuntu-seeds/+git/"}, 3610 SeedBranch: hostSuite, 3611 Names: []string{"server", "minimal", "standard", "cloud-image"}, 3612 Vcs: helper.BoolPtr(true), 3613 }, 3614 }, 3615 } 3616 stateMachine.ImageDef = imageDef 3617 3618 // mock os.Mkdir 3619 osMkdir = mockMkdir 3620 t.Cleanup(func() { 3621 osMkdir = os.Mkdir 3622 }) 3623 err = stateMachine.germinate() 3624 asserter.AssertErrContains(err, "Error creating germinate directory") 3625 osMkdir = os.Mkdir 3626 3627 // Setup the exec.Command mock 3628 testCaseName = "TestFailedGerminate" 3629 execCommand = fakeExecCommand 3630 t.Cleanup(func() { 3631 execCommand = exec.Command 3632 }) 3633 err = stateMachine.germinate() 3634 asserter.AssertErrContains(err, "Error running germinate command") 3635 execCommand = exec.Command 3636 3637 // mock os.Open 3638 osOpen = mockOpen 3639 t.Cleanup(func() { 3640 osOpen = os.Open 3641 }) 3642 err = stateMachine.germinate() 3643 asserter.AssertErrContains(err, "Error opening seed file") 3644 osOpen = os.Open 3645 3646 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 3647 } 3648 3649 // TestBuildGadgetTreeGit tests the successful build of a gadget tree 3650 func TestBuildGadgetTreeGit(t *testing.T) { 3651 t.Parallel() 3652 if testing.Short() { 3653 t.Skip("skipping test in short mode.") 3654 } 3655 asserter := helper.Asserter{T: t} 3656 restoreCWD := testhelper.SaveCWD() 3657 defer restoreCWD() 3658 3659 var stateMachine ClassicStateMachine 3660 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3661 stateMachine.parent = &stateMachine 3662 3663 err := stateMachine.makeTemporaryDirectories() 3664 asserter.AssertErrNil(err, true) 3665 3666 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 3667 3668 // test the directory method 3669 d, err := os.Getwd() 3670 asserter.AssertErrNil(err, true) 3671 sourcePath := filepath.Join(d, "testdata", "gadget_source") 3672 sourcePath = "file://" + sourcePath 3673 imageDef := imagedefinition.ImageDefinition{ 3674 Architecture: getHostArch(), 3675 Series: getHostSuite(), 3676 Gadget: &imagedefinition.Gadget{ 3677 GadgetURL: sourcePath, 3678 GadgetType: "directory", 3679 }, 3680 } 3681 3682 stateMachine.ImageDef = imageDef 3683 3684 err = stateMachine.buildGadgetTree() 3685 asserter.AssertErrNil(err, true) 3686 3687 // test the git method 3688 imageDef = imagedefinition.ImageDefinition{ 3689 Architecture: getHostArch(), 3690 Series: getHostSuite(), 3691 Gadget: &imagedefinition.Gadget{ 3692 GadgetURL: "https://github.com/snapcore/pc-gadget", 3693 GadgetType: "git", 3694 GadgetBranch: "classic", 3695 }, 3696 } 3697 3698 stateMachine.ImageDef = imageDef 3699 3700 err = stateMachine.buildGadgetTree() 3701 asserter.AssertErrNil(err, true) 3702 } 3703 3704 // TestBuildGadgetTreeDirectory tests the successful build of a gadget tree 3705 func TestBuildGadgetTreeDirectory(t *testing.T) { 3706 t.Parallel() 3707 if testing.Short() { 3708 t.Skip("skipping test in short mode.") 3709 } 3710 asserter := helper.Asserter{T: t} 3711 saveCWD := testhelper.SaveCWD() 3712 defer saveCWD() 3713 3714 var stateMachine ClassicStateMachine 3715 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3716 stateMachine.parent = &stateMachine 3717 3718 // need workdir set up for this 3719 err := stateMachine.makeTemporaryDirectories() 3720 asserter.AssertErrNil(err, true) 3721 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 3722 3723 // git clone the gadget into a /tmp dir 3724 gadgetDir, err := os.MkdirTemp("", "pc-gadget-") 3725 asserter.AssertErrNil(err, true) 3726 t.Cleanup(func() { os.RemoveAll(gadgetDir) }) 3727 gitCloneCommand := *exec.Command( 3728 "git", 3729 "clone", 3730 "--depth", 3731 "1", 3732 "--branch", 3733 "classic", 3734 "https://github.com/snapcore/pc-gadget", 3735 gadgetDir, 3736 ) 3737 err = gitCloneCommand.Run() 3738 asserter.AssertErrNil(err, true) 3739 3740 // now set up the image definition to build from this directory 3741 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 3742 Architecture: getHostArch(), 3743 Series: getHostSuite(), 3744 Gadget: &imagedefinition.Gadget{ 3745 GadgetURL: fmt.Sprintf("file://%s", gadgetDir), 3746 GadgetType: "directory", 3747 }, 3748 } 3749 3750 err = stateMachine.buildGadgetTree() 3751 asserter.AssertErrNil(err, true) 3752 3753 // now make sure the gadget.yaml is in the expected location 3754 // this was a bug reported by the CPC team 3755 err = stateMachine.prepareGadgetTree() 3756 asserter.AssertErrNil(err, true) 3757 err = stateMachine.loadGadgetYaml() 3758 asserter.AssertErrNil(err, true) 3759 } 3760 3761 func TestStateMachine_buildGadgetTree_paths(t *testing.T) { 3762 if testing.Short() { 3763 t.Skip("skipping test in short mode.") 3764 } 3765 asserter := helper.Asserter{T: t} 3766 // git clone the gadget into a /tmp dir 3767 originGadgetDir, err := os.MkdirTemp("", "pc-gadget-") 3768 asserter.AssertErrNil(err, true) 3769 t.Cleanup(func() { 3770 err = os.RemoveAll(originGadgetDir) 3771 if err != nil { 3772 t.Error(err) 3773 } 3774 }) 3775 gitCloneCommand := *exec.Command( 3776 "git", 3777 "clone", 3778 "--depth", 3779 "1", 3780 "--branch", 3781 "classic", 3782 "https://github.com/snapcore/pc-gadget", 3783 originGadgetDir, 3784 ) 3785 err = gitCloneCommand.Run() 3786 asserter.AssertErrNil(err, true) 3787 3788 tmpDir, err := os.MkdirTemp("", "") 3789 t.Cleanup(func() { 3790 err := osRemoveAll(tmpDir) 3791 if err != nil { 3792 t.Error(err) 3793 } 3794 }) 3795 3796 testCases := []struct { 3797 name string 3798 gadgetDir string 3799 }{ 3800 { 3801 name: "gadget URL poiting to an absolute dir", 3802 gadgetDir: originGadgetDir, 3803 }, 3804 { 3805 name: "gadget URL pointing to an absolute sub dir", 3806 gadgetDir: filepath.Join(tmpDir, "a", "b"), 3807 }, 3808 { 3809 name: "gadget URL pointing to a relative sub dir", 3810 gadgetDir: filepath.Join("a", "b"), 3811 }, 3812 } 3813 3814 for _, tc := range testCases { 3815 t.Run("test_build_gadget_tree_paths_"+tc.name, func(t *testing.T) { 3816 asserter := helper.Asserter{T: t} 3817 restoreCWD := testhelper.SaveCWD() 3818 defer restoreCWD() 3819 3820 var stateMachine ClassicStateMachine 3821 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3822 stateMachine.parent = &stateMachine 3823 3824 err := stateMachine.makeTemporaryDirectories() 3825 asserter.AssertErrNil(err, true) 3826 t.Cleanup(func() { 3827 err := os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 3828 if err != nil { 3829 t.Error(err) 3830 } 3831 }) 3832 3833 // move the original gadget dir to the desire location to test it will be found 3834 if originGadgetDir != tc.gadgetDir { 3835 fullGadgetDir := tc.gadgetDir 3836 if !filepath.IsAbs(tc.gadgetDir) { 3837 fullGadgetDir = filepath.Join(tmpDir, tc.gadgetDir) 3838 } 3839 3840 err = os.MkdirAll(filepath.Dir(fullGadgetDir), 0777) 3841 asserter.AssertErrNil(err, true) 3842 3843 err = os.Rename(originGadgetDir, fullGadgetDir) 3844 asserter.AssertErrNil(err, true) 3845 // move it back once the test is done 3846 t.Cleanup(func() { 3847 err := os.Rename(fullGadgetDir, originGadgetDir) 3848 if err != nil { 3849 t.Error(err) 3850 } 3851 }) 3852 } 3853 3854 // now set up the image definition to build from this directory 3855 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 3856 Architecture: getHostArch(), 3857 Series: getHostSuite(), 3858 Gadget: &imagedefinition.Gadget{ 3859 GadgetURL: fmt.Sprintf("file://%s", tc.gadgetDir), 3860 GadgetType: "directory", 3861 }, 3862 } 3863 3864 err = stateMachine.setConfDefDir(filepath.Join(tmpDir, "image_definition.yaml")) 3865 asserter.AssertErrNil(err, true) 3866 3867 err = stateMachine.buildGadgetTree() 3868 asserter.AssertErrNil(err, true) 3869 3870 // now make sure the gadget.yaml is in the expected location 3871 // this was a bug reported by the CPC team 3872 err = stateMachine.prepareGadgetTree() 3873 asserter.AssertErrNil(err, true) 3874 err = stateMachine.loadGadgetYaml() 3875 asserter.AssertErrNil(err, true) 3876 }) 3877 } 3878 } 3879 3880 // TestGadgetGadgetTargets tests using alternate make targets with gadget builds 3881 func TestGadgetGadgetTargets(t *testing.T) { 3882 testCases := []struct { 3883 name string 3884 target string 3885 expectedOutput string 3886 }{ 3887 { 3888 "default", 3889 "", 3890 "make target test1", 3891 }, 3892 { 3893 "test2", 3894 "test2", 3895 "make target test2", 3896 }, 3897 } 3898 for _, tc := range testCases { 3899 t.Run("test_gadget_make_targets_"+tc.name, func(t *testing.T) { 3900 asserter := helper.Asserter{T: t} 3901 restoreCWD := testhelper.SaveCWD() 3902 defer restoreCWD() 3903 3904 var stateMachine ClassicStateMachine 3905 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3906 stateMachine.parent = &stateMachine 3907 stateMachine.commonFlags.Debug = true 3908 3909 err := stateMachine.makeTemporaryDirectories() 3910 asserter.AssertErrNil(err, true) 3911 3912 wd, err := os.Getwd() 3913 asserter.AssertErrNil(err, true) 3914 gadgetSrc := filepath.Join(wd, "testdata", "gadget_source") 3915 imageDef := imagedefinition.ImageDefinition{ 3916 Architecture: getHostArch(), 3917 Series: getHostSuite(), 3918 Gadget: &imagedefinition.Gadget{ 3919 GadgetURL: fmt.Sprintf("file://%s", gadgetSrc), 3920 GadgetType: "directory", 3921 GadgetTarget: tc.target, 3922 }, 3923 } 3924 stateMachine.ImageDef = imageDef 3925 3926 // capture stdout, build the gadget tree, and make 3927 // sure the expected output matches the make target 3928 stdout, restoreStdout, err := helper.CaptureStd(&os.Stdout) 3929 defer restoreStdout() 3930 asserter.AssertErrNil(err, true) 3931 3932 err = stateMachine.buildGadgetTree() 3933 asserter.AssertErrNil(err, true) 3934 3935 // restore stdout and examine what was printed 3936 restoreStdout() 3937 readStdout, err := io.ReadAll(stdout) 3938 asserter.AssertErrNil(err, true) 3939 if !strings.Contains(string(readStdout), tc.expectedOutput) { 3940 t.Errorf("Expected make output\n\"%s\"\nto contain the string \"%s\"", 3941 string(readStdout), 3942 tc.expectedOutput, 3943 ) 3944 } 3945 }) 3946 } 3947 } 3948 3949 // TestFailedBuildGadgetTree tests failures in the buildGadgetTree function 3950 func TestFailedBuildGadgetTree(t *testing.T) { 3951 asserter := helper.Asserter{T: t} 3952 restoreCWD := testhelper.SaveCWD() 3953 defer restoreCWD() 3954 3955 var stateMachine ClassicStateMachine 3956 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 3957 stateMachine.parent = &stateMachine 3958 3959 err := stateMachine.makeTemporaryDirectories() 3960 asserter.AssertErrNil(err, true) 3961 3962 // mock os.MkdirAll 3963 osMkdir = mockMkdir 3964 t.Cleanup(func() { 3965 osMkdir = os.Mkdir 3966 }) 3967 err = stateMachine.buildGadgetTree() 3968 asserter.AssertErrContains(err, "Error creating scratch/gadget") 3969 osMkdir = os.Mkdir 3970 3971 // try to clone a repo that doesn't exist 3972 imageDef := imagedefinition.ImageDefinition{ 3973 Architecture: getHostArch(), 3974 Series: getHostSuite(), 3975 Gadget: &imagedefinition.Gadget{ 3976 GadgetURL: "http://fakerepo.git", 3977 GadgetType: "git", 3978 }, 3979 } 3980 stateMachine.ImageDef = imageDef 3981 3982 err = stateMachine.buildGadgetTree() 3983 asserter.AssertErrContains(err, "Error cloning gadget repository") 3984 3985 // try to copy a file that doesn't exist 3986 imageDef = imagedefinition.ImageDefinition{ 3987 Architecture: getHostArch(), 3988 Series: getHostSuite(), 3989 Gadget: &imagedefinition.Gadget{ 3990 GadgetURL: "file:///fake/file/that/does/not/exist", 3991 GadgetType: "directory", 3992 }, 3993 } 3994 stateMachine.ImageDef = imageDef 3995 3996 err = stateMachine.buildGadgetTree() 3997 asserter.AssertErrContains(err, "Error reading gadget tree") 3998 3999 // mock osutil.CopySpecialFile and run with /tmp as the gadget source 4000 imageDef = imagedefinition.ImageDefinition{ 4001 Architecture: getHostArch(), 4002 Series: getHostSuite(), 4003 Gadget: &imagedefinition.Gadget{ 4004 GadgetURL: "file:///tmp", 4005 GadgetType: "directory", 4006 }, 4007 } 4008 stateMachine.ImageDef = imageDef 4009 4010 // mock osutil.CopySpecialFile 4011 osutilCopySpecialFile = mockCopySpecialFile 4012 t.Cleanup(func() { 4013 osutilCopySpecialFile = osutil.CopySpecialFile 4014 }) 4015 err = stateMachine.buildGadgetTree() 4016 asserter.AssertErrContains(err, "Error copying gadget source") 4017 osutilCopySpecialFile = osutil.CopySpecialFile 4018 4019 // run a "make" command that will fail by mocking exec.Command 4020 testCaseName = "TestFailedBuildGadgetTree" 4021 execCommand = fakeExecCommand 4022 t.Cleanup(func() { 4023 execCommand = exec.Command 4024 }) 4025 wd, err := os.Getwd() 4026 asserter.AssertErrNil(err, true) 4027 sourcePath := filepath.Join(wd, "testdata", "gadget_source") 4028 sourcePath = "file://" + sourcePath 4029 imageDef = imagedefinition.ImageDefinition{ 4030 Architecture: getHostArch(), 4031 Series: getHostSuite(), 4032 Gadget: &imagedefinition.Gadget{ 4033 GadgetURL: sourcePath, 4034 GadgetType: "directory", 4035 }, 4036 } 4037 stateMachine.ImageDef = imageDef 4038 4039 err = stateMachine.buildGadgetTree() 4040 asserter.AssertErrContains(err, "Error running \"make\" in gadget source") 4041 4042 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 4043 } 4044 4045 // TestCreateChroot runs the createChroot step and spot checks that some 4046 // expected files in the chroot exist 4047 func TestCreateChroot(t *testing.T) { 4048 if testing.Short() { 4049 t.Skip("skipping test in short mode.") 4050 } 4051 asserter := helper.Asserter{T: t} 4052 restoreCWD := testhelper.SaveCWD() 4053 defer restoreCWD() 4054 4055 var stateMachine ClassicStateMachine 4056 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4057 stateMachine.parent = &stateMachine 4058 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4059 Architecture: getHostArch(), 4060 Series: getHostSuite(), 4061 Rootfs: &imagedefinition.Rootfs{ 4062 Pocket: "proposed", 4063 SourcesListDeb822: helper.BoolPtr(true), 4064 }, 4065 } 4066 4067 err := helper.SetDefaults(&stateMachine.ImageDef) 4068 asserter.AssertErrNil(err, true) 4069 4070 err = stateMachine.makeTemporaryDirectories() 4071 asserter.AssertErrNil(err, true) 4072 4073 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 4074 4075 err = stateMachine.createChroot() 4076 asserter.AssertErrNil(err, true) 4077 4078 expectedFiles := []string{ 4079 "etc", 4080 "home", 4081 "boot", 4082 "var", 4083 } 4084 for _, expectedFile := range expectedFiles { 4085 fullPath := filepath.Join(stateMachine.tempDirs.chroot, expectedFile) 4086 _, err := os.Stat(fullPath) 4087 if err != nil { 4088 if os.IsNotExist(err) { 4089 t.Errorf("File \"%s\" should exist, but does not", fullPath) 4090 } 4091 } 4092 } 4093 4094 // check that the hostname is set correctly 4095 hostnameFile := filepath.Join(stateMachine.tempDirs.chroot, "etc", "hostname") 4096 hostnameData, err := os.ReadFile(hostnameFile) 4097 asserter.AssertErrNil(err, true) 4098 if string(hostnameData) != "ubuntu\n" { 4099 t.Errorf("Expected hostname to be \"ubuntu\", but is \"%s\"", string(hostnameData)) 4100 } 4101 4102 // check that the resolv.conf file was truncated 4103 resolvConfFile := filepath.Join(stateMachine.tempDirs.chroot, "etc", "resolv.conf") 4104 resolvConfData, err := os.ReadFile(resolvConfFile) 4105 asserter.AssertErrNil(err, true) 4106 if string(resolvConfData) != "" { 4107 t.Errorf("Expected resolv.conf to be empty, but is \"%s\"", string(resolvConfData)) 4108 } 4109 4110 // check if components and pocket correctly setup in /etc/apt/sources.list.d/ubuntu.sources 4111 aptDeb822SourcesListBytes, err := os.ReadFile(filepath.Join(stateMachine.tempDirs.chroot, "etc", "apt", "sources.list.d", "ubuntu.sources")) 4112 asserter.AssertErrNil(err, true) 4113 wantAptDeb822SourcesList := `## Ubuntu distribution repository 4114 ## 4115 ## The following settings can be adjusted to configure which packages to use from Ubuntu. 4116 ## Mirror your choices (except for URIs and Suites) in the security section below to 4117 ## ensure timely security updates. 4118 ## 4119 ## Types: Append deb-src to enable the fetching of source package. 4120 ## URIs: A URL to the repository (you may add multiple URLs) 4121 ## Suites: The following additional suites can be configured 4122 ## <name>-updates - Major bug fix updates produced after the final release of the 4123 ## distribution. 4124 ## <name>-backports - software from this repository may not have been tested as 4125 ## extensively as that contained in the main release, although it includes 4126 ## newer versions of some applications which may provide useful features. 4127 ## Also, please note that software in backports WILL NOT receive any review 4128 ## or updates from the Ubuntu security team. 4129 ## Components: Aside from main, the following components can be added to the list 4130 ## restricted - Software that may not be under a free license, or protected by patents. 4131 ## universe - Community maintained packages. Software in this repository receives maintenance 4132 ## from volunteers in the Ubuntu community, or a 10 year security maintenance 4133 ## commitment from Canonical when an Ubuntu Pro subscription is attached. 4134 ## multiverse - Community maintained of restricted. Software from this repository is 4135 ## ENTIRELY UNSUPPORTED by the Ubuntu team, and may not be under a free 4136 ## licence. Please satisfy yourself as to your rights to use the software. 4137 ## Also, please note that software in multiverse WILL NOT receive any 4138 ## review or updates from the Ubuntu security team. 4139 ## 4140 ## See the sources.list(5) manual page for further settings. 4141 Types: deb 4142 URIs: http://archive.ubuntu.com/ubuntu/ 4143 Suites: jammy jammy-updates jammy-proposed 4144 Components: main restricted 4145 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 4146 4147 ## Ubuntu security updates. Aside from URIs and Suites, 4148 ## this should mirror your choices in the previous section. 4149 Types: deb 4150 URIs: http://security.ubuntu.com/ubuntu/ 4151 Suites: jammy jammy-updates jammy-proposed 4152 Components: main restricted 4153 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 4154 4155 ` 4156 asserter.AssertEqual(wantAptDeb822SourcesList, string(aptDeb822SourcesListBytes)) 4157 4158 } 4159 4160 // TestFailedCreateChroot tests failure cases in createChroot 4161 func TestFailedCreateChroot(t *testing.T) { 4162 if testing.Short() { 4163 t.Skip("skipping test in short mode.") 4164 } 4165 asserter := helper.Asserter{T: t} 4166 restoreCWD := testhelper.SaveCWD() 4167 defer restoreCWD() 4168 4169 var stateMachine ClassicStateMachine 4170 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4171 stateMachine.parent = &stateMachine 4172 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4173 Architecture: getHostArch(), 4174 Series: getHostSuite(), 4175 Rootfs: &imagedefinition.Rootfs{ 4176 SourcesListDeb822: helper.BoolPtr(false), 4177 }, 4178 } 4179 4180 err := stateMachine.makeTemporaryDirectories() 4181 asserter.AssertErrNil(err, true) 4182 4183 // mock os.Mkdir 4184 osMkdir = mockMkdir 4185 t.Cleanup(func() { 4186 osMkdir = os.Mkdir 4187 }) 4188 err = stateMachine.createChroot() 4189 asserter.AssertErrContains(err, "Failed to create chroot") 4190 osMkdir = os.Mkdir 4191 4192 // Setup the exec.Command mock 4193 testCaseName = "TestFailedCreateChroot" 4194 execCommand = fakeExecCommand 4195 t.Cleanup(func() { 4196 execCommand = exec.Command 4197 }) 4198 err = stateMachine.createChroot() 4199 asserter.AssertErrContains(err, "Error running debootstrap command") 4200 execCommand = exec.Command 4201 4202 // Check if failure of open hostname file is detected 4203 4204 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 4205 err = stateMachine.makeTemporaryDirectories() 4206 asserter.AssertErrNil(err, true) 4207 4208 // Prepare a fallthrough debootstrap 4209 testCaseName = "TestFailedCreateChrootNoHostname" 4210 execCommand = fakeExecCommand 4211 t.Cleanup(func() { 4212 execCommand = exec.Command 4213 }) 4214 osOpenFile = mockOpenFile 4215 t.Cleanup(func() { 4216 osOpenFile = os.OpenFile 4217 }) 4218 4219 err = stateMachine.createChroot() 4220 asserter.AssertErrContains(err, "unable to open hostname file") 4221 4222 osOpenFile = os.OpenFile 4223 execCommand = exec.Command 4224 4225 // Check if failure of truncation is detected 4226 4227 // Clean the work directory 4228 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 4229 err = stateMachine.makeTemporaryDirectories() 4230 asserter.AssertErrNil(err, true) 4231 4232 // Prepare a fallthrough debootstrap 4233 testCaseName = "TestFailedCreateChrootSkip" 4234 osTruncate = mockTruncate 4235 t.Cleanup(func() { 4236 osTruncate = os.Truncate 4237 }) 4238 err = stateMachine.createChroot() 4239 asserter.AssertErrContains(err, "Error truncating resolv.conf") 4240 osTruncate = os.Truncate 4241 execCommand = exec.Command 4242 4243 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 4244 } 4245 4246 // TestStateMachine_installPackages_checkcmds checks commands to install packages order is ok 4247 func TestStateMachine_installPackages_checkcmds(t *testing.T) { 4248 asserter := helper.Asserter{T: t} 4249 var stateMachine ClassicStateMachine 4250 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4251 stateMachine.commonFlags.Debug = true 4252 stateMachine.parent = &stateMachine 4253 stateMachine.commonFlags.OutputDir = "/tmp" 4254 4255 err := stateMachine.makeTemporaryDirectories() 4256 asserter.AssertErrNil(err, true) 4257 err = os.MkdirAll(stateMachine.tempDirs.chroot, 0755) 4258 asserter.AssertErrNil(err, true) 4259 4260 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 4261 4262 // create an /usr/sbin/policy-rc.d in the chroot 4263 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "usr", "sbin"), 0755) 4264 asserter.AssertErrNil(err, true) 4265 _, err = os.Create(filepath.Join(stateMachine.tempDirs.chroot, "usr", "sbin", "policy-rc.d")) 4266 asserter.AssertErrNil(err, true) 4267 4268 // create an /sbin/start-stop-daemon in the chroot 4269 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "sbin"), 0755) 4270 asserter.AssertErrNil(err, true) 4271 _, err = os.Create(filepath.Join(stateMachine.tempDirs.chroot, "sbin", "start-stop-daemon")) 4272 asserter.AssertErrNil(err, true) 4273 4274 // create an /sbin/initctl in the chroot 4275 _, err = os.Create(filepath.Join(stateMachine.tempDirs.chroot, "sbin", "initctl")) 4276 asserter.AssertErrNil(err, true) 4277 4278 mockCmder := NewMockExecCommand() 4279 4280 execCommand = mockCmder.Command 4281 t.Cleanup(func() { execCommand = exec.Command }) 4282 4283 stdout, restoreStdout, err := helper.CaptureStd(&os.Stdout) 4284 asserter.AssertErrNil(err, true) 4285 t.Cleanup(func() { restoreStdout() }) 4286 4287 helperBackupAndCopyResolvConf = mockBackupAndCopyResolvConfSuccess 4288 t.Cleanup(func() { 4289 helperBackupAndCopyResolvConf = helper.BackupAndCopyResolvConf 4290 }) 4291 4292 err = stateMachine.installPackages() 4293 asserter.AssertErrNil(err, true) 4294 4295 restoreStdout() 4296 readStdout, err := io.ReadAll(stdout) 4297 asserter.AssertErrNil(err, true) 4298 4299 expectedCmds := []*regexp.Regexp{ 4300 regexp.MustCompile("^mount -t devtmpfs devtmpfs-build /tmp.*/chroot/dev$"), 4301 regexp.MustCompile("^mount -t devpts devpts-build -o nodev,nosuid /tmp.*/chroot/dev/pts$"), 4302 regexp.MustCompile("^mount -t proc proc-build /tmp.*/chroot/proc$"), 4303 regexp.MustCompile("^mount -t sysfs sysfs-build /tmp.*/chroot/sys$"), 4304 regexp.MustCompile("^mount --bind .*/scratch/run.* .*/chroot/run$"), 4305 regexp.MustCompile("^chroot /tmp.*/chroot dpkg-divert"), 4306 regexp.MustCompile("^chroot /tmp.*/chroot apt update$"), 4307 regexp.MustCompile("^chroot /tmp.*/chroot apt install --assume-yes --quiet --option=Dpkg::options::=--force-unsafe-io --option=Dpkg::Options::=--force-confold$"), 4308 regexp.MustCompile("^chroot /tmp.*/chroot dpkg-divert --remove"), 4309 regexp.MustCompile("^udevadm settle$"), 4310 regexp.MustCompile("^mount --make-rprivate /tmp.*/chroot/run$"), 4311 regexp.MustCompile("^umount --recursive /tmp.*/chroot/run$"), 4312 regexp.MustCompile("^mount --make-rprivate /tmp.*/chroot/sys$"), 4313 regexp.MustCompile("^umount --recursive /tmp.*/chroot/sys$"), 4314 regexp.MustCompile("^mount --make-rprivate /tmp.*/chroot/proc$"), 4315 regexp.MustCompile("^umount --recursive /tmp.*/chroot/proc$"), 4316 regexp.MustCompile("^mount --make-rprivate /tmp.*/chroot/dev/pts$"), 4317 regexp.MustCompile("^umount --recursive /tmp.*/chroot/dev/pts$"), 4318 regexp.MustCompile("^mount --make-rprivate /tmp.*/chroot/dev$"), 4319 regexp.MustCompile("^umount --recursive /tmp.*/chroot/dev$"), 4320 } 4321 4322 gotCmds := strings.Split(strings.TrimSpace(string(readStdout)), "\n") 4323 if len(expectedCmds) != len(gotCmds) { 4324 t.Fatalf("%v commands to be executed, expected %v commands. Got: %v", len(gotCmds), len(expectedCmds), gotCmds) 4325 } 4326 4327 for i, gotCmd := range gotCmds { 4328 expected := expectedCmds[i] 4329 4330 if !expected.Match([]byte(gotCmd)) { 4331 t.Errorf("Cmd \"%v\" not matching. Expected %v\n", gotCmd, expected.String()) 4332 } 4333 } 4334 } 4335 4336 // TestStateMachine_installPackages_checkcmds checks commands to install packages order is ok when failing 4337 func TestStateMachine_installPackages_checkcmds_failing(t *testing.T) { 4338 asserter := helper.Asserter{T: t} 4339 var stateMachine ClassicStateMachine 4340 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4341 stateMachine.commonFlags.Debug = true 4342 stateMachine.parent = &stateMachine 4343 stateMachine.commonFlags.OutputDir = "/tmp" 4344 4345 err := stateMachine.makeTemporaryDirectories() 4346 asserter.AssertErrNil(err, true) 4347 4348 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 4349 4350 mockCmder := NewMockExecCommand() 4351 4352 execCommand = mockCmder.Command 4353 t.Cleanup(func() { execCommand = exec.Command }) 4354 4355 stdout, restoreStdout, err := helper.CaptureStd(&os.Stdout) 4356 asserter.AssertErrNil(err, true) 4357 t.Cleanup(func() { restoreStdout() }) 4358 4359 helperBackupAndCopyResolvConf = mockBackupAndCopyResolvConfSuccess 4360 t.Cleanup(func() { 4361 helperBackupAndCopyResolvConf = helper.BackupAndCopyResolvConf 4362 }) 4363 4364 osMkdirTemp = mockMkdirTemp 4365 t.Cleanup(func() { 4366 osMkdirTemp = os.MkdirTemp 4367 }) 4368 4369 err = stateMachine.installPackages() 4370 asserter.AssertErrContains(err, "Test error") 4371 4372 restoreStdout() 4373 readStdout, err := io.ReadAll(stdout) 4374 asserter.AssertErrNil(err, true) 4375 4376 gotCmds := strings.Split(strings.TrimSpace(string(readStdout)), "\n") 4377 // Clean empty commands 4378 for i, cmd := range gotCmds { 4379 if len(cmd) == 0 { 4380 copy(gotCmds[i:], gotCmds[i+1:]) 4381 gotCmds[len(gotCmds)-1] = "" 4382 gotCmds = gotCmds[:len(gotCmds)-1] 4383 } 4384 } 4385 4386 if len(gotCmds) != 0 { 4387 t.Fatalf("%v commands to be executed, expected no commands. Got: %v", len(gotCmds), gotCmds) 4388 } 4389 } 4390 4391 // TestStateMachine_installPackages_fail tests failure cases in installPackages 4392 func TestStateMachine_installPackages_fail(t *testing.T) { 4393 asserter := helper.Asserter{T: t} 4394 restoreCWD := testhelper.SaveCWD() 4395 defer restoreCWD() 4396 4397 var stateMachine ClassicStateMachine 4398 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4399 stateMachine.parent = &stateMachine 4400 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4401 Architecture: getHostArch(), 4402 Series: getHostSuite(), 4403 Rootfs: &imagedefinition.Rootfs{}, 4404 Customization: &imagedefinition.Customization{ 4405 ExtraPackages: []*imagedefinition.Package{ 4406 { 4407 PackageName: "test1", 4408 }, 4409 }, 4410 }, 4411 } 4412 4413 err := stateMachine.makeTemporaryDirectories() 4414 asserter.AssertErrNil(err, true) 4415 4416 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 4417 4418 // create an /etc/resolv.conf in the chroot 4419 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "etc"), 0755) 4420 asserter.AssertErrNil(err, true) 4421 _, err = os.Create(filepath.Join(stateMachine.tempDirs.chroot, "etc", "resolv.conf")) 4422 asserter.AssertErrNil(err, true) 4423 4424 osMkdirTemp = mockMkdirTemp 4425 t.Cleanup(func() { 4426 osMkdirTemp = os.MkdirTemp 4427 }) 4428 err = stateMachine.installPackages() 4429 asserter.AssertErrContains(err, "Error making temporary directory for mountpoint") 4430 osMkdirTemp = os.MkdirTemp 4431 4432 // Setup the exec.Command mock 4433 testCaseName = "TestStateMachine_installPackages_fail" 4434 execCommand = fakeExecCommand 4435 t.Cleanup(func() { 4436 execCommand = exec.Command 4437 }) 4438 err = stateMachine.installPackages() 4439 asserter.AssertErrContains(err, "Error running command") 4440 execCommand = exec.Command 4441 4442 // delete the backed up resolv.conf to trigger another backup 4443 err = os.Remove(filepath.Join(stateMachine.tempDirs.chroot, "etc", "resolv.conf.tmp")) 4444 asserter.AssertErrNil(err, true) 4445 // mock helper.BackupAndCopyResolvConf 4446 helperBackupAndCopyResolvConf = mockBackupAndCopyResolvConfFail 4447 t.Cleanup(func() { 4448 helperBackupAndCopyResolvConf = helper.BackupAndCopyResolvConf 4449 }) 4450 err = stateMachine.installPackages() 4451 asserter.AssertErrContains(err, "Error setting up /etc/resolv.conf") 4452 helperBackupAndCopyResolvConf = helper.BackupAndCopyResolvConf 4453 4454 osMkdirAll = mockMkdirAll 4455 t.Cleanup(func() { 4456 osMkdirAll = os.MkdirAll 4457 }) 4458 err = stateMachine.installPackages() 4459 asserter.AssertErrContains(err, "Error creating policy-rc.d dir") 4460 osMkdirAll = os.MkdirAll 4461 4462 osWriteFile = mockWriteFile 4463 t.Cleanup(func() { 4464 osWriteFile = os.WriteFile 4465 }) 4466 err = stateMachine.installPackages() 4467 asserter.AssertErrContains(err, "Error writing to policy-rc.d") 4468 osWriteFile = os.WriteFile 4469 4470 osRename = mockRename 4471 t.Cleanup(func() { 4472 osRename = os.Rename 4473 }) 4474 err = stateMachine.installPackages() 4475 asserter.AssertErrContains(err, "Error moving file ") 4476 osRename = os.Rename 4477 4478 } 4479 4480 // Test_generateMountPointCmds_fail tests when generateMountPointCmds fails 4481 func Test_generateMountPointCmds_fail(t *testing.T) { 4482 asserter := helper.Asserter{T: t} 4483 4484 tmpDirPath := filepath.Join("/tmp", "test_failed_set_conf_dir") 4485 err := os.Mkdir(tmpDirPath, 0755) 4486 t.Cleanup(func() { 4487 os.RemoveAll(tmpDirPath) 4488 }) 4489 asserter.AssertErrNil(err, true) 4490 4491 mountPoints := []*mountPoint{ 4492 { 4493 src: "devtmpfs-build", 4494 basePath: tmpDirPath, 4495 relpath: "/dev", 4496 typ: "devtmpfs", 4497 }, 4498 { 4499 src: "doesnotexists", 4500 basePath: "/doesnotexists", 4501 relpath: "/doesnotexists", 4502 typ: "devpts", 4503 bind: true, 4504 opts: []string{"nodev", "nosuid"}, 4505 }, 4506 } 4507 4508 gotAllMountCmds, gotAllUmountCmds, err := generateMountPointCmds(mountPoints, tmpDirPath) 4509 asserter.AssertErrContains(err, "Error preparing mountpoint") 4510 asserter.AssertEqual(nil, gotAllMountCmds) 4511 asserter.AssertEqual(nil, gotAllUmountCmds) 4512 4513 } 4514 4515 // TestCustomizeFstab tests functionality of the customizeFstab function 4516 func TestCustomizeFstab(t *testing.T) { 4517 testCases := []struct { 4518 name string 4519 fstab []*imagedefinition.Fstab 4520 expectedFstab string 4521 existingFstab string 4522 }{ 4523 { 4524 name: "one entry to an empty fstab", 4525 fstab: []*imagedefinition.Fstab{ 4526 { 4527 Label: "writable", 4528 Mountpoint: "/", 4529 FSType: "ext4", 4530 MountOptions: "defaults", 4531 Dump: true, 4532 FsckOrder: 1, 4533 }, 4534 }, 4535 expectedFstab: `LABEL=writable / ext4 defaults 1 1 4536 `, 4537 }, 4538 { 4539 name: "one entry to a non-empty fstab", 4540 fstab: []*imagedefinition.Fstab{ 4541 { 4542 Label: "writable", 4543 Mountpoint: "/", 4544 FSType: "ext4", 4545 MountOptions: "defaults", 4546 Dump: true, 4547 FsckOrder: 1, 4548 }, 4549 }, 4550 expectedFstab: `LABEL=writable / ext4 defaults 1 1 4551 `, 4552 existingFstab: `LABEL=xxx / ext4 discard,errors=remount-ro 0 1`, 4553 }, 4554 { 4555 name: "two entries", 4556 fstab: []*imagedefinition.Fstab{ 4557 { 4558 Label: "writable", 4559 Mountpoint: "/", 4560 FSType: "ext4", 4561 MountOptions: "defaults", 4562 Dump: false, 4563 FsckOrder: 1, 4564 }, 4565 { 4566 Label: "system-boot", 4567 Mountpoint: "/boot/firmware", 4568 FSType: "vfat", 4569 MountOptions: "defaults", 4570 Dump: false, 4571 FsckOrder: 1, 4572 }, 4573 }, 4574 expectedFstab: `LABEL=writable / ext4 defaults 0 1 4575 LABEL=system-boot /boot/firmware vfat defaults 0 1 4576 `, 4577 }, 4578 } 4579 4580 for _, tc := range testCases { 4581 t.Run(tc.name, func(t *testing.T) { 4582 asserter := helper.Asserter{T: t} 4583 restoreCWD := testhelper.SaveCWD() 4584 defer restoreCWD() 4585 4586 var stateMachine ClassicStateMachine 4587 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4588 stateMachine.parent = &stateMachine 4589 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4590 Architecture: getHostArch(), 4591 Series: getHostSuite(), 4592 Rootfs: &imagedefinition.Rootfs{}, 4593 Customization: &imagedefinition.Customization{ 4594 Fstab: tc.fstab, 4595 }, 4596 } 4597 4598 // set the defaults for the imageDef 4599 err := helper.SetDefaults(&stateMachine.ImageDef) 4600 asserter.AssertErrNil(err, true) 4601 4602 err = stateMachine.makeTemporaryDirectories() 4603 asserter.AssertErrNil(err, true) 4604 4605 // create the <chroot>/etc directory 4606 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "etc"), 0644) 4607 asserter.AssertErrNil(err, true) 4608 4609 fstabPath := filepath.Join(stateMachine.tempDirs.chroot, "etc", "fstab") 4610 4611 // simulate an already existing fstab file 4612 if len(tc.existingFstab) != 0 { 4613 err = osWriteFile(fstabPath, []byte(tc.existingFstab), 0644) 4614 asserter.AssertErrNil(err, true) 4615 } 4616 4617 // customize the fstab, ensure no errors, and check the contents 4618 err = stateMachine.customizeFstab() 4619 asserter.AssertErrNil(err, true) 4620 4621 fstabBytes, err := os.ReadFile(fstabPath) 4622 asserter.AssertErrNil(err, true) 4623 4624 if string(fstabBytes) != tc.expectedFstab { 4625 t.Errorf("Expected fstab contents \"%s\", but got \"%s\"", 4626 tc.expectedFstab, string(fstabBytes)) 4627 } 4628 }) 4629 } 4630 } 4631 4632 // TestStateMachine_customizeFstab_fail tests failures in the customizeFstab function 4633 func TestStateMachine_customizeFstab_fail(t *testing.T) { 4634 asserter := helper.Asserter{T: t} 4635 restoreCWD := testhelper.SaveCWD() 4636 defer restoreCWD() 4637 4638 var stateMachine ClassicStateMachine 4639 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4640 stateMachine.parent = &stateMachine 4641 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4642 Architecture: getHostArch(), 4643 Series: getHostSuite(), 4644 Rootfs: &imagedefinition.Rootfs{}, 4645 Customization: &imagedefinition.Customization{ 4646 Fstab: []*imagedefinition.Fstab{ 4647 { 4648 Label: "writable", 4649 Mountpoint: "/", 4650 FSType: "ext4", 4651 MountOptions: "defaults", 4652 Dump: false, 4653 FsckOrder: 1, 4654 }, 4655 }, 4656 }, 4657 } 4658 4659 osOpenFile = mockOpenFile 4660 t.Cleanup(func() { 4661 osOpenFile = os.OpenFile 4662 }) 4663 err := stateMachine.customizeFstab() 4664 asserter.AssertErrContains(err, "Error opening fstab") 4665 } 4666 4667 // TestGenerateRootfsTarball tests that a rootfs tarball is generated 4668 // when appropriate and that it contains the correct files 4669 func TestGenerateRootfsTarball(t *testing.T) { 4670 testCases := []struct { 4671 name string // the name will double as the compression type 4672 tarPath string 4673 fileType string 4674 }{ 4675 { 4676 "uncompressed", 4677 "test_generate_rootfs_tarball.tar", 4678 "tar archive", 4679 }, 4680 { 4681 "bzip2", 4682 "test_generate_rootfs_tarball.tar.bz2", 4683 "bzip2 compressed data", 4684 }, 4685 { 4686 "gzip", 4687 "test_generate_rootfs_tarball.tar.gz", 4688 "gzip compressed data", 4689 }, 4690 { 4691 "xz", 4692 "test_generate_rootfs_tarball.tar.xz", 4693 "XZ compressed data", 4694 }, 4695 { 4696 "zstd", 4697 "test_generate_rootfs_tarball.tar.zst", 4698 "Zstandard compressed data", 4699 }, 4700 } 4701 for _, tc := range testCases { 4702 t.Run(tc.name, func(t *testing.T) { 4703 asserter := helper.Asserter{T: t} 4704 restoreCWD := testhelper.SaveCWD() 4705 defer restoreCWD() 4706 4707 var stateMachine ClassicStateMachine 4708 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4709 stateMachine.parent = &stateMachine 4710 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4711 Architecture: getHostArch(), 4712 Series: getHostSuite(), 4713 Rootfs: &imagedefinition.Rootfs{}, 4714 Artifacts: &imagedefinition.Artifact{ 4715 RootfsTar: &imagedefinition.RootfsTar{ 4716 RootfsTarName: tc.tarPath, 4717 Compression: tc.name, 4718 }, 4719 }, 4720 } 4721 4722 err := stateMachine.makeTemporaryDirectories() 4723 asserter.AssertErrNil(err, true) 4724 stateMachine.commonFlags.OutputDir = stateMachine.stateMachineFlags.WorkDir 4725 4726 err = stateMachine.generateRootfsTarball() 4727 asserter.AssertErrNil(err, true) 4728 4729 // make sure tar archive exists and is the correct compression type 4730 _, err = os.Stat(filepath.Join(stateMachine.stateMachineFlags.WorkDir, tc.tarPath)) 4731 if err != nil { 4732 t.Errorf("File %s should be in workdir, but is missing", tc.tarPath) 4733 } 4734 4735 fullPath := filepath.Join(stateMachine.commonFlags.OutputDir, tc.tarPath) 4736 fileCommand := *exec.Command("file", fullPath) 4737 cmdOutput, err := fileCommand.CombinedOutput() 4738 asserter.AssertErrNil(err, true) 4739 if !strings.Contains(string(cmdOutput), tc.fileType) { 4740 t.Errorf("File \"%s\" is the wrong file type. Expected \"%s\" but got \"%s\"", 4741 fullPath, tc.fileType, string(cmdOutput)) 4742 } 4743 }) 4744 } 4745 } 4746 4747 // TestTarXattrs sets an xattr on a file, puts it in a tar archive, 4748 // extracts the tar archive and ensures the xattr is still present 4749 func TestTarXattrs(t *testing.T) { 4750 asserter := helper.Asserter{T: t} 4751 restoreCWD := testhelper.SaveCWD() 4752 defer restoreCWD() 4753 4754 var stateMachine ClassicStateMachine 4755 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4756 stateMachine.parent = &stateMachine 4757 4758 // create a file with xattrs in a temporary directory 4759 xattrBytes := []byte("ui-test") 4760 testDir, err := os.MkdirTemp("/tmp", "ubuntu-image-xattr-test") 4761 asserter.AssertErrNil(err, true) 4762 extractDir, err := os.MkdirTemp("/tmp", "ubuntu-image-xattr-test") 4763 asserter.AssertErrNil(err, true) 4764 testFile, err := os.CreateTemp(testDir, "test-xattrs-") 4765 asserter.AssertErrNil(err, true) 4766 testFileName := filepath.Base(testFile.Name()) 4767 t.Cleanup(func() { os.RemoveAll(testDir) }) 4768 t.Cleanup(func() { os.RemoveAll(extractDir) }) 4769 4770 err = xattr.FSet(testFile, "user.test", xattrBytes) 4771 asserter.AssertErrNil(err, true) 4772 4773 // now run the helper tar creation and extraction functions 4774 tarPath := filepath.Join(testDir, "test-xattrs.tar") 4775 err = helper.CreateTarArchive(testDir, tarPath, "uncompressed", false, false) 4776 asserter.AssertErrNil(err, true) 4777 4778 err = helper.ExtractTarArchive(tarPath, extractDir, false, false) 4779 asserter.AssertErrNil(err, true) 4780 4781 // now read the extracted file's extended attributes 4782 finalXattrs, err := xattr.List(filepath.Join(extractDir, testFileName)) 4783 asserter.AssertErrNil(err, true) 4784 4785 if !reflect.DeepEqual(finalXattrs, []string{"user.test"}) { 4786 t.Errorf("test file \"%s\" does not have correct xattrs set", testFile.Name()) 4787 } 4788 } 4789 4790 // TestPingXattrs runs the ExtractTarArchive file on a pre-made test file that contains /bin/ping 4791 // and ensures that the security.capability extended attribute is still present 4792 func TestPingXattrs(t *testing.T) { 4793 asserter := helper.Asserter{T: t} 4794 restoreCWD := testhelper.SaveCWD() 4795 defer restoreCWD() 4796 4797 var stateMachine ClassicStateMachine 4798 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4799 stateMachine.parent = &stateMachine 4800 4801 testDir, err := os.MkdirTemp("/tmp", "ubuntu-image-ping-xattr-test") 4802 asserter.AssertErrNil(err, true) 4803 t.Cleanup(func() { os.RemoveAll(testDir) }) 4804 testFile := filepath.Join("testdata", "rootfs_tarballs", "ping.tar") 4805 4806 err = helper.ExtractTarArchive(testFile, testDir, true, true) 4807 asserter.AssertErrNil(err, true) 4808 4809 binPing := filepath.Join(testDir, "bin", "ping") 4810 pingXattrs, err := xattr.List(binPing) 4811 asserter.AssertErrNil(err, true) 4812 if !reflect.DeepEqual(pingXattrs, []string{"security.capability"}) { 4813 t.Error("ping has lost the security.capability xattr after tar extraction") 4814 } 4815 } 4816 4817 // TestFailedMakeQcow2Img tests failures in the makeQcow2Img function 4818 func TestFailedMakeQcow2Img(t *testing.T) { 4819 asserter := helper.Asserter{T: t} 4820 restoreCWD := testhelper.SaveCWD() 4821 defer restoreCWD() 4822 4823 var stateMachine ClassicStateMachine 4824 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4825 stateMachine.parent = &stateMachine 4826 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4827 Architecture: getHostArch(), 4828 Series: getHostSuite(), 4829 Artifacts: &imagedefinition.Artifact{ 4830 Qcow2: &[]imagedefinition.Qcow2{ 4831 { 4832 Qcow2Name: "test.qcow2", 4833 }, 4834 }, 4835 }, 4836 } 4837 4838 // Setup the exec.Command mock 4839 testCaseName = "TestFailedMakeQcow2Image" 4840 execCommand = fakeExecCommand 4841 defer func() { 4842 execCommand = exec.Command 4843 }() 4844 4845 err := stateMachine.makeQcow2Img() 4846 asserter.AssertErrContains(err, "Error creating qcow2 artifact") 4847 } 4848 4849 // TestPreseedResetChroot tests that calling prepareClassicImage on a 4850 // preseeded chroot correctly resets the chroot and preseeds over it 4851 func TestPreseedResetChroot(t *testing.T) { 4852 t.Parallel() 4853 if testing.Short() { 4854 t.Skip("skipping test in short mode.") 4855 } 4856 asserter := helper.Asserter{T: t} 4857 restoreCWD := testhelper.SaveCWD() 4858 defer restoreCWD() 4859 4860 var stateMachine ClassicStateMachine 4861 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4862 stateMachine.parent = &stateMachine 4863 stateMachine.Snaps = []string{"lxd"} 4864 stateMachine.commonFlags.Channel = "stable" 4865 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4866 Architecture: getHostArch(), 4867 Series: getHostSuite(), 4868 Rootfs: &imagedefinition.Rootfs{ 4869 Archive: "ubuntu", 4870 }, 4871 Customization: &imagedefinition.Customization{ 4872 ExtraPackages: []*imagedefinition.Package{ 4873 { 4874 PackageName: "squashfs-tools", 4875 }, 4876 { 4877 PackageName: "snapd", 4878 }, 4879 }, 4880 ExtraSnaps: []*imagedefinition.Snap{ 4881 { 4882 SnapName: "hello", 4883 }, 4884 { 4885 SnapName: "core", 4886 }, 4887 { 4888 SnapName: "core20", 4889 }, 4890 }, 4891 }, 4892 } 4893 4894 err := helper.SetDefaults(&stateMachine.ImageDef) 4895 asserter.AssertErrNil(err, true) 4896 4897 err = stateMachine.makeTemporaryDirectories() 4898 asserter.AssertErrNil(err, true) 4899 4900 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 4901 4902 err = getBasicChroot(stateMachine.StateMachine) 4903 asserter.AssertErrNil(err, true) 4904 4905 // install the packages that snap-preseed needs 4906 err = stateMachine.installPackages() 4907 asserter.AssertErrNil(err, true) 4908 4909 // first call prepareClassicImage to eventually preseed it 4910 err = stateMachine.prepareClassicImage() 4911 asserter.AssertErrNil(err, true) 4912 4913 // now preseed the chroot 4914 err = stateMachine.preseedClassicImage() 4915 asserter.AssertErrNil(err, true) 4916 4917 // set up a new set of snaps to be installed 4918 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4919 Architecture: getHostArch(), 4920 Customization: &imagedefinition.Customization{ 4921 ExtraSnaps: []*imagedefinition.Snap{ 4922 { 4923 SnapName: "ubuntu-image", 4924 }, 4925 }, 4926 }, 4927 } 4928 4929 // call prepareClassicImage again to trigger the reset 4930 err = stateMachine.prepareClassicImage() 4931 asserter.AssertErrNil(err, true) 4932 4933 // make sure the snaps from both prepares are present 4934 expectedSnaps := []string{"lxd", "hello", "ubuntu-image"} 4935 for _, expectedSnap := range expectedSnaps { 4936 snapGlobs, err := filepath.Glob(filepath.Join(stateMachine.tempDirs.chroot, 4937 "var", "lib", "snapd", "seed", "snaps", fmt.Sprintf("%s*.snap", expectedSnap))) 4938 asserter.AssertErrNil(err, true) 4939 if len(snapGlobs) == 0 { 4940 t.Errorf("expected snap %s to exist in the chroot but it does not", expectedSnap) 4941 } 4942 } 4943 } 4944 4945 // TestFailedUpdateBootloader tests failures in the updateBootloader function 4946 func TestFailedUpdateBootloader(t *testing.T) { 4947 asserter := helper.Asserter{T: t} 4948 restoreCWD := testhelper.SaveCWD() 4949 defer restoreCWD() 4950 4951 var stateMachine ClassicStateMachine 4952 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 4953 stateMachine.parent = &stateMachine 4954 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 4955 Architecture: getHostArch(), 4956 Series: getHostSuite(), 4957 Gadget: &imagedefinition.Gadget{}, 4958 } 4959 4960 // set up work dir 4961 err := stateMachine.makeTemporaryDirectories() 4962 asserter.AssertErrNil(err, true) 4963 4964 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 4965 4966 // first, test that updateBootloader fails when the rootfs partition 4967 // has not been found in earlier steps 4968 stateMachine.RootfsPartNum = -1 4969 stateMachine.RootfsVolName = "" 4970 err = stateMachine.updateBootloader() 4971 asserter.AssertErrContains(err, "Error: could not determine partition number of the root filesystem") 4972 4973 // place a test gadget tree in the scratch directory so we don't 4974 // have to build one 4975 gadgetDir := filepath.Join(stateMachine.tempDirs.scratch, "gadget") 4976 err = os.MkdirAll(gadgetDir, 0755) 4977 asserter.AssertErrNil(err, true) 4978 4979 gadgetSource := filepath.Join("testdata", "gadget_tree") 4980 gadgetDest := filepath.Join(gadgetDir, "install") 4981 err = osutil.CopySpecialFile(gadgetSource, gadgetDest) 4982 asserter.AssertErrNil(err, true) 4983 // also copy gadget.yaml to the root of the scratch/gadget dir 4984 err = osutil.CopyFile( 4985 filepath.Join(gadgetDest, "meta", "gadget.yaml"), 4986 filepath.Join(gadgetDest, "gadget.yaml"), 4987 osutil.CopyFlagDefault, 4988 ) 4989 asserter.AssertErrNil(err, true) 4990 4991 // prepare state in such a way that the rootfs partition was found in 4992 // earlier steps 4993 stateMachine.RootfsPartNum = 3 4994 stateMachine.RootfsVolName = "pc" 4995 4996 // parse gadget.yaml and run updateBootloader with the mocked os.Mkdir 4997 err = stateMachine.prepareGadgetTree() 4998 asserter.AssertErrNil(err, true) 4999 err = stateMachine.loadGadgetYaml() 5000 asserter.AssertErrNil(err, true) 5001 osMkdir = mockMkdir 5002 t.Cleanup(func() { 5003 osMkdir = os.Mkdir 5004 }) 5005 5006 err = stateMachine.updateBootloader() 5007 asserter.AssertErrContains(err, "Error creating scratch/loopback directory") 5008 } 5009 5010 // TestUnsupportedBootloader tests that a warning is thrown if the 5011 // bootloader specified in gadget.yaml is not supported 5012 func TestUnsupportedBootloader(t *testing.T) { 5013 asserter := helper.Asserter{T: t} 5014 restoreCWD := testhelper.SaveCWD() 5015 defer restoreCWD() 5016 5017 var stateMachine ClassicStateMachine 5018 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 5019 stateMachine.parent = &stateMachine 5020 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 5021 Architecture: getHostArch(), 5022 Series: getHostSuite(), 5023 Gadget: &imagedefinition.Gadget{}, 5024 } 5025 5026 err := stateMachine.makeTemporaryDirectories() 5027 asserter.AssertErrNil(err, true) 5028 5029 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 5030 5031 // place a test gadget tree in the scratch directory so we don't 5032 // have to build one 5033 gadgetDir := filepath.Join(stateMachine.tempDirs.scratch, "gadget") 5034 err = os.MkdirAll(gadgetDir, 0755) 5035 asserter.AssertErrNil(err, true) 5036 5037 gadgetSource := filepath.Join("testdata", "gadget_tree") 5038 gadgetDest := filepath.Join(gadgetDir, "install") 5039 err = osutil.CopySpecialFile(gadgetSource, gadgetDest) 5040 asserter.AssertErrNil(err, true) 5041 // also copy gadget.yaml to the root of the scratch/gadget dir 5042 err = osutil.CopyFile( 5043 filepath.Join(gadgetDest, "meta", "gadget.yaml"), 5044 filepath.Join(gadgetDest, "gadget.yaml"), 5045 osutil.CopyFlagDefault, 5046 ) 5047 asserter.AssertErrNil(err, true) 5048 // parse gadget.yaml 5049 err = stateMachine.prepareGadgetTree() 5050 asserter.AssertErrNil(err, true) 5051 err = stateMachine.loadGadgetYaml() 5052 asserter.AssertErrNil(err, true) 5053 5054 // prepare state in such a way that the rootfs partition was found in 5055 // earlier steps 5056 stateMachine.RootfsPartNum = 3 5057 stateMachine.RootfsVolName = "pc" 5058 5059 // set the bootloader for the volume to "test" 5060 stateMachine.GadgetInfo.Volumes["pc"].Bootloader = "test" 5061 5062 // capture stdout, run updateBootloader and make sure the states were printed 5063 stdout, restoreStdout, err := helper.CaptureStd(&os.Stdout) 5064 defer restoreStdout() 5065 asserter.AssertErrNil(err, true) 5066 5067 err = stateMachine.updateBootloader() 5068 asserter.AssertErrNil(err, true) 5069 5070 // restore stdout and examine what was printed 5071 restoreStdout() 5072 readStdout, err := io.ReadAll(stdout) 5073 asserter.AssertErrNil(err, true) 5074 if !strings.Contains(string(readStdout), "WARNING: updating bootloader test not yet supported") { 5075 t.Error("Warning for unsupported bootloader not printed") 5076 } 5077 } 5078 5079 // TestPreseedClassicImage unit tests the prepareClassicImage function 5080 func TestPreseedClassicImage(t *testing.T) { 5081 t.Parallel() 5082 if testing.Short() { 5083 t.Skip("skipping test in short mode.") 5084 } 5085 5086 asserter := helper.Asserter{T: t} 5087 restoreCWD := testhelper.SaveCWD() 5088 defer restoreCWD() 5089 5090 var stateMachine ClassicStateMachine 5091 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 5092 stateMachine.parent = &stateMachine 5093 stateMachine.Snaps = []string{"lxd"} 5094 stateMachine.commonFlags.Channel = "stable" 5095 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 5096 Architecture: getHostArch(), 5097 Series: getHostSuite(), 5098 Rootfs: &imagedefinition.Rootfs{ 5099 Archive: "ubuntu", 5100 }, 5101 Customization: &imagedefinition.Customization{ 5102 ExtraPackages: []*imagedefinition.Package{ 5103 { 5104 PackageName: "squashfs-tools", 5105 }, 5106 { 5107 PackageName: "snapd", 5108 }, 5109 }, 5110 ExtraSnaps: []*imagedefinition.Snap{ 5111 { 5112 SnapName: "hello", 5113 }, 5114 { 5115 SnapName: "core", 5116 }, 5117 { 5118 SnapName: "core20", 5119 }, 5120 }, 5121 }, 5122 } 5123 5124 err := helper.SetDefaults(&stateMachine.ImageDef) 5125 asserter.AssertErrNil(err, true) 5126 5127 err = stateMachine.makeTemporaryDirectories() 5128 asserter.AssertErrNil(err, true) 5129 5130 err = getBasicChroot(stateMachine.StateMachine) 5131 asserter.AssertErrNil(err, true) 5132 5133 // install the packages that snap-preseed needs 5134 err = stateMachine.installPackages() 5135 asserter.AssertErrNil(err, true) 5136 5137 // first call prepareClassicImage 5138 err = stateMachine.prepareClassicImage() 5139 asserter.AssertErrNil(err, true) 5140 5141 // now preseed the chroot 5142 err = stateMachine.preseedClassicImage() 5143 asserter.AssertErrNil(err, true) 5144 5145 // make sure the snaps are fully preseeded 5146 expectedSnaps := []string{"lxc", "lxd", "hello"} 5147 for _, expectedSnap := range expectedSnaps { 5148 snapPath := filepath.Join(stateMachine.tempDirs.chroot, "snap", "bin", expectedSnap) 5149 _, err := os.Stat(snapPath) 5150 if err != nil { 5151 t.Errorf("File %s should be in chroot, but is missing", snapPath) 5152 } 5153 } 5154 } 5155 5156 // TestFailedPreseedClassicImage tests failures in the preseedClassicImage function 5157 func TestFailedPreseedClassicImage(t *testing.T) { 5158 asserter := helper.Asserter{T: t} 5159 restoreCWD := testhelper.SaveCWD() 5160 defer restoreCWD() 5161 5162 var stateMachine ClassicStateMachine 5163 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 5164 stateMachine.parent = &stateMachine 5165 5166 err := stateMachine.makeTemporaryDirectories() 5167 asserter.AssertErrNil(err, true) 5168 5169 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 5170 5171 // mock os.MkdirAll 5172 osMkdirAll = mockMkdirAll 5173 t.Cleanup(func() { 5174 osMkdirAll = os.MkdirAll 5175 }) 5176 err = stateMachine.preseedClassicImage() 5177 asserter.AssertErrContains(err, "Error creating mountpoint") 5178 osMkdirAll = os.MkdirAll 5179 5180 testCaseName = "TestFailedPreseedClassicImage" 5181 execCommand = fakeExecCommand 5182 t.Cleanup(func() { 5183 execCommand = exec.Command 5184 }) 5185 err = stateMachine.preseedClassicImage() 5186 asserter.AssertErrContains(err, "Error running command") 5187 execCommand = exec.Command 5188 } 5189 5190 // TestStateMachine_defaultLocale tests that the default locale is set 5191 func TestStateMachine_defaultLocale(t *testing.T) { 5192 testCases := []struct { 5193 name string 5194 localeContents string 5195 localeExpected string 5196 }{ 5197 { 5198 "no_locale", 5199 "", 5200 "# Default Ubuntu locale\nLANG=C.UTF-8\n", 5201 }, 5202 { 5203 "locale_set", 5204 "LANG=en_US.UTF-8\n", 5205 "LANG=en_US.UTF-8\n", 5206 }, 5207 { 5208 "locale_set_non_lang", 5209 "LC_ALL=en_US.UTF-8\n", 5210 "LC_ALL=en_US.UTF-8\n", 5211 }, 5212 { 5213 "locale_set_with_comment", 5214 "# some comment\nLANG=en_US.UTF-8\n", 5215 "# some comment\nLANG=en_US.UTF-8\n", 5216 }, 5217 { 5218 "no_locale_with_comment", 5219 "# some comment\n", 5220 "# Default Ubuntu locale\nLANG=C.UTF-8\n", 5221 }, 5222 { 5223 "no_locale_with_comment_locale", 5224 "# LANG=en_US.UTF-8", 5225 "# Default Ubuntu locale\nLANG=C.UTF-8\n", 5226 }, 5227 } 5228 for _, tc := range testCases { 5229 t.Run(tc.name, func(t *testing.T) { 5230 asserter := helper.Asserter{T: t} 5231 var stateMachine ClassicStateMachine 5232 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 5233 stateMachine.parent = &stateMachine 5234 5235 err := stateMachine.makeTemporaryDirectories() 5236 asserter.AssertErrNil(err, true) 5237 5238 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 5239 5240 // create the <chroot>/etc/default directory 5241 defaultPath := filepath.Join(stateMachine.tempDirs.chroot, "etc", "default") 5242 err = os.MkdirAll(defaultPath, 0744) 5243 asserter.AssertErrNil(err, true) 5244 5245 // create the <chroot>/etc/default/locale file 5246 localePath := filepath.Join(defaultPath, "locale") 5247 err = os.WriteFile(localePath, []byte(tc.localeContents), 0600) 5248 asserter.AssertErrNil(err, true) 5249 5250 // call the function under test 5251 err = stateMachine.setDefaultLocale() 5252 asserter.AssertErrNil(err, true) 5253 5254 // read the locale file and make sure it matches the expected contents 5255 localeBytes, err := os.ReadFile(localePath) 5256 asserter.AssertErrNil(err, true) 5257 5258 if string(localeBytes) != tc.localeExpected { 5259 t.Errorf("Expected locale contents \"%s\", but got \"%s\"", 5260 tc.localeExpected, string(localeBytes)) 5261 } 5262 }) 5263 } 5264 } 5265 5266 // TestStateMachine_defaultLocaleFailures tests failures in the setDefaultLocale function 5267 func TestStateMachine_defaultLocaleFailures(t *testing.T) { 5268 asserter := helper.Asserter{T: t} 5269 5270 var stateMachine ClassicStateMachine 5271 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 5272 stateMachine.parent = &stateMachine 5273 5274 err := stateMachine.makeTemporaryDirectories() 5275 asserter.AssertErrNil(err, true) 5276 5277 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 5278 5279 // check failure in MkDirAll 5280 osMkdirAll = mockMkdirAll 5281 t.Cleanup(func() { 5282 osMkdirAll = os.MkdirAll 5283 }) 5284 err = stateMachine.setDefaultLocale() 5285 asserter.AssertErrContains(err, "Error creating default directory") 5286 osMkdirAll = os.MkdirAll 5287 5288 // check failure in WriteFile 5289 osWriteFile = mockWriteFile 5290 t.Cleanup(func() { 5291 osWriteFile = os.WriteFile 5292 }) 5293 err = stateMachine.setDefaultLocale() 5294 asserter.AssertErrContains(err, "Error writing to locale file") 5295 osWriteFile = os.WriteFile 5296 } 5297 5298 func TestClassicStateMachine_cleanRootfs_real_rootfs(t *testing.T) { 5299 t.Parallel() 5300 if testing.Short() { 5301 t.Skip("skipping test in short mode.") 5302 } 5303 asserter := helper.Asserter{T: t} 5304 restoreCWD := testhelper.SaveCWD() 5305 t.Cleanup(restoreCWD) 5306 5307 var stateMachine ClassicStateMachine 5308 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 5309 stateMachine.parent = &stateMachine 5310 stateMachine.Snaps = []string{"lxd"} 5311 stateMachine.commonFlags.Channel = "stable" 5312 stateMachine.commonFlags.Debug = true 5313 stateMachine.ImageDef = imagedefinition.ImageDefinition{ 5314 Architecture: getHostArch(), 5315 Series: getHostSuite(), 5316 Rootfs: &imagedefinition.Rootfs{ 5317 Archive: "ubuntu", 5318 }, 5319 Customization: &imagedefinition.Customization{ 5320 ExtraPackages: []*imagedefinition.Package{ 5321 { 5322 PackageName: "squashfs-tools", 5323 }, 5324 { 5325 PackageName: "snapd", 5326 }, 5327 }, 5328 }, 5329 } 5330 5331 err := helper.SetDefaults(&stateMachine.ImageDef) 5332 asserter.AssertErrNil(err, true) 5333 5334 err = stateMachine.makeTemporaryDirectories() 5335 asserter.AssertErrNil(err, true) 5336 5337 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 5338 5339 err = getBasicChroot(stateMachine.StateMachine) 5340 asserter.AssertErrNil(err, true) 5341 5342 // install the packages that snap-preseed needs 5343 err = stateMachine.installPackages() 5344 asserter.AssertErrNil(err, true) 5345 5346 err = stateMachine.cleanRootfs() 5347 asserter.AssertErrNil(err, true) 5348 5349 // Check cleaned files were removed 5350 cleaned := []string{ 5351 filepath.Join(stateMachine.tempDirs.chroot, "var", "lib", "dbus", "machine-id"), 5352 filepath.Join(stateMachine.tempDirs.chroot, "etc", "ssh", "ssh_host_rsa_key"), 5353 filepath.Join(stateMachine.tempDirs.chroot, "etc", "ssh", "ssh_host_rsa_key.pub"), 5354 filepath.Join(stateMachine.tempDirs.chroot, "etc", "ssh", "ssh_host_ecdsa_key"), 5355 filepath.Join(stateMachine.tempDirs.chroot, "etc", "ssh", "ssh_host_ecdsa_key.pub"), 5356 } 5357 for _, file := range cleaned { 5358 _, err := os.Stat(file) 5359 if !os.IsNotExist(err) { 5360 t.Errorf("File %s should not exist, but does", file) 5361 } 5362 } 5363 5364 truncated := []string{ 5365 filepath.Join(stateMachine.tempDirs.chroot, "etc", "machine-id"), 5366 } 5367 for _, file := range truncated { 5368 fileInfo, err := os.Stat(file) 5369 if os.IsNotExist(err) { 5370 t.Errorf("File %s should exist, but does not", file) 5371 } 5372 5373 if fileInfo.Size() != 0 { 5374 t.Errorf("File %s should be empty, but it is not. Size: %v", file, fileInfo.Size()) 5375 } 5376 } 5377 } 5378 5379 func TestClassicStateMachine_cleanRootfs(t *testing.T) { 5380 sampleContent := "test" 5381 sampleSize := int64(len(sampleContent)) 5382 5383 testCases := []struct { 5384 name string 5385 mockFuncs func() func() 5386 expectedErr string 5387 initialRootfsContent []string 5388 wantRootfsContent map[string]int64 // name: size 5389 }{ 5390 { 5391 name: "success", 5392 initialRootfsContent: []string{ 5393 filepath.Join("etc", "machine-id"), 5394 filepath.Join("var", "lib", "dbus", "machine-id"), 5395 filepath.Join("etc", "udev", "rules.d", "test-persistent-net.rules"), 5396 filepath.Join("etc", "udev", "rules.d", "test2-persistent-net.rules"), 5397 filepath.Join("var", "cache", "debconf", "test-old"), 5398 filepath.Join("var", "lib", "dpkg", "testdpkg-old"), 5399 }, 5400 wantRootfsContent: map[string]int64{ 5401 filepath.Join("etc", "machine-id"): 0, 5402 filepath.Join("etc", "udev", "rules.d", "test-persistent-net.rules"): 0, 5403 filepath.Join("etc", "udev", "rules.d", "test2-persistent-net.rules"): 0, 5404 }, 5405 }, 5406 { 5407 name: "fail to clean files", 5408 mockFuncs: func() func() { 5409 mock := testhelper.NewOSMock( 5410 &testhelper.OSMockConf{}, 5411 ) 5412 5413 osRemove = mock.Remove 5414 return func() { osRemove = os.Remove } 5415 }, 5416 expectedErr: "Error removing", 5417 initialRootfsContent: []string{ 5418 filepath.Join("etc", "machine-id"), 5419 filepath.Join("var", "lib", "dbus", "machine-id"), 5420 filepath.Join("etc", "udev", "rules.d", "test-persistent-net.rules"), 5421 filepath.Join("var", "cache", "debconf", "test-old"), 5422 filepath.Join("var", "lib", "dpkg", "testdpkg-old"), 5423 }, 5424 wantRootfsContent: map[string]int64{ 5425 filepath.Join("etc", "machine-id"): sampleSize, 5426 filepath.Join("var", "lib", "dbus", "machine-id"): sampleSize, 5427 filepath.Join("etc", "udev", "rules.d", "test-persistent-net.rules"): sampleSize, 5428 filepath.Join("var", "cache", "debconf", "test-old"): sampleSize, 5429 filepath.Join("var", "lib", "dpkg", "testdpkg-old"): sampleSize, 5430 }, 5431 }, 5432 { 5433 name: "fail to truncate files", 5434 mockFuncs: func() func() { 5435 mock := testhelper.NewOSMock( 5436 &testhelper.OSMockConf{}, 5437 ) 5438 5439 osTruncate = mock.Truncate 5440 return func() { osTruncate = os.Truncate } 5441 }, 5442 expectedErr: "Error truncating", 5443 initialRootfsContent: []string{ 5444 filepath.Join("etc", "machine-id"), 5445 filepath.Join("var", "lib", "dbus", "machine-id"), 5446 filepath.Join("etc", "udev", "rules.d", "test-persistent-net.rules"), 5447 }, 5448 wantRootfsContent: map[string]int64{ 5449 filepath.Join("etc", "machine-id"): sampleSize, 5450 filepath.Join("etc", "udev", "rules.d", "test-persistent-net.rules"): sampleSize, 5451 }, 5452 }, 5453 } 5454 5455 for _, tc := range testCases { 5456 t.Run(tc.name, func(t *testing.T) { 5457 asserter := helper.Asserter{T: t} 5458 stateMachine := &ClassicStateMachine{} 5459 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 5460 stateMachine.parent = stateMachine 5461 5462 err := stateMachine.makeTemporaryDirectories() 5463 asserter.AssertErrNil(err, true) 5464 5465 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 5466 5467 if tc.mockFuncs != nil { 5468 restoreMock := tc.mockFuncs() 5469 t.Cleanup(restoreMock) 5470 } 5471 5472 for _, path := range tc.initialRootfsContent { 5473 // create dir if necessary 5474 fullPath := filepath.Join(stateMachine.tempDirs.chroot, path) 5475 err = os.MkdirAll(filepath.Dir(fullPath), 0777) 5476 asserter.AssertErrNil(err, true) 5477 5478 err := os.WriteFile(fullPath, []byte(sampleContent), 0600) 5479 asserter.AssertErrNil(err, true) 5480 } 5481 5482 err = stateMachine.cleanRootfs() 5483 if err != nil || len(tc.expectedErr) != 0 { 5484 asserter.AssertErrContains(err, tc.expectedErr) 5485 } 5486 5487 for path, size := range tc.wantRootfsContent { 5488 fullPath := filepath.Join(stateMachine.tempDirs.chroot, path) 5489 s, err := os.Stat(fullPath) 5490 if os.IsNotExist(err) { 5491 t.Errorf("File %s should exist, but does not", path) 5492 } 5493 5494 if s.Size() != size { 5495 t.Errorf("File size of %s is not matching: want %d, got %d", path, size, s.Size()) 5496 } 5497 } 5498 }) 5499 } 5500 }