1
0
mirror of https://github.com/openshift/installer.git synced 2026-02-05 06:46:36 +01:00
Files
installer/internal/tshelpers/custom_commands.go
Zane Bitter 32fe75446e integration tests: Allow literal '$' in expected contents
Escape a literal $ with $$.
2025-11-07 14:08:59 +13:00

657 lines
21 KiB
Go

package tshelpers
import (
"compress/gzip"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/cavaliercoder/go-cpio"
igntypes "github.com/coreos/ignition/v2/config/v3_2/types"
"github.com/diskfs/go-diskfs"
"github.com/go-openapi/errors"
"github.com/pkg/diff"
"github.com/rogpeppe/go-internal/testscript"
"github.com/vincent-petithory/dataurl"
)
// Command | Short description | Usage example
// ------------------------|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------
// IgnitionImgContains | checks if the specified file is stored within /images/ignition.img | ignitionImgContains agent.x86_64.iso config.ign
// IsoIgnitionUser | checks if the ignition file extracted from the ISO contains the specified user with | isoIgnitionUser node.x86_64.iso core my-sshKey
// | the given `authKey`. |
// IsoIgnitionContains | checks that the file extracted from the ISO embedded configuration file exists | isoIgnitionContains node.x86_64.iso /usr/local/bin/add-node.sh
// IsoCmp | check that the content of the file extracted from the ISO embedded configuration file | isoCmp agent.x86_64.iso /etc/assisted/manifests/infraenv.yaml expected/infraenv.yaml
// | matches the content of the local file |
// IsoCmpRegEx | Same as IsoCmp, but the expected file can contain a regex pattern that will be applied| isoCmpRegEx agent.x86_64.iso /etc/assisted/manifests/infraenv.yaml expected/infraenv.yaml
// | during the comparison. |
// IsoFileCmpRegEx | checks that file context extracted directly from the ISO matches the content of the | isoFileCmpRegEx node.x86_64.iso /EFI/redhat/grub.cfg expected/grub.cfg
// | local file, by applying a regex comparison. |
// IsoSizeMin | checks that the ISO is greater than a minimum number of megabytes | isoSizeMin agent.x86_64.iso 100
// IsoSizeMax | checks that the ISO is less than a maximum number of megabytes | isoSizeMax agent.x86_64.iso 1000
// InitrdImgContains | check if the specified file is stored within a compressed cpio archive by scanning the| initrdImgContains agent.x86_64.iso /agent-files/agent-tui
// | content of /images/ignition.img archive in the ISO |
// ------------------------|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------
// ConfigImgContains | check if the specified file is stored within the config image ISO | configImgContains agentconfig.noarch.iso /config.gz
// UnconfiguredIgnContains | check if the specified file is stored within the unconfigured ignition Storage Files | unconfiguredIgnContains /etc/assisted/manifests/infraenv.yaml
// IgnitionStorageContains | check if the specified file is stored within the ignition Storage Files | - (note: works directly on a ignition file)
// UnconfiguredIgnCmp | check that the content extracted from the unconfigured ignition configuration file | unconfiguredIgnCmp /etc/assisted/manifests/infraenv.yaml expected/infraenv.yaml
// | matches the content of the local file |
// IsoContains | check if the specified is stored within the ISO | isoContains imagebasedconfig.iso /cluster-configuration/manifest.json
// IgnitionImgContains `isoPath` `file` check if the specified file `file`
// is stored within /images/ignition.img archive in the ISO `isoPath` image.
func IgnitionImgContains(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: ignitionImgContains isoPath file")
}
workDir := ts.Getenv("WORK")
isoPath, eFilePath := args[0], args[1]
isoPathAbs := filepath.Join(workDir, isoPath)
_, err := extractArchiveFile(isoPathAbs, "/images/ignition.img", eFilePath)
ts.Check(err)
}
// IsoIgnitionContains `isoPath` `file` checks that the file
// `isoFile` - extracted from the ISO embedded configuration file
// referenced by `isoPath` - exists.
func IsoIgnitionContains(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: isoIgnitionContains isoPath")
}
workDir := ts.Getenv("WORK")
isoPath, eFilePath := args[0], args[1]
isoPathAbs := filepath.Join(workDir, isoPath)
archiveFile, ignitionFile, err := archiveFileNames(isoPath)
if err != nil {
ts.Check(err)
}
_, err = readFileFromISO(isoPathAbs, archiveFile, ignitionFile, eFilePath)
ts.Check(err)
}
// IsoIgnitionUser `isoPath` `user` `authKey` checks if the ignition file extracted
// from the ISO `isoPath` contains the specified `user` with the given `authKey`.
func IsoIgnitionUser(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 3 {
ts.Fatalf("usage: IsoIgnitionUser isoPath user authKey")
}
workDir := ts.Getenv("WORK")
isoPath, eUser, eAuthKey := args[0], args[1], args[2]
isoPathAbs := filepath.Join(workDir, isoPath)
archiveFile, ignitionFile, err := archiveFileNames(isoPath)
if err != nil {
ts.Check(err)
}
ignition, err := extractIgnition(isoPathAbs, archiveFile, ignitionFile)
ts.Check(err)
for _, user := range ignition.Passwd.Users {
if user.Name != eUser {
continue
}
if len(user.SSHAuthorizedKeys) == 0 || user.SSHAuthorizedKeys[0] != igntypes.SSHAuthorizedKey(eAuthKey) {
continue
}
return
}
ts.Fatalf("expected user '%s' with SSH auth key '%s' not found", eUser, eAuthKey)
}
// IsoCmp `isoPath` `isoFile` `expectedFile` check that the content of the file
// `isoFile` - extracted from the ISO embedded configuration file referenced
// by `isoPath` - matches the content of the local file `expectedFile`.
// Environment variables in `expectedFile` are substituted before the comparison.
func IsoCmp(ts *testscript.TestScript, neg bool, args []string) {
isoCmpInternal(ts, neg, args, byteCompare)
}
// IsoCmpRegEx `isoPath` `isoFile` `expectedFile` works as `IsoCmp`,
// but the expected file can contain a regex pattern that will be
// applied during the comparison.
func IsoCmpRegEx(ts *testscript.TestScript, neg bool, args []string) {
isoCmpInternal(ts, neg, args, byteCompareRegEx)
}
func isoCmpInternal(ts *testscript.TestScript, neg bool, args []string, cmp func(ts *testscript.TestScript, neg bool, aData, eData []byte, aFilePath, eFilePath string)) {
if len(args) != 3 {
ts.Fatalf("usage: isocmp isoPath file1 file2")
}
workDir := ts.Getenv("WORK")
isoPath, aFilePath, eFilePath := args[0], args[1], args[2]
isoPathAbs := filepath.Join(workDir, isoPath)
archiveFile, ignitionFile, err := archiveFileNames(isoPath)
if err != nil {
ts.Check(err)
}
aData, err := readFileFromISO(isoPathAbs, archiveFile, ignitionFile, aFilePath)
ts.Check(err)
eFilePathAbs := filepath.Join(workDir, eFilePath)
eData, err := os.ReadFile(eFilePathAbs)
ts.Check(err)
cmp(ts, neg, aData, eData, aFilePath, eFilePath)
}
// IsoFileCmpRegEx `isoPath` `isoFile` `expectedFile` check that the content of the ISO
// `isoFile` - matches the content of the local file `expectedFile` (using a regex
// comparison). Environment variables in `expectedFile` are substituted before the comparison.
func IsoFileCmpRegEx(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 3 {
ts.Fatalf("usage: isofilecmpregex isoPath file1 file2")
}
workDir := ts.Getenv("WORK")
isoPath, aFilePath, eFilePath := args[0], args[1], args[2]
isoPathAbs := filepath.Join(workDir, isoPath)
disk, err := diskfs.Open(isoPathAbs, diskfs.WithOpenMode(diskfs.ReadOnly))
ts.Check(err)
fs, err := disk.GetFilesystem(0)
ts.Check(err)
aFile, err := fs.OpenFile(aFilePath, os.O_RDONLY)
ts.Check(err)
defer aFile.Close()
aData, err := io.ReadAll(aFile)
ts.Check(err)
eFilePathAbs := filepath.Join(workDir, eFilePath)
eData, err := os.ReadFile(eFilePathAbs)
ts.Check(err)
byteCompareRegEx(ts, neg, aData, eData, aFilePath, eFilePath)
}
// InitrdImgContains `isoPath` `file` check if the specified file `file`
// is stored within a compressed cpio archive by scanning the content of
// /images/ignition.img archive in the ISO `isoPath` image (note: plain cpio
// archives are ignored).
func InitrdImgContains(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: initrdImgContains isoPath file")
}
workDir := ts.Getenv("WORK")
isoPath, eFilePath := args[0], args[1]
isoPathAbs := filepath.Join(workDir, isoPath)
err := checkFileFromInitrdImg(isoPathAbs, eFilePath)
ts.Check(err)
}
// ConfigImgContains `isoPath` `file` check if the specified file `file`
// is stored within the config image ISO.
func ConfigImgContains(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: configImgContains isoPath file")
}
workDir := ts.Getenv("WORK")
isoPath, eFilePath := args[0], args[1]
isoPathAbs := filepath.Join(workDir, isoPath)
_, err := extractArchiveFile(isoPathAbs, eFilePath, "")
ts.Check(err)
}
// UnconfiguredIgnContains `file` check if the specified file `file`
// is stored within the unconfigured ignition Storage Files.
func UnconfiguredIgnContains(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 1 {
ts.Fatalf("usage: unconfiguredIgnContains file")
}
IgnitionStorageContains(ts, neg, []string{"unconfigured-agent.ign", args[0]})
}
// IgnitionStorageContains `ignPath` `file` check if the specified file `file`
// is stored within the ignition Storage Files.
func IgnitionStorageContains(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: ignitionStorageContains ignPath file")
}
workDir := ts.Getenv("WORK")
ignPath, eFilePath := args[0], args[1]
ignPathAbs := filepath.Join(workDir, ignPath)
config, err := readIgnition(ts, ignPathAbs)
ts.Check(err)
found := false
for _, f := range config.Storage.Files {
if f.Path == eFilePath {
found = true
}
}
if !found && !neg {
ts.Fatalf("%s does not contain %s", ignPath, eFilePath)
}
if neg && found {
ts.Fatalf("%s should not contain %s", ignPath, eFilePath)
}
}
// UnconfiguredIgnCmp `fileInIgn` `expectedFile` check that the content
// of the file `fileInIgn` extracted from the unconfigured ignition
// configuration file matches the content of the local file `expectedFile`.
// Environment variables in in `expectedFile` are substituted before the comparison.
func UnconfiguredIgnCmp(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: iunconfiguredIgnCmp file1 file2")
}
argsNext := []string{"unconfigured-agent.ign", args[0], args[1]}
ignitionStorageCmp(ts, neg, argsNext)
}
// IsoContains `isoPath` `file` check if the specified `file` is stored
// within the ISO `isoPath` image.
func IsoContains(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: isoContains isoPath file")
}
workDir := ts.Getenv("WORK")
isoPath, filePath := args[0], args[1]
isoPathAbs := filepath.Join(workDir, isoPath)
disk, err := diskfs.Open(isoPathAbs, diskfs.WithOpenMode(diskfs.ReadOnly))
ts.Check(err)
fs, err := disk.GetFilesystem(0)
ts.Check(err)
_, err = fs.OpenFile(filePath, os.O_RDONLY)
ts.Check(err)
}
// ExpandFile `file...` can be used to substitute environment variables
// references for each file specified.
func ExpandFile(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 1 {
ts.Fatalf("usage: expandFile file...")
}
workDir := ts.Getenv("WORK")
for _, f := range args {
fileName := filepath.Join(workDir, f)
data, err := os.ReadFile(fileName)
ts.Check(err)
newData := expand(ts, data)
err = os.WriteFile(fileName, []byte(newData), 0)
ts.Check(err)
}
}
// IsoSizeMin `isoPath` `size` checks if the specified ISO is larger
// than the specified number of bytes.
func IsoSizeMin(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: IsoSizeMin isoPath size")
}
isoPath := args[0]
size, err := strconv.ParseInt(args[1], 10, 64)
ts.Check(err)
size *= 1000000
fileSize := isoSize(ts, isoPath)
if fileSize < size {
ts.Fatalf("%s of size %d bytes is less than %d", isoPath, fileSize, size)
}
}
// IsoSizeMax `isoPath` `size` checks if the specified ISO is smaller
// than the specified number of bytes.
func IsoSizeMax(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 2 {
ts.Fatalf("usage: IsoSizeMax isoPath size")
}
isoPath := args[0]
size, err := strconv.ParseInt(args[1], 10, 64)
ts.Check(err)
size *= 1000000
fileSize := isoSize(ts, isoPath)
if fileSize > size {
ts.Fatalf("%s of size %d bytes is greater than %d", isoPath, fileSize, size)
}
}
func isoSize(ts *testscript.TestScript, path string) int64 {
workDir := ts.Getenv("WORK")
isoPathAbs := filepath.Join(workDir, path)
fileInfo, err := os.Stat(isoPathAbs)
ts.Check(err)
return fileInfo.Size()
}
// ignitionStorageCmp `ignPath` `ignFile` `expectedFile` check that the content of the file
// `ignFile` - extracted from the ignition configuration file referenced
// by `ignPath` - matches the content of the local file `expectedFile`.
// Environment variables in in `expectedFile` are substituted before the comparison.
func ignitionStorageCmp(ts *testscript.TestScript, neg bool, args []string) {
if len(args) != 3 {
ts.Fatalf("usage: ignitionStorageCmp ignPath file1 file2")
}
workDir := ts.Getenv("WORK")
ignPath, aFilePath, eFilePath := args[0], args[1], args[2]
ignPathAbs := filepath.Join(workDir, ignPath)
config, err := readIgnition(ts, ignPathAbs)
ts.Check(err)
aData, err := readFileFromIgnitionCfg(&config, aFilePath)
ts.Check(err)
eFilePathAbs := filepath.Join(workDir, eFilePath)
eData, err := os.ReadFile(eFilePathAbs)
ts.Check(err)
byteCompare(ts, neg, aData, eData, aFilePath, eFilePath)
}
func readIgnition(ts *testscript.TestScript, ignPath string) (config igntypes.Config, err error) {
rawIgn, err := os.ReadFile(ignPath)
ts.Check(err)
err = json.Unmarshal(rawIgn, &config)
return config, err
}
// archiveFileNames `isoPath` get the names of the archive files to use
// based on the name of the ISO image.
func archiveFileNames(isoPath string) (string, string, error) {
if strings.HasPrefix(isoPath, "agent.") || strings.HasPrefix(isoPath, "node.") {
return "/images/ignition.img", "config.ign", nil
} else if strings.HasPrefix(isoPath, "agentconfig.") {
return "/config.gz", "", nil
}
return "", "", errors.NotFound(fmt.Sprintf("ISO %s has unrecognized prefix", isoPath))
}
func expand(ts *testscript.TestScript, s []byte) string {
return os.Expand(string(s), func(key string) string {
if key == "$" {
return "$"
}
return ts.Getenv(key)
})
}
func byteCompare(ts *testscript.TestScript, neg bool, aData, eData []byte, aFilePath, eFilePath string) {
byteCompareInternal(ts, neg, aData, eData, aFilePath, eFilePath, func(aText, eText string) (bool, error) {
return aText == eText, nil
})
}
func byteCompareRegEx(ts *testscript.TestScript, neg bool, aData, eData []byte, aFilePath, eFilePath string) {
byteCompareInternal(ts, neg, aData, eData, aFilePath, eFilePath, func(aText, eText string) (bool, error) {
return regexp.MatchString(eText, aText)
})
}
func byteCompareInternal(ts *testscript.TestScript, neg bool, aData, eData []byte, aFilePath, eFilePath string, cmp func(string, string) (bool, error)) {
aText := string(aData)
eText := expand(ts, eData)
eq, err := cmp(aText, eText)
if err != nil {
ts.Fatalf("unexpected error while comparing strings: %v", err)
}
if neg {
if eq {
ts.Fatalf("%s and %s do not differ", aFilePath, eFilePath)
}
return
}
if eq {
return
}
ts.Logf("%s", aText)
var sb strings.Builder
if err := diff.Text(eFilePath, aFilePath, eText, aText, &sb); err != nil {
ts.Check(err)
}
ts.Logf("%s", sb.String())
ts.Fatalf("%s and %s differ", eFilePath, aFilePath)
}
func readFileFromISO(isoPath, archiveFile, ignitionFile, nodePath string) ([]byte, error) {
config, err := extractCfgStorageData(isoPath, archiveFile, ignitionFile, nodePath)
if err != nil {
return nil, err
}
return config, nil
}
func readFileFromIgnitionCfg(config *igntypes.Config, nodePath string) ([]byte, error) {
for _, f := range config.Storage.Files {
if f.Node.Path == nodePath {
actualData, err := dataurl.DecodeString(*f.FileEmbedded1.Contents.Source)
if err != nil {
return nil, err
}
return actualData.Data, nil
}
}
return nil, errors.NotFound(nodePath)
}
func extractArchiveFile(isoPath, archive, fileName string) ([]byte, error) {
disk, err := diskfs.Open(isoPath, diskfs.WithOpenMode(diskfs.ReadOnly))
if err != nil {
return nil, err
}
fs, err := disk.GetFilesystem(0)
if err != nil {
return nil, err
}
ignitionImg, err := fs.OpenFile(archive, os.O_RDONLY)
if err != nil {
return nil, err
}
gzipReader, err := gzip.NewReader(ignitionImg)
if err != nil {
return nil, err
}
cpioReader := cpio.NewReader(gzipReader)
for {
header, err := cpioReader.Next()
if err == io.EOF { //nolint:errorlint
// end of cpio archive
break
}
if err != nil {
return nil, err
}
// If the file is not in ignition return it directly
if fileName == "" || header.Name == fileName {
rawContent, err := io.ReadAll(cpioReader)
if err != nil {
return nil, err
}
return rawContent, nil
}
}
return nil, errors.NotFound(fmt.Sprintf("File %s not found within the %s archive", fileName, archive))
}
func extractIgnition(isoPath, archiveFile, ignitionFile string) (igntypes.Config, error) {
var config igntypes.Config
rawContent, err := extractArchiveFile(isoPath, archiveFile, ignitionFile)
if err != nil {
return config, err
}
err = json.Unmarshal(rawContent, &config)
if err != nil {
return config, err
}
return config, nil
}
func extractCfgStorageData(isoPath, archiveFile, ignitionFile, nodePath string) ([]byte, error) {
if ignitionFile == "" {
// If the archive is not part of an ignition file return the archive data
rawContent, err := extractArchiveFile(isoPath, archiveFile, nodePath)
if err != nil {
return nil, err
}
return rawContent, nil
}
config, err := extractIgnition(isoPath, archiveFile, ignitionFile)
if err != nil {
return nil, err
}
for _, f := range config.Storage.Files {
if f.Node.Path == nodePath {
actualData, err := dataurl.DecodeString(*f.FileEmbedded1.Contents.Source)
if err != nil {
return nil, err
}
return actualData.Data, nil
}
}
return nil, errors.NotFound(fmt.Sprintf("File %s not found within the %s archive", nodePath, archiveFile))
}
func checkFileFromInitrdImg(isoPath string, fileName string) error {
disk, err := diskfs.Open(isoPath, diskfs.WithOpenMode(diskfs.ReadOnly))
if err != nil {
return err
}
fs, err := disk.GetFilesystem(0)
if err != nil {
return err
}
initRdImg, err := fs.OpenFile("/images/pxeboot/initrd.img", os.O_RDONLY)
if err != nil {
return err
}
defer initRdImg.Close()
const (
gzipID1 = 0x1f
gzipID2 = 0x8b
gzipDeflate = 0x08
)
buff := make([]byte, 4096)
for {
_, err := initRdImg.Read(buff)
if err == io.EOF { //nolint:errorlint
break
}
foundAt := -1
for idx := 0; idx < len(buff)-2; idx++ {
// scan the buffer for a potential gzip header
if buff[idx+0] == gzipID1 && buff[idx+1] == gzipID2 && buff[idx+2] == gzipDeflate {
foundAt = idx
break
}
}
if foundAt >= 0 {
// check if it's really a compressed cpio archive
delta := int64(foundAt - len(buff))
newPos, err := initRdImg.Seek(delta, io.SeekCurrent)
if err != nil {
break
}
files, err := lookForCpioFiles(initRdImg)
if err != nil {
if _, err := initRdImg.Seek(newPos+2, io.SeekStart); err != nil {
break
}
continue
}
// check if the current cpio files match the required ones
for _, f := range files {
matched, err := filepath.Match(fileName, f)
if err != nil {
return err
}
if matched {
return nil
}
}
}
}
return errors.NotFound(fmt.Sprintf("File %s not found within the /images/pxeboot/initrd.img archive", fileName))
}
func lookForCpioFiles(r io.Reader) ([]string, error) {
var files []string
gr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gr.Close()
// skip in case of garbage
if gr.OS != 255 && gr.OS >= 13 {
return nil, fmt.Errorf("unknown OS code: %v", gr.Header.OS)
}
cr := cpio.NewReader(gr)
for {
h, err := cr.Next()
if err != nil {
break
}
files = append(files, h.Name)
}
return files, nil
}