github.com/openshift/installer@v1.4.17/cmd/openshift-install/internal_integration_test.go (about) 1 package main 2 3 import ( 4 "compress/gzip" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/fs" 9 "os" 10 "path/filepath" 11 "strings" 12 "testing" 13 14 "github.com/cavaliercoder/go-cpio" 15 igntypes "github.com/coreos/ignition/v2/config/v3_2/types" 16 "github.com/diskfs/go-diskfs" 17 "github.com/go-openapi/errors" 18 "github.com/pkg/diff" 19 "github.com/rogpeppe/go-internal/testscript" 20 "github.com/stretchr/testify/assert" 21 "github.com/vincent-petithory/dataurl" 22 23 "github.com/openshift/installer/pkg/asset/releaseimage" 24 ) 25 26 // This file contains a number of functions useful for 27 // setting up the environment and running the integration 28 // tests for the agent-based installer 29 30 // runAllIntegrationTests runs all the tests found in the (sub)folders 31 // rooted at rootPath. Folders that do not contain a test file (.txt or .txtar) 32 // are ignored. 33 func runAllIntegrationTests(t *testing.T, rootPath string) { 34 t.Helper() 35 suites := []string{} 36 37 err := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error { 38 if err != nil { 39 return err 40 } 41 if d.IsDir() { 42 files, err := os.ReadDir(path) 43 if err != nil { 44 return err 45 } 46 for _, f := range files { 47 if !f.IsDir() && (strings.HasSuffix(f.Name(), ".txt") || strings.HasSuffix(f.Name(), ".txtar")) { 48 for _, s := range suites { 49 if s == path { 50 return nil 51 } 52 } 53 suites = append(suites, path) 54 } 55 } 56 } 57 return nil 58 }) 59 if err != nil { 60 t.Fatal(err) 61 } 62 63 for _, s := range suites { 64 t.Run(generateTestName(s), func(t *testing.T) { 65 runIntegrationTest(t, s) 66 }) 67 } 68 } 69 70 func generateTestName(path string) string { 71 name := strings.TrimPrefix(path, "testdata/") 72 return strings.ReplaceAll(name, "/", "_") 73 } 74 75 func runIntegrationTest(t *testing.T, testFolder string) { 76 t.Helper() 77 78 if testing.Short() { 79 t.Skip("skipping integration test") 80 } 81 82 projectDir, err := os.Getwd() 83 assert.NoError(t, err) 84 homeDir, err := os.UserHomeDir() 85 assert.NoError(t, err) 86 87 testscript.Run(t, testscript.Params{ 88 Dir: testFolder, 89 // Uncomment below line to help debug the testcases 90 // TestWork: true, 91 92 Setup: func(e *testscript.Env) error { 93 // This is required to allow proper 94 // loading of the embedded resources 95 e.Cd = filepath.Join(projectDir, "../../data") 96 97 // For agent commands, let's use the 98 // current home dir 99 for i, v := range e.Vars { 100 if v == "HOME=/no-home" { 101 e.Vars[i] = fmt.Sprintf("HOME=%s", homeDir) 102 break 103 } 104 } 105 106 // Let's get the current release version, so that 107 // it could be used within the tests 108 pullspec, err := releaseimage.Default() 109 if err != nil { 110 return err 111 } 112 e.Vars = append(e.Vars, fmt.Sprintf("RELEASE_IMAGE=%s", pullspec)) 113 114 return nil 115 }, 116 117 Cmds: map[string]func(*testscript.TestScript, bool, []string){ 118 "isocmp": isoCmp, 119 "ignitionImgContains": ignitionImgContains, 120 "configImgContains": configImgContains, 121 "initrdImgContains": initrdImgContains, 122 "unconfiguredIgnContains": unconfiguredIgnContains, 123 "unconfiguredIgnCmp": unconfiguredIgnCmp, 124 "expandFile": expandFile, 125 "isoContains": isoContains, 126 }, 127 }) 128 } 129 130 // [!] ignitionImgContains `isoPath` `file` check if the specified file `file` 131 // is stored within /images/ignition.img archive in the ISO `isoPath` image. 132 func ignitionImgContains(ts *testscript.TestScript, neg bool, args []string) { 133 if len(args) != 2 { 134 ts.Fatalf("usage: ignitionImgContains isoPath file") 135 } 136 137 workDir := ts.Getenv("WORK") 138 isoPath, eFilePath := args[0], args[1] 139 isoPathAbs := filepath.Join(workDir, isoPath) 140 141 _, err := extractArchiveFile(isoPathAbs, "/images/ignition.img", eFilePath) 142 ts.Check(err) 143 } 144 145 // [!] configImgContains `isoPath` `file` check if the specified file `file` 146 // is stored within the config image ISO. 147 func configImgContains(ts *testscript.TestScript, neg bool, args []string) { 148 if len(args) != 2 { 149 ts.Fatalf("usage: configImgContains isoPath file") 150 } 151 152 workDir := ts.Getenv("WORK") 153 isoPath, eFilePath := args[0], args[1] 154 isoPathAbs := filepath.Join(workDir, isoPath) 155 156 _, err := extractArchiveFile(isoPathAbs, eFilePath, "") 157 ts.Check(err) 158 } 159 160 // archiveFileNames `isoPath` get the names of the archive files to use 161 // based on the name of the ISO image. 162 func archiveFileNames(isoPath string) (string, string, error) { 163 if strings.HasPrefix(isoPath, "agent.") { 164 return "/images/ignition.img", "config.ign", nil 165 } else if strings.HasPrefix(isoPath, "agentconfig.") { 166 return "/config.gz", "", nil 167 } 168 169 return "", "", errors.NotFound(fmt.Sprintf("ISO %s has unrecognized prefix", isoPath)) 170 } 171 172 // [!] unconfiguredIgnContains `file` check if the specified file `file` 173 // is stored within the unconfigured ignition Storage Files. 174 func unconfiguredIgnContains(ts *testscript.TestScript, neg bool, args []string) { 175 if len(args) != 1 { 176 ts.Fatalf("usage: unconfiguredIgnContains file") 177 } 178 ignitionStorageContains(ts, neg, []string{"unconfigured-agent.ign", args[0]}) 179 } 180 181 // [!] ignitionStorageContains `ignPath` `file` check if the specified file `file` 182 // is stored within the ignition Storage Files. 183 func ignitionStorageContains(ts *testscript.TestScript, neg bool, args []string) { 184 if len(args) != 2 { 185 ts.Fatalf("usage: ignitionStorageContains ignPath file") 186 } 187 188 workDir := ts.Getenv("WORK") 189 ignPath, eFilePath := args[0], args[1] 190 ignPathAbs := filepath.Join(workDir, ignPath) 191 192 config, err := readIgnition(ts, ignPathAbs) 193 ts.Check(err) 194 195 found := false 196 for _, f := range config.Storage.Files { 197 if f.Path == eFilePath { 198 found = true 199 } 200 } 201 202 if !found && !neg { 203 ts.Fatalf("%s does not contain %s", ignPath, eFilePath) 204 } 205 206 if neg && found { 207 ts.Fatalf("%s should not contain %s", ignPath, eFilePath) 208 } 209 } 210 211 // [!] isoCmp `isoPath` `isoFile` `expectedFile` check that the content of the file 212 // `isoFile` - extracted from the ISO embedded configuration file referenced 213 // by `isoPath` - matches the content of the local file `expectedFile`. 214 // Environment variables in `expectedFile` are substituted before the comparison. 215 func isoCmp(ts *testscript.TestScript, neg bool, args []string) { 216 if len(args) != 3 { 217 ts.Fatalf("usage: isocmp isoPath file1 file2") 218 } 219 220 workDir := ts.Getenv("WORK") 221 isoPath, aFilePath, eFilePath := args[0], args[1], args[2] 222 isoPathAbs := filepath.Join(workDir, isoPath) 223 224 archiveFile, ignitionFile, err := archiveFileNames(isoPath) 225 if err != nil { 226 ts.Check(err) 227 } 228 229 aData, err := readFileFromISO(isoPathAbs, archiveFile, ignitionFile, aFilePath) 230 ts.Check(err) 231 232 eFilePathAbs := filepath.Join(workDir, eFilePath) 233 eData, err := os.ReadFile(eFilePathAbs) 234 ts.Check(err) 235 236 byteCompare(ts, neg, aData, eData, aFilePath, eFilePath) 237 } 238 239 // [!] unconfiguredIgnCmp `fileInIgn` `expectedFile` check that the content 240 // of the file `fileInIgn` extracted from the unconfigured ignition 241 // configuration file matches the content of the local file `expectedFile`. 242 // Environment variables in in `expectedFile` are substituted before the comparison. 243 func unconfiguredIgnCmp(ts *testscript.TestScript, neg bool, args []string) { 244 if len(args) != 2 { 245 ts.Fatalf("usage: iunconfiguredIgnCmp file1 file2") 246 } 247 argsNext := []string{"unconfigured-agent.ign", args[0], args[1]} 248 ignitionStorageCmp(ts, neg, argsNext) 249 } 250 251 // [!] ignitionStorageCmp `ignPath` `ignFile` `expectedFile` check that the content of the file 252 // `ignFile` - extracted from the ignition configuration file referenced 253 // by `ignPath` - matches the content of the local file `expectedFile`. 254 // Environment variables in in `expectedFile` are substituted before the comparison. 255 func ignitionStorageCmp(ts *testscript.TestScript, neg bool, args []string) { 256 if len(args) != 3 { 257 ts.Fatalf("usage: ignitionStorageCmp ignPath file1 file2") 258 } 259 260 workDir := ts.Getenv("WORK") 261 ignPath, aFilePath, eFilePath := args[0], args[1], args[2] 262 ignPathAbs := filepath.Join(workDir, ignPath) 263 264 config, err := readIgnition(ts, ignPathAbs) 265 ts.Check(err) 266 267 aData, err := readFileFromIgnitionCfg(&config, aFilePath) 268 ts.Check(err) 269 270 eFilePathAbs := filepath.Join(workDir, eFilePath) 271 eData, err := os.ReadFile(eFilePathAbs) 272 ts.Check(err) 273 274 byteCompare(ts, neg, aData, eData, aFilePath, eFilePath) 275 } 276 277 func readIgnition(ts *testscript.TestScript, ignPath string) (config igntypes.Config, err error) { 278 rawIgn, err := os.ReadFile(ignPath) 279 ts.Check(err) 280 err = json.Unmarshal(rawIgn, &config) 281 return config, err 282 } 283 284 // [!] expandFile `file...` can be used to substitute environment variables 285 // references for each file specified. 286 func expandFile(ts *testscript.TestScript, neg bool, args []string) { 287 if len(args) != 1 { 288 ts.Fatalf("usage: expandFile file...") 289 } 290 291 workDir := ts.Getenv("WORK") 292 for _, f := range args { 293 fileName := filepath.Join(workDir, f) 294 data, err := os.ReadFile(fileName) 295 ts.Check(err) 296 297 newData := expand(ts, data) 298 err = os.WriteFile(fileName, []byte(newData), 0) 299 ts.Check(err) 300 } 301 } 302 303 func expand(ts *testscript.TestScript, s []byte) string { 304 return os.Expand(string(s), func(key string) string { 305 return ts.Getenv(key) 306 }) 307 } 308 309 func byteCompare(ts *testscript.TestScript, neg bool, aData, eData []byte, aFilePath, eFilePath string) { 310 aText := string(aData) 311 eText := expand(ts, eData) 312 313 eq := aText == eText 314 if neg { 315 if eq { 316 ts.Fatalf("%s and %s do not differ", aFilePath, eFilePath) 317 } 318 return 319 } 320 if eq { 321 return 322 } 323 324 ts.Logf(aText) 325 326 var sb strings.Builder 327 if err := diff.Text(eFilePath, aFilePath, eText, aText, &sb); err != nil { 328 ts.Check(err) 329 } 330 331 ts.Logf("%s", sb.String()) 332 ts.Fatalf("%s and %s differ", eFilePath, aFilePath) 333 } 334 335 func readFileFromISO(isoPath, archiveFile, ignitionFile, nodePath string) ([]byte, error) { 336 config, err := extractCfgData(isoPath, archiveFile, ignitionFile, nodePath) 337 if err != nil { 338 return nil, err 339 } 340 341 return config, nil 342 } 343 344 func readFileFromIgnitionCfg(config *igntypes.Config, nodePath string) ([]byte, error) { 345 for _, f := range config.Storage.Files { 346 if f.Node.Path == nodePath { 347 actualData, err := dataurl.DecodeString(*f.FileEmbedded1.Contents.Source) 348 if err != nil { 349 return nil, err 350 } 351 return actualData.Data, nil 352 } 353 } 354 355 return nil, errors.NotFound(nodePath) 356 } 357 358 func extractArchiveFile(isoPath, archive, fileName string) ([]byte, error) { 359 disk, err := diskfs.Open(isoPath, diskfs.WithOpenMode(diskfs.ReadOnly)) 360 if err != nil { 361 return nil, err 362 } 363 364 fs, err := disk.GetFilesystem(0) 365 if err != nil { 366 return nil, err 367 } 368 369 ignitionImg, err := fs.OpenFile(archive, os.O_RDONLY) 370 if err != nil { 371 return nil, err 372 } 373 374 gzipReader, err := gzip.NewReader(ignitionImg) 375 if err != nil { 376 return nil, err 377 } 378 379 cpioReader := cpio.NewReader(gzipReader) 380 381 for { 382 header, err := cpioReader.Next() 383 if err == io.EOF { //nolint:errorlint 384 // end of cpio archive 385 break 386 } 387 if err != nil { 388 return nil, err 389 } 390 391 // If the file is not in ignition return it directly 392 if fileName == "" || header.Name == fileName { 393 rawContent, err := io.ReadAll(cpioReader) 394 if err != nil { 395 return nil, err 396 } 397 return rawContent, nil 398 } 399 } 400 401 return nil, errors.NotFound(fmt.Sprintf("File %s not found within the %s archive", fileName, archive)) 402 } 403 404 func extractCfgData(isoPath, archiveFile, ignitionFile, nodePath string) ([]byte, error) { 405 if ignitionFile == "" { 406 // If the archive is not part of an ignition file return the archive data 407 rawContent, err := extractArchiveFile(isoPath, archiveFile, nodePath) 408 if err != nil { 409 return nil, err 410 } 411 return rawContent, nil 412 } 413 414 rawContent, err := extractArchiveFile(isoPath, archiveFile, ignitionFile) 415 if err != nil { 416 return nil, err 417 } 418 419 var config igntypes.Config 420 err = json.Unmarshal(rawContent, &config) 421 if err != nil { 422 return nil, err 423 } 424 425 for _, f := range config.Storage.Files { 426 if f.Node.Path == nodePath { 427 actualData, err := dataurl.DecodeString(*f.FileEmbedded1.Contents.Source) 428 if err != nil { 429 return nil, err 430 } 431 return actualData.Data, nil 432 } 433 } 434 435 return nil, errors.NotFound(fmt.Sprintf("File %s not found within the %s archive", nodePath, archiveFile)) 436 } 437 438 // [!] initrdImgContains `isoPath` `file` check if the specified file `file` 439 // is stored within a compressed cpio archive by scanning the content of 440 // /images/ignition.img archive in the ISO `isoPath` image (note: plain cpio 441 // archives are ignored). 442 func initrdImgContains(ts *testscript.TestScript, neg bool, args []string) { 443 if len(args) != 2 { 444 ts.Fatalf("usage: initrdImgContains isoPath file") 445 } 446 447 workDir := ts.Getenv("WORK") 448 isoPath, eFilePath := args[0], args[1] 449 isoPathAbs := filepath.Join(workDir, isoPath) 450 451 err := checkFileFromInitrdImg(isoPathAbs, eFilePath) 452 ts.Check(err) 453 } 454 455 // [!] isoContains `isoPath` `file` check if the specified `file` is stored 456 // within the ISO `isoPath` image. 457 func isoContains(ts *testscript.TestScript, neg bool, args []string) { 458 if len(args) != 2 { 459 ts.Fatalf("usage: isoContains isoPath file") 460 } 461 462 workDir := ts.Getenv("WORK") 463 isoPath, filePath := args[0], args[1] 464 isoPathAbs := filepath.Join(workDir, isoPath) 465 466 disk, err := diskfs.Open(isoPathAbs, diskfs.WithOpenMode(diskfs.ReadOnly)) 467 ts.Check(err) 468 469 fs, err := disk.GetFilesystem(0) 470 ts.Check(err) 471 472 _, err = fs.OpenFile(filePath, os.O_RDONLY) 473 ts.Check(err) 474 } 475 476 func checkFileFromInitrdImg(isoPath string, fileName string) error { 477 disk, err := diskfs.Open(isoPath, diskfs.WithOpenMode(diskfs.ReadOnly)) 478 if err != nil { 479 return err 480 } 481 482 fs, err := disk.GetFilesystem(0) 483 if err != nil { 484 return err 485 } 486 487 initRdImg, err := fs.OpenFile("/images/pxeboot/initrd.img", os.O_RDONLY) 488 if err != nil { 489 return err 490 } 491 defer initRdImg.Close() 492 493 const ( 494 gzipID1 = 0x1f 495 gzipID2 = 0x8b 496 gzipDeflate = 0x08 497 ) 498 499 buff := make([]byte, 4096) 500 for { 501 _, err := initRdImg.Read(buff) 502 if err == io.EOF { //nolint:errorlint 503 break 504 } 505 506 foundAt := -1 507 for idx := 0; idx < len(buff)-2; idx++ { 508 // scan the buffer for a potential gzip header 509 if buff[idx+0] == gzipID1 && buff[idx+1] == gzipID2 && buff[idx+2] == gzipDeflate { 510 foundAt = idx 511 break 512 } 513 } 514 515 if foundAt >= 0 { 516 // check if it's really a compressed cpio archive 517 delta := int64(foundAt - len(buff)) 518 newPos, err := initRdImg.Seek(delta, io.SeekCurrent) 519 if err != nil { 520 break 521 } 522 523 files, err := lookForCpioFiles(initRdImg) 524 if err != nil { 525 if _, err := initRdImg.Seek(newPos+2, io.SeekStart); err != nil { 526 break 527 } 528 continue 529 } 530 531 // check if the current cpio files match the required ones 532 for _, f := range files { 533 matched, err := filepath.Match(fileName, f) 534 if err != nil { 535 return err 536 } 537 if matched { 538 return nil 539 } 540 } 541 } 542 } 543 544 return errors.NotFound(fmt.Sprintf("File %s not found within the /images/pxeboot/initrd.img archive", fileName)) 545 } 546 547 func lookForCpioFiles(r io.Reader) ([]string, error) { 548 var files []string 549 550 gr, err := gzip.NewReader(r) 551 if err != nil { 552 return nil, err 553 } 554 defer gr.Close() 555 556 // skip in case of garbage 557 if gr.OS != 255 && gr.OS >= 13 { 558 return nil, fmt.Errorf("Unknown OS code: %v", gr.Header.OS) 559 } 560 561 cr := cpio.NewReader(gr) 562 for { 563 h, err := cr.Next() 564 if err != nil { 565 break 566 } 567 568 files = append(files, h.Name) 569 } 570 571 return files, nil 572 }