package patchmanager import ( "archive/tar" "compress/gzip" "errors" "fmt" "io" fsPkg "io/fs" "os" "path/filepath" "reflect" "strings" "testing" "github.com/go-logr/logr" versionPkg "github.com/hashicorp/go-version" "gotest.tools/v3/fs" "edge-infra.dev/pkg/lib/logging" common "edge-infra.dev/pkg/sds/patching/common" ) func newConfig(t *testing.T) common.Config { dir := fs.NewDir(t, "testing") MountPath := dir.Path() c := common.Config{ MountPath: MountPath, ArtefactsPath: MountPath + "/versions", ArtefactsTempPath: MountPath + "/versions/.tmp", LiveBootPath: MountPath + "/live", ScriptsPath: MountPath + "/patching", ScriptsTempPath: MountPath + "/patching/.tmp", EnvFilePath: MountPath + "/patching/patching.env", LegacyScriptsPath: MountPath + "/upgrade", BootFiles: MountPath + "/boot/", ScriptFiles: MountPath + "/patching/", PatchsetMount: MountPath + "/mnt/patchset", RebootPath: MountPath + "/mnt/run/reboot-required", } return c } func createFileTree(cfg common.Config) error { items := []string{ cfg.MountPath, cfg.ArtefactsPath, cfg.ArtefactsTempPath, cfg.LiveBootPath, cfg.ScriptsPath, cfg.ScriptsTempPath, cfg.EnvFilePath, cfg.LegacyScriptsPath, cfg.BootFiles, cfg.ScriptFiles, cfg.MountPath + "/mnt", cfg.MountPath + "/mnt/run", cfg.MountPath + "/boot", } for _, item := range items { if err := os.MkdirAll(item, 0744); err != nil { return err } } return nil } func createTempFile(dir string, count int) ([]os.File, error) { tempFiles := make([]os.File, count) for i := 0; i < count; i++ { tempFile, err := os.CreateTemp(dir, "") if err != nil { return nil, fmt.Errorf("error creating tempfile: %s", err) } tempFiles[i] = *tempFile tempFile.Close() } return tempFiles, nil } func setupTarArchive(filePath string, tempFiles []os.File, cfg common.Config) (*os.File, error) { writeArchive, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0744) if err != nil { return nil, fmt.Errorf("error opening archive file: %s", err) } gw := gzip.NewWriter(writeArchive) defer gw.Close() tw := tar.NewWriter(gw) defer tw.Close() for _, f := range tempFiles { file, err := os.Open(f.Name()) if err != nil { return nil, fmt.Errorf("error opening tempfile: %s", err) } tempFileInfo, err := file.Stat() if err != nil { return nil, fmt.Errorf("%s", err) } header, err := tar.FileInfoHeader(tempFileInfo, tempFileInfo.Name()) if err != nil { return nil, fmt.Errorf("error creating tempfile header: %s", err) } header.Name = strings.TrimPrefix(strings.Replace(file.Name(), cfg.MountPath, "", -1), string(filepath.Separator)) err = tw.WriteHeader(header) if err != nil { return nil, fmt.Errorf("error writing header to archive: %s", err) } _, err = io.Copy(tw, file) if err != nil { return nil, fmt.Errorf("error copying tempfile to archive: %s", err) } if err := file.Close(); err != nil { return nil, fmt.Errorf("error closing tempfile: %s", err) } } readArchive, err := os.OpenFile(filePath, os.O_RDONLY, 0744) if err != nil { return nil, fmt.Errorf("error opening archive file: %s", err) } return readArchive, nil } func createDummyScripts(t *testing.T, filePath, phaseScriptsTempPath, phaseScriptsPath string) { if err := os.MkdirAll(phaseScriptsTempPath, 0744); err != nil { t.Errorf("unable to create temp script phase directories: %s", err) } if err := os.MkdirAll(phaseScriptsPath, 0744); err != nil { t.Errorf("unable to create script phase directories: %s", err) } file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0744) if err != nil { t.Errorf("unable to create dummy scripts: %s", err) } defer file.Close() } func TestGetArtefactsPath(t *testing.T) { cfg := newConfig(t) version := "v1.10.1" got := GetArtefactsPath(version, cfg) expect := cfg.MountPath + "/versions/v1.10.1" if got != expect { t.Errorf("unexpected result. Wanted: %s; Got: %s", expect, got) } t.Logf("ArtefactsPath: %s", got) } func TestValidateChecksum(t *testing.T) { scenarios := []struct { cfg common.Config checksum, contents string }{ {cfg: newConfig(t), checksum: "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", contents: "foobar"}, } for _, s := range scenarios { file, err := os.CreateTemp(s.cfg.MountPath, "TestValidateChecksum") if err != nil { t.Errorf("error while creating temp file: %s", err) } defer file.Close() _, err = file.Write([]byte(s.contents)) if err != nil { t.Errorf("error while writing to temp file: %s", err) } err = ValidateChecksum(file.Name(), s.checksum) if err != nil { t.Errorf("error: Got %s. Wanted %s.", err, s.checksum) } t.Logf("Checksum: %s", s.checksum) } } func TestGetSha256(t *testing.T) { scenarios := []struct { cfg common.Config checksum, contents string }{ {cfg: newConfig(t), checksum: "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", contents: "foobar"}, } for _, s := range scenarios { file, err := os.CreateTemp(s.cfg.MountPath, "TestValidateChecksum") if err != nil { t.Errorf("error while creating temp file: %s", err) } defer file.Close() _, err = file.Write([]byte(s.contents)) if err != nil { t.Errorf("error while writing to temp file: %s", err) } sha, err := GetSha256(file.Name()) if len(sha) != 64 { t.Errorf("SHA256sum value is invalid; not 64 characters") } else if err != nil { t.Errorf("error: Got %s. Wanted %s.", err, s.checksum) } } } func TestLoadDirectivesConfig(t *testing.T) { scenarios := []struct { cfg common.Config versions []string source string expect []DirectivesConfig }{ { // Multiple directives and script options cfg: newConfig(t), versions: []string{ "v1.11.0", }, source: `directives: - regex: (v1.10.1) script_options: - name: v1.09.4 skip: false weight: 1 - name: v1.10.1 skip: true weight: 2 - name: v1.10.1 - regex: (v1.11.1) script_options: - name: v1.11.1 skip: false weight: 3`, expect: []DirectivesConfig{ { Directives: []struct { Regex string `yaml:"regex"` ScriptOptions []struct { Name string Skip bool Weight int } `yaml:"script_options"` }{ { Regex: "(v1.10.1)", ScriptOptions: []struct { Name string Skip bool Weight int }{ { Name: "v1.09.4", Skip: false, Weight: 1, }, { Name: "v1.10.1", Skip: true, Weight: 2, }, { Name: "v1.10.1", // Skip and Weight default to equivalent zero-values Skip: false, Weight: 0, }, }, }, { Regex: "(v1.11.1)", ScriptOptions: []struct { Name string Skip bool Weight int }{ { Name: "v1.11.1", Skip: false, Weight: 3, }, }, }, }, }, }, }, } for _, s := range scenarios { versions := make([]*versionPkg.Version, len(s.versions)) for i, v := range s.versions { version, err := versionPkg.NewVersion(v) if err != nil { t.Errorf("error converting slice of version strings into version structs: %s", err) } versions[i] = version configPath := filepath.Join(s.cfg.ScriptsTempPath, version.Original(), "config.yaml") if err := os.MkdirAll(filepath.Dir(configPath), 0744); err != nil { t.Errorf("error creating filepath: %s", err) } file, err := os.OpenFile(configPath, os.O_RDWR|os.O_CREATE, 0744) if err != nil { t.Errorf("error creating config.yaml: %s", err) } _, err = file.Write([]byte(s.source)) if err != nil { t.Errorf("error writing YAML string to config.yaml: %s", err) } if err := file.Close(); err != nil { t.Errorf("error closing file: %s", err) } } got, err := loadDirectivesConfig(versions, s.cfg) if err != nil { t.Errorf("%s", err) } if reflect.DeepEqual(got, s.expect) != true { t.Errorf("unexpected return value for loadDirectivesConfig. Got %v. Expected %v", got, s.expect) } } } func TestInstallScriptsVersion(t *testing.T) { scenarios := []struct { cfg common.Config version, currentVer string isControlPlane bool log logr.Logger hashMaps map[string]map[string]string configs []DirectivesConfig }{ { // Multiple directives and script options cfg: newConfig(t), version: "v1.11.0", currentVer: "v1.10.1", isControlPlane: true, log: logging.NewLogger().Logger, hashMaps: make(map[string]map[string]string), configs: []DirectivesConfig{ { Directives: []struct { Regex string `yaml:"regex"` ScriptOptions []struct { Name string Skip bool Weight int } `yaml:"script_options"` }{ { Regex: "v1.10.1", ScriptOptions: []struct { Name string Skip bool Weight int }{ { Name: "01_script.sh", Skip: false, Weight: 1, }, }, }, }, }, }, }, } for _, s := range scenarios { version, err := versionPkg.NewVersion(s.version) if err != nil { t.Errorf("error converting slice of version strings into version structs: %s", err) } currentVer, err := versionPkg.NewVersion(s.currentVer) if err != nil { t.Errorf("error converting slice of version strings into version structs: %s", err) } for _, phase := range upgradePhases { var ( phaseScriptsTempPath = filepath.Join(s.cfg.ScriptsTempPath, version.Original(), phase) phaseScriptsPath = filepath.Join(s.cfg.ScriptsPath, phase) filePaths []string ) filePaths = append(filePaths, filepath.Join(phaseScriptsTempPath, "01_script.sh")) if phase == "post-reboot" { filePaths = append(filePaths, filepath.Join(phaseScriptsTempPath, "02_script.sh")) // tests skip duplicate scripts branch } for _, filePath := range filePaths { createDummyScripts(t, filePath, phaseScriptsPath, phaseScriptsTempPath) } s.hashMaps[phase] = make(map[string]string) } err = installScriptsVersion(currentVer, version, s.configs, s.isControlPlane, s.log, s.cfg, s.hashMaps) if err != nil { t.Errorf("%s", err) } } } func TestExtractTarFile(t *testing.T) { scenarios := []struct { cfg common.Config log logr.Logger subPath string }{ {cfg: newConfig(t), log: logging.NewLogger().Logger, subPath: "boot"}, {cfg: newConfig(t), log: logging.NewLogger().Logger, subPath: "patching"}, } for _, s := range scenarios { var dir = filepath.Join(s.cfg.MountPath, s.subPath) var path = filepath.Join(dir, "archive.tar.gz") if err := createFileTree(s.cfg); err != nil { t.Errorf("unable to create file tree: %s", err) } tempFiles, err := createTempFile(dir, 1) if err != nil { t.Errorf("error creating tempfile: %s", err) } archive, err := setupTarArchive(path, tempFiles, s.cfg) if err != nil { t.Errorf("error creating archive file: %s", err) } err = extractTarFile(archive, s.log, s.cfg) if err != nil { t.Errorf("error extracting tar archive: %s", err) } } } func TestPrepareDirectories(t *testing.T) { scenarios := []struct { cfg common.Config targetPath string }{ {cfg: newConfig(t), targetPath: "/boot/versions/v1.11.0"}, } for _, s := range scenarios { if err := prepareDirectories(s.targetPath, s.cfg); err != nil { t.Errorf("%s", err) } dirs, err := os.ReadDir(s.cfg.ScriptsPath) if err != nil { t.Errorf("error reading prepared directories: %s", err) } t.Logf("Script directories: %v", dirs) } } func TestCreateScriptDirectories(t *testing.T) { scenarios := []struct { cfg common.Config }{ {cfg: newConfig(t)}, } for _, s := range scenarios { if err := createScriptDirectories(s.cfg); err != nil { t.Errorf("%s", err) } dirs, err := os.ReadDir(s.cfg.ScriptsPath) if err != nil { t.Errorf("error reading prepared directories: %s", err) } t.Logf("Script directories: %v", dirs) } } func TestHousekeepVersions(t *testing.T) { scenarios := []struct { cfg common.Config retentionVers []string log logr.Logger }{ { cfg: newConfig(t), retentionVers: []string{"v1.10.1", "v1.11.0"}, log: logging.NewLogger().Logger, }, } for _, s := range scenarios { if err := createFileTree(s.cfg); err != nil { t.Errorf("unable to create file tree: %s", err) } versions := []string{"v1.9.2", "v1.10.1", "v1.11.0", "v1.12.0"} for _, v := range versions { _, err := os.Create(filepath.Join(s.cfg.ArtefactsPath, v)) if err != nil { t.Errorf("unable to create version files: %s", err) } } if err := housekeepVersions(s.retentionVers, s.log, s.cfg); err != nil { t.Errorf("%s", err) } } } func TestVersionSort(t *testing.T) { scenarios := []struct { cfg common.Config unsortedVersions, expect []string }{ { cfg: newConfig(t), unsortedVersions: []string{"v1.12.0", "v1.9.2", "v1.11.0", "v1.10.1", "v1.12.0-123456"}, expect: []string{"v1.9.2", "v1.10.1", "v1.11.0", "v1.12.0-123456", "v1.12.0"}, }, } for _, s := range scenarios { var ( versionsRaw = make([]fsPkg.FileInfo, len(s.unsortedVersions)) sortedVersions = make([]string, len(s.unsortedVersions)) ) if err := createFileTree(s.cfg); err != nil { t.Errorf("unable to create file tree: %s", err) } for i, v := range s.unsortedVersions { filePath := filepath.Join(s.cfg.ArtefactsPath, v) _, err := os.Create(filePath) if err != nil { t.Errorf("unable to create version files: %s", err) } fileInfo, err := os.Stat(filePath) if err != nil { t.Errorf("%s", err) } versionsRaw[i] = fileInfo } got, err := versionSort(versionsRaw) if err != nil { t.Errorf("%s", err) } for i := range got { sortedVersions[i] = got[i].Original() } if reflect.DeepEqual(sortedVersions, s.expect) != true { t.Errorf("unexpected return value for versionSort. Got %v. Expected %v", got, s.expect) } t.Logf("Sorted versions: %s", sortedVersions) } } func TestPrepareLegacy(t *testing.T) { scenarios := []struct { cfg common.Config currentVer string log logr.Logger legacy bool }{ { cfg: newConfig(t), currentVer: "v1.11.0", log: logging.NewLogger().Logger, legacy: false, }, { cfg: newConfig(t), currentVer: "v1.9.2", log: logging.NewLogger().Logger, legacy: false, }, { cfg: newConfig(t), currentVer: "v1.9.2", log: logging.NewLogger().Logger, legacy: true, }, } for _, s := range scenarios { if s.legacy == true { if err := os.MkdirAll(s.cfg.LegacyScriptsPath, 0744); err != nil { t.Errorf("error creating upgrade directory: %s", err) } } if err := prepareLegacy(s.currentVer, s.log, s.cfg); err != nil { t.Errorf("%s", err) } } } func TestPadVersion(t *testing.T) { scenarios := []struct { versionString, expect string }{ { versionString: "v1.11.0", expect: "v01.11.00", }, { versionString: "v1.11.0-beta+012345", expect: "v01.11.00-beta", }, } for _, s := range scenarios { version, err := versionPkg.NewVersion(s.versionString) if err != nil { t.Errorf("%s", err) } got := padVersion(version) if got != s.expect { t.Errorf("unexpected return value. Got: %s, expected: %s", got, s.expect) } t.Logf("Padded version: %s", got) } } func TestRoleFilter(t *testing.T) { scenarios := []struct { fileName string isControlPlane, expect bool }{ { fileName: "01_script.cp.sh", isControlPlane: true, expect: false, }, { fileName: "01_script.cp.sh", isControlPlane: false, expect: true, }, { fileName: "01_script.tp.sh", isControlPlane: true, expect: true, }, { fileName: "01_script.tp.sh", isControlPlane: false, expect: false, }, { fileName: "01_script.sh", isControlPlane: true, expect: false, }, { fileName: "01_script.sh", isControlPlane: false, expect: false, }, } for _, s := range scenarios { if got := roleFilter(s.fileName, s.isControlPlane); got != s.expect { t.Errorf("unexpected return value. Got: %t, expected: %t\n", got, s.expect) } } } func TestIsExecutable(t *testing.T) { scenarios := []struct { cfg common.Config filePath string expect bool }{ { cfg: newConfig(t), filePath: "01_script.cp.sh", expect: true, }, { cfg: newConfig(t), filePath: "01_script.cp.sh", expect: false, }, } for _, s := range scenarios { if err := createFileTree(s.cfg); err != nil { t.Errorf("error creating file tree: %s", err) } var filePath = filepath.Join(s.cfg.MountPath, s.filePath) file, err := os.Create(filePath) if err != nil { t.Errorf("%s", err) } switch s.expect { case true: if err := os.Chmod(file.Name(), 0777); err != nil { t.Errorf("an error occurred when modifying file permissions: %s", err) } case false: if err := os.Chmod(file.Name(), 0666); err != nil { t.Errorf("an error occurred when modifying file permissions: %s", err) } } if got := IsExecutable(filePath); got != s.expect { t.Errorf("unexpected return value. Got: %t, expected: %t\n", got, s.expect) } } } func TestCheckDirectives(t *testing.T) { scenarios := []struct { name, currentVer string expect struct { skip bool weight int } }{ { name: "01_script.sh", currentVer: "v1.11.1", // Regex won't match expect: struct { skip bool weight int }{ skip: false, weight: 10, }, }, { name: "100_script.sh", // Script doesn't exist currentVer: "v1.10.1", expect: struct { skip bool weight int }{ skip: false, weight: 10, }, }, { name: "01_script.sh", currentVer: "v1.10.1", expect: struct { skip bool weight int }{ skip: true, weight: 1, }, }, } config := []DirectivesConfig{ { Directives: []struct { Regex string `yaml:"regex"` ScriptOptions []struct { Name string Skip bool Weight int } `yaml:"script_options"` }{ { Regex: "v1.10.1", ScriptOptions: []struct { Name string Skip bool Weight int }{ { Name: "01_script.sh", Skip: true, Weight: 1, }, }, }, }, }, } for _, s := range scenarios { skip, weight := checkDirectives(config, s.currentVer, s.name) if skip != s.expect.skip { t.Errorf("unexpected return value. Got: %+v, expected: %+v\n", skip, s.expect.skip) } if weight != s.expect.weight { t.Errorf("unexpected return value. Got: %+v, expected: %+v\n", weight, s.expect.weight) } } } func TestGetDestPath(t *testing.T) { scenarios := []struct { cfg common.Config subPath string }{ { cfg: newConfig(t), subPath: "boot", }, { cfg: newConfig(t), subPath: "patching", }, } for _, s := range scenarios { var path = filepath.Join(s.cfg.MountPath, s.subPath) if err := createFileTree(s.cfg); err != nil { t.Errorf("error creating file tree: %s", err) } tempFile, err := os.CreateTemp(path, "") if err != nil { t.Errorf("error creating tempfile: %s", err) } var header = strings.TrimPrefix(strings.Replace(tempFile.Name(), s.cfg.MountPath, "", -1), "/") got, err := getDestPath(header, s.cfg) if err != nil { t.Errorf("%s", err) } var expect string switch s.subPath { case "boot": var fileName = strings.TrimPrefix(strings.Replace(tempFile.Name(), s.cfg.BootFiles, "", -1), "/") expect = filepath.Join(s.cfg.ArtefactsTempPath, fileName) case "patching": var fileName = strings.TrimPrefix(strings.Replace(tempFile.Name(), s.cfg.ScriptFiles, "", -1), "/") expect = filepath.Join(s.cfg.ScriptsTempPath, fileName) } if got != expect { t.Errorf("unexpected return value. Got: %s, expected: %s", got, expect) } t.Logf("DestPath for %s: %s", s.subPath, got) } } func TestWriteFile(t *testing.T) { scenarios := []struct { cfg common.Config log logr.Logger path, dir string }{ { cfg: newConfig(t), log: logging.NewLogger().Logger, }, } for _, s := range scenarios { if err := createFileTree(s.cfg); err != nil { t.Errorf("error creating file tree: %s", err) } tempFiles, err := createTempFile(s.cfg.ArtefactsTempPath, 2) if err != nil { t.Errorf("error creating tempfiles: %s", err) } var filepath = filepath.Join(s.cfg.ArtefactsTempPath, "archive.tar.gz") archive, err := setupTarArchive(filepath, tempFiles, s.cfg) if err != nil { t.Errorf("error creating archive: %s", err) } gr, err := gzip.NewReader(archive) if err != nil { t.Errorf("error creating gzip reader: %s", err) } defer gr.Close() tr := tar.NewReader(gr) for _, file := range tempFiles { filePath := file.Name() if err := os.Remove(filePath); err != nil { t.Errorf("error deleting file: %s", err) } header, err := tr.Next() if err != nil { t.Errorf("error advancing to next archive entry: %s", err) } if err := writeFile(filePath, tr, header, s.log); err != nil { t.Errorf("error extracting patchset: %s", err) } file, err := os.OpenFile(filePath, os.O_RDONLY, 0644) if errors.Is(err, fsPkg.ErrNotExist) { t.Errorf("file does not exist: %s", err) } else if err != nil { t.Errorf("error opening file: %s", err) } defer file.Close() } } } func TestExtractPatchset(t *testing.T) { scenarios := []struct { cfg common.Config log logr.Logger targetVersion string }{ { cfg: newConfig(t), log: logging.NewLogger().Logger, targetVersion: "v1.11.0", }, } for _, s := range scenarios { if err := createFileTree(s.cfg); err != nil { t.Errorf("error creating file tree: %s", err) } tempFiles, err := createTempFile(s.cfg.BootFiles, 2) if err != nil { t.Errorf("error creating temp files: %s", err) } _, err = setupTarArchive(s.cfg.PatchsetMount, tempFiles, s.cfg) if err != nil { t.Errorf("error creating archive: %s", err) } if err := extractPatchset(s.targetVersion, s.log, s.cfg); err != nil { t.Errorf("error extracting patchset, %s", err) } } } func TestMain(m *testing.M) { os.Exit(m.Run()) }