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  }