diff --git a/.github/workflows/PR.yaml b/.github/workflows/PR.yaml index 7fe051f3..bf0e3c55 100644 --- a/.github/workflows/PR.yaml +++ b/.github/workflows/PR.yaml @@ -7,19 +7,6 @@ permissions: contents: read jobs: - # While golanglint-ci is also run in the mage file, - # adding an explicit gha step highlights the syntax errors - # when reviewing PRs - golint: - runs-on: windows-2022 - steps: - - uses: actions/checkout@v4 - - name: golangci-lint - uses: golangci/golangci-lint-action@v6.1.0 - with: - args: --timeout=10m - version: v1.60 - test: strategy: fail-fast: false @@ -37,12 +24,23 @@ jobs: uses: actions/setup-go@v5 with: go-version: 'stable' - + - name: Install Dependencies run: | go install github.com/magefile/mage@v1.15.0 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.0 + - name: Build + shell: pwsh + run: | + set PSModulePath=&&powershell -command "mage BuildAll" + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6.1.0 + with: + args: --timeout=10m + version: v1.60 + - name: Run E2E tests shell: pwsh run: | diff --git a/.golangci.json b/.golangci.json index 90618788..b93b8a36 100644 --- a/.golangci.json +++ b/.golangci.json @@ -17,7 +17,8 @@ "scripts", "charts", "package", - "pkg/powershell/powershell.go" + "pkg/powershell/powershell.go", + "suc/pkg/host/embed.go" ] } } diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 8cd12b5d..8cb75e73 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -101,12 +101,12 @@ func Validate() error { } func BuildAll() error { - mg.SerialDeps(Build, BuildSUC) + mg.SerialDeps(Build, BuildSUC, Validate) return nil } func Build() error { - mg.Deps(Clean, Dependencies, Validate) + mg.Deps(Clean, Dependencies) winsOutput := filepath.Join("bin", "wins.exe") log.Printf("[Build] Building wins version: %s \n", version) @@ -175,6 +175,12 @@ func Build() error { func BuildSUC() error { log.Printf("[Build] Building wins SUC version: %s \n", version) + // move wins.exe into the suc package so that it can be embedded + err := sh.Copy(filepath.Join("suc/pkg/host/wins.exe"), filepath.Join(artifactOutput, "wins.exe")) + if err != nil { + log.Printf("failed to copy wins.exe to suc/pkg/host") + return err + } winsSucOutput := filepath.Join("bin", "wins-suc.exe") if err := g.Build(flags, "suc/main.go", winsSucOutput); err != nil { return err @@ -198,7 +204,7 @@ func Test() error { // Integration target must be run on a wins system // with Containers feature / docker installed func Integration() error { - mg.Deps(Build) + mg.Deps(BuildAll) log.Printf("[Integration] Starting Integration Test for wins version %s \n", version) // make sure the docker files have access to the exe @@ -221,20 +227,12 @@ func Integration() error { } func TestAll() error { - mg.Deps(BuildAll) - // don't run Test and Integration in mg.Deps - // as deps run in an unordered asynchronous fashion - if err := Test(); err != nil { - return err - } - if err := Integration(); err != nil { - return err - } + mg.SerialDeps(Test, Integration) return nil } func CI() { - mg.Deps(Test) + mg.Deps(TestAll) } func flags(version string, commit string) string { diff --git a/suc/pkg/cmd.go b/suc/pkg/cmd.go index 85500994..e7a3a6f8 100644 --- a/suc/pkg/cmd.go +++ b/suc/pkg/cmd.go @@ -4,10 +4,11 @@ import ( "errors" "fmt" + "github.com/rancher/wins/suc/pkg/host" "github.com/rancher/wins/suc/pkg/rancher" "github.com/rancher/wins/suc/pkg/service" "github.com/rancher/wins/suc/pkg/service/config" - "github.com/rancher/wins/suc/pkg/service/state" + "github.com/rancher/wins/suc/pkg/state" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -49,7 +50,12 @@ func Run(_ *cli.Context) error { errs = append(errs, err) } - if restartServiceDueToConfigChange { + restartServiceDueToBinaryUpgrade, err := host.UpgradeRancherWinsBinary() + if err != nil { + return fmt.Errorf("failed to upgrade wins.exe: %w", err) + } + + if restartServiceDueToConfigChange || restartServiceDueToBinaryUpgrade { err = service.RefreshWinsService() if err != nil { errs = append(errs, fmt.Errorf("error encountered while attempting to restart rancher-wins: %w", err)) diff --git a/suc/pkg/host/common.go b/suc/pkg/host/common.go new file mode 100644 index 00000000..eff48567 --- /dev/null +++ b/suc/pkg/host/common.go @@ -0,0 +1,162 @@ +package host + +import ( + "errors" + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +const ( + defaultWinsPath = "c:\\Windows\\wins.exe" + defaultWinsUsrLocalBinPath = "c:\\usr\\local\\bin\\wins.exe" + defaultConfigDir = "c:\\etc\\rancher\\wins" + fileOperationAttempts = 5 + fileOperationAttemptDelayInSeconds = 5 + + // skipBinaryUpgradeEnvVar prevents the suc image from attempting to upgrade the wins binary. + // This is primarily used in CI, to allow for test cases to run without having to completely + // install rancher-wins. + skipBinaryUpgradeEnvVar = "CATTLE_WINS_SKIP_BINARY_UPGRADE" +) + +// getRancherWinsVersionFromBinary executes the wins.exe binary located at 'path' and passes the '--version' +// flag. The release version or commit hash is returned. If the binary returns unexpected output, +// was built with a dirty commit, or does not exist, an error will be returned. +func getRancherWinsVersionFromBinary(path string) (string, error) { + if path == "" { + return "", fmt.Errorf("must specify a path") + } + + _, err := os.Stat(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return "", fmt.Errorf("provided path (%s) does not exist", path) + } + return "", fmt.Errorf("encoutered error stat'ing '%s': %w", path, err) + } + + out, err := exec.Command(path, "--version").CombinedOutput() + if err != nil { + logrus.Errorf("could not invoke '%s --version' to determine installed wins.exe version: %v", path, err) + return "", fmt.Errorf("failed to invoke '%s --version': %w", path, err) + } + + logrus.Debugf("'%s --version' output: %s", path, string(out)) + return parseWinsVersion(string(out)) +} + +func confirmWinsBinaryIsInstalled() (bool, error) { + _, err := os.Stat(defaultWinsPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + return false, fmt.Errorf("could not determine if installed wins binary exists: %v", err) + } + return true, nil +} + +func confirmWinsBinaryVersion(desiredVersion string, path string) error { + installedVersion, err := getRancherWinsVersionFromBinary(path) + if err != nil { + return fmt.Errorf("failed to confirm '%s' version: %w", path, err) + } + + if installedVersion == desiredVersion { + logrus.Debugf("'%s' returned expected version (%s)", path, desiredVersion) + return nil + } + + return fmt.Errorf("'%s' version ('%s') did not match desired version ('%s')", path, installedVersion, desiredVersion) +} + +func parseWinsVersion(winsOutput string) (string, error) { + // Expected output format is 'rancher-wins version v0.x.y[-rc.z]'" + // A dirty binary will return 'rancher-wins version COMMIT-dirty' + // A non-tagged version will return 'rancher-wins version COMMIT' + s := strings.Split(winsOutput, " ") + if len(s) != 3 { + return "", fmt.Errorf("'wins.exe --version' did not return expected output length ('%v' was returned)", s) + } + + verString := strings.Trim(s[2], "\n") + // We should error out if the binary we're working with is dirty, but + // if it's simply untagged we should proceed with the upgrade. + if strings.Contains(verString, "dirty") { + return "", fmt.Errorf("wins.exe binary returned a dirty version (%s)", verString) + } + + return verString, nil +} + +// copyFile opens the file located at 'source' and creates a new file at 'destination' +// with the same contents. In the event that the 'source' or 'destination' file is being used, +// copyFile will reattempt the operation 5 times over the course of 25 seconds. If the file still cannot +// be moved, an error will be returned. This behavior is beneficial when handling binaries +// that are referenced by services, as the underlying binary used by a service may continue to run +// for a brief time after the service has processed the stop signal. +// +// Note that permission bits on Windows do not function in the same +// way as Linux, the owner bit is always copied to all other bits. The caller of copyFile must +// ensure that the destination is covered by appropriate access control lists. +func copyFile(source, dest string) error { + var err error + var b []byte + + _, err = os.Stat(source) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("specified source file '%s' cannot be copied as it does not exist: %w", source, err) + } + return fmt.Errorf("failed to stat source file '%s': %w", source, err) + } + + for i := 0; i < fileOperationAttempts; i++ { + b, err = os.ReadFile(source) + if err != nil { + if strings.Contains(err.Error(), "because it is being used by another process") { + logrus.Debugf("file copy attempt failed as the source file is in use, waiting %d seconds before reattempting", fileOperationAttemptDelayInSeconds) + time.Sleep(fileOperationAttemptDelayInSeconds * time.Second) + continue + } + return fmt.Errorf("failed to read from '%s': %w", source, err) + } + + err = os.WriteFile(dest, b, os.ModePerm) + if err != nil { + if strings.Contains(err.Error(), "because it is being used by another process") { + logrus.Debugf("file copy attempt failed as the destination file is in use, waiting %d seconds before reattempting", fileOperationAttemptDelayInSeconds) + time.Sleep(fileOperationAttemptDelayInSeconds * time.Second) + continue + } + return fmt.Errorf("failed to write to '%s': %w", dest, err) + } + } + + if err != nil { + return fmt.Errorf("failed to copy '%s' to '%s': %w", source, dest, err) + } + + return nil +} + +func getWinsConfigDir() string { + customPath := os.Getenv("CATTLE_AGENT_CONFIG_DIR") + if customPath != "" { + return customPath + } + return defaultConfigDir +} + +func getWinsUsrLocalBinBinary() string { + customPath := os.Getenv("CATTLE_AGENT_BIN_PREFIX") + if customPath != "" { + return fmt.Sprintf("%s\\bin\\wins.exe", customPath) + } + return defaultWinsUsrLocalBinPath +} diff --git a/suc/pkg/host/common_test.go b/suc/pkg/host/common_test.go new file mode 100644 index 00000000..8bcdca50 --- /dev/null +++ b/suc/pkg/host/common_test.go @@ -0,0 +1,63 @@ +package host + +import "testing" + +func TestParseWinsVersion(t *testing.T) { + type test struct { + name string + winsOutput string + expectedVersion string + errExpected bool + } + + tests := []test{ + { + name: "Released version", + winsOutput: "rancher-wins version v0.4.20", + expectedVersion: "v0.4.20", + errExpected: false, + }, + { + name: "RC version", + winsOutput: "rancher-wins version v0.4.20-rc.1", + expectedVersion: "v0.4.20-rc.1", + errExpected: false, + }, + { + name: "Dirty Commit", + winsOutput: "rancher-wins version 06685df-dirty", + expectedVersion: "", + errExpected: true, + }, + { + name: "Unreleased Clean Commit", + winsOutput: "rancher-wins version 06685df", + expectedVersion: "06685df", + errExpected: false, + }, + { + name: "Empty output", + winsOutput: "", + expectedVersion: "", + errExpected: true, + }, + { + name: "unexpected format output", + winsOutput: "rancher-wins version", + expectedVersion: "", + errExpected: true, + }, + } + + for _, tst := range tests { + t.Run(tst.name, func(t *testing.T) { + version, err := parseWinsVersion(tst.winsOutput) + if err != nil && !tst.errExpected { + t.Fatalf("encountered unexpected errror, wins output: '%s', returned version: '%s': %v", tst.winsOutput, version, err) + } + if version != tst.expectedVersion { + t.Fatalf("encountered unexpected version, wins output: '%s', returned version: '%s', expected version: '%s'", tst.winsOutput, version, tst.expectedVersion) + } + }) + } +} diff --git a/suc/pkg/host/embed.go b/suc/pkg/host/embed.go new file mode 100644 index 00000000..78712e4b --- /dev/null +++ b/suc/pkg/host/embed.go @@ -0,0 +1,6 @@ +package host + +import _ "embed" + +//go:embed wins.exe +var winsBinary []byte diff --git a/suc/pkg/host/upgrade.go b/suc/pkg/host/upgrade.go new file mode 100644 index 00000000..aaf87a9f --- /dev/null +++ b/suc/pkg/host/upgrade.go @@ -0,0 +1,131 @@ +package host + +import ( + "fmt" + "os" + "strings" + + "github.com/rancher/wins/pkg/defaults" + "github.com/rancher/wins/suc/pkg/service" + "github.com/sirupsen/logrus" +) + +// UpgradeRancherWinsBinary will attempt to upgrade the wins.exe binary installed on the host. +// The version to be installed is embedded within the SUC binary, located in the winsBinary variable. +// Upgrades will only be attempted if the CATTLE_WINS_SKIP_BINARY_UPGRADE environment variable is not set to 'true' or '$true', +// and the currently installed version differs from the one embedded (determined by the output of 'wins.exe --version'). +// During an upgrade attempt the rancher-wins service will be temporarily stopped. +// A boolean is returned to indicate if the rancher-wins service needs to be restarted due to a successful upgrade. +func UpgradeRancherWinsBinary() (bool, error) { + if strings.ToLower(os.Getenv(skipBinaryUpgradeEnvVar)) == "true" || + strings.ToLower(os.Getenv(skipBinaryUpgradeEnvVar)) == "$true" { + logrus.Warnf("environment variable '%s' was set to true, will not attempt to upgrade binary", skipBinaryUpgradeEnvVar) + return false, nil + } + + // we use the AppVersion set during compilation to indicate + // the version of wins.exe that is packaged in the SUC binary. + // See magetools/gotool.go for more information. + desiredVersion := defaults.AppVersion + + // We should never install a dirty version of wins.exe onto a host. + if strings.Contains(desiredVersion, "-dirty") { + return false, fmt.Errorf("will not attempt to upgrade wins.exe version, refusing to install embedded dirty version (version: %s)", desiredVersion) + } + + binaryExists, err := confirmWinsBinaryIsInstalled() + if err != nil { + return false, err + } + + if binaryExists { + currentVersion, err := getRancherWinsVersionFromBinary(defaultWinsPath) + if err != nil { + return false, fmt.Errorf("could not determine current wins.exe version: %w", err) + } + + if currentVersion == desiredVersion { + logrus.Debugf("wins.exe is up to date (%s)", currentVersion) + return false, nil + } + } + + restartService, upgradeErr := updateBinaries(desiredVersion) + if upgradeErr != nil { + return false, upgradeErr + } + + return restartService, nil +} + +// updateBinaries writes the embedded binary onto the disk in the rancher-wins config directory (c:\etc\rancher\wins, by default). +// Once written, the binary is invoked to ensure that it is not corrupted and is running the expected version. +// After confirming the version, the updated binary is moved into the wins.exe binary directory ('c:\usr\local\bin', by default) +// and 'c:\Windows' directories. Once the upgraded binary has been moved into place, it is invoked once again +// to confirm the file was copied correctly. +func updateBinaries(desiredVersion string) (bool, error) { + logrus.Info("Writing updated wins.exe to disk") + // write the embedded binary to disk + updatedBinaryPath := fmt.Sprintf("%s/wins-%s.exe", getWinsConfigDir(), strings.Trim(desiredVersion, "\n")) + err := os.WriteFile(updatedBinaryPath, winsBinary, os.ModePerm) + if err != nil { + return false, err + } + + // confirm that the new binary works and returns the version that we expect + err = confirmWinsBinaryVersion(desiredVersion, updatedBinaryPath) + if err != nil { + return false, fmt.Errorf("failed to stage updated binary: %w", err) + } + + logrus.Info("Stopping rancher-wins...") + rw, rwExists, err := service.OpenRancherWinsService() + if err != nil { + return false, fmt.Errorf("failed to open rancher-wins service while attempting to upgrade binary: %w", err) + } + + if rwExists { + // The service needs to be stopped before we can modify the binary it uses + err = rw.Stop() + if err != nil { + return false, fmt.Errorf("failed to stop rancher-wins service while attempting to upgrade binary: %w", err) + } + } + + logrus.Infof("Copying %s to %s", updatedBinaryPath, defaultWinsPath) + err = copyFile(updatedBinaryPath, defaultWinsPath) + if err != nil { + return false, fmt.Errorf("failed to copy new wins.exe binary to %s: %w", defaultWinsPath, err) + } + + // While the rancher-wins service looks for wins.exe in c:\Windows + // for consistency’s sake we should also ensure it's updated in c:\usr\local\bin + // as the install script places it there as well + usrLocalBinPath := getWinsUsrLocalBinBinary() + + logrus.Infof("Copying %s to %s", updatedBinaryPath, usrLocalBinPath) + err = copyFile(updatedBinaryPath, usrLocalBinPath) + if err != nil { + return false, fmt.Errorf("failed to copy new wins.exe binary to %s: %w", usrLocalBinPath, err) + } + + logrus.Infof("Validating updated binaries...") + err = confirmWinsBinaryVersion(desiredVersion, defaultWinsPath) + if err != nil { + return false, err + } + + err = confirmWinsBinaryVersion(desiredVersion, usrLocalBinPath) + if err != nil { + return false, err + } + + logrus.Infof("Removing %s", updatedBinaryPath) + err = os.Remove(updatedBinaryPath) + if err != nil { + return false, fmt.Errorf("failed to remove temporary wins.exe binary (%s): %w", updatedBinaryPath, err) + } + + logrus.Infof("Successfully upgraded wins.exe to version %s", desiredVersion) + return rwExists, nil +} diff --git a/suc/pkg/service/service.go b/suc/pkg/service/service.go index d3dbac00..4b56359c 100644 --- a/suc/pkg/service/service.go +++ b/suc/pkg/service/service.go @@ -13,7 +13,7 @@ import ( ) const ( - stateTransitionAttempts = 5 + stateTransitionAttempts = 12 stateTransitionDelayInSeconds = 5 ) @@ -98,7 +98,17 @@ func (s *Service) Restart() error { // Stop sends a svc.Stop control signal to the Service and waits // for it to enter the svc.Stopped state func (s *Service) Stop() error { - _, err := s.svc.Control(svc.Stop) + state, err := s.GetState() + if err != nil { + return fmt.Errorf("error getting state for %s service while attempting to send stop signal", s.Name) + } + + if state == windows.SERVICE_STOPPED { + logrus.Debugf("cannot stop service %s as it is not running", s.Name) + return nil + } + + _, err = s.svc.Control(svc.Stop) if err != nil { return fmt.Errorf("failed to send Stop signal to %s: %w", s.Name, err) } diff --git a/suc/pkg/service/state/state.go b/suc/pkg/state/state.go similarity index 97% rename from suc/pkg/service/state/state.go rename to suc/pkg/state/state.go index 5029f3f1..d38d94be 100644 --- a/suc/pkg/service/state/state.go +++ b/suc/pkg/state/state.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - winsConfig "github.com/rancher/wins/cmd/server/config" "github.com/rancher/wins/pkg/defaults" "github.com/rancher/wins/suc/pkg/service" @@ -125,5 +124,9 @@ func RestoreInitialState(state InitialState) error { errs = append(errs, err) } - return errors.Join(errs...) + if len(errs) > 0 { + return errors.Join(errs...) + } + + return service.RefreshWinsService() } diff --git a/tests/integration/install_test.ps1 b/tests/integration/install_test.ps1 index 74e68703..a0999197 100644 --- a/tests/integration/install_test.ps1 +++ b/tests/integration/install_test.ps1 @@ -35,7 +35,6 @@ Describe "install" { # note: since this script may not be run by an administrator, it's possible that it might fail # on trying to delete certain files with ACLs attached to them. # If you are running this locally, make sure you run with admin privileges. - # On CI, since we don't run as an admin today, this prevents automatic failure when the right ACLs are set. .\uninstall.ps1 } catch { Log-Warn "You need to manually run uninstall.ps1, encountered error: $($_.Exception.Message)" diff --git a/tests/integration/suc_test.ps1 b/tests/integration/suc_test.ps1 index 1b75f38c..548df4f6 100644 --- a/tests/integration/suc_test.ps1 +++ b/tests/integration/suc_test.ps1 @@ -29,12 +29,12 @@ Describe "SUC rancher-wins Config File Updater" { It "updates fields in config file" { $env:CATTLE_WINS_DEBUG="true" $env:STRICT_VERIFY="true" + $env:CATTLE_WINS_SKIP_BINARY_UPGRADE="true" Execute-Binary -FilePath "bin\wins-suc.exe" - - Log-Info "Command exited successfully: $?" # Ensure command executed successfully by looking at the most recent exit code $LASTEXITCODE | Should -Be -ExpectedValue 0 + Log-Info "Command exited successfully: $?" $out = $(Get-Content $env:CATTLE_AGENT_CONFIG_DIR/config | out-string) Log-Info $out @@ -61,10 +61,11 @@ Describe "SUC rancher-wins Service Configurator" { It "enables rancher-wins delayed start" { Log-Info "TEST: Enabling rancher-wins delayed start" $env:CATTLE_ENABLE_WINS_DELAYED_START = "true" + $env:CATTLE_WINS_SKIP_BINARY_UPGRADE = "true" Execute-Binary -FilePath "bin\wins-suc.exe" $LASTEXITCODE | Should -Be -ExpectedValue 0 - Log-Info "Command exited successfully: $($LASTEXITCODE -eq 0)" + Log-Info "Command exited successfully" Log-Info (sc.exe qc "rancher-wins" | Out-String) $winsStartType = (sc.exe qc "rancher-wins" | Select-String "START_TYPE" | ForEach-Object { ($_ -replace '\s+', ' ').trim().Split(" ") | Select-Object -Last 1 }) @@ -75,10 +76,11 @@ Describe "SUC rancher-wins Service Configurator" { It "disables rancher-wins delayed start" { Log-Info "TEST: Disabling rancher-wins delayed start" $env:CATTLE_ENABLE_WINS_DELAYED_START = "false" + $env:CATTLE_WINS_SKIP_BINARY_UPGRADE = "true" Execute-Binary -FilePath "bin\wins-suc.exe" $LASTEXITCODE | Should -Be -ExpectedValue 0 - Log-Info "Command exited successfully: $?" + Log-Info "Command exited successfully" Log-Info (sc.exe qc "rancher-wins" | Out-String) $winsStartType = (sc.exe qc "rancher-wins" | Select-String "START_TYPE" | ForEach-Object { ($_ -replace '\s+', ' ').trim().Split(" ") | Select-Object -Last 1 }) @@ -89,10 +91,11 @@ Describe "SUC rancher-wins Service Configurator" { It "enables rke2 service dependency" { Log-Info "TEST: Enabling rke2 service dependency" $env:CATTLE_ENABLE_WINS_SERVICE_DEPENDENCY = "true" + $env:CATTLE_WINS_SKIP_BINARY_UPGRADE = "true" Execute-Binary -FilePath "bin\wins-suc.exe" $LASTEXITCODE | Should -Be -ExpectedValue 0 - Log-Info "Command exited successfully: $($LASTEXITCODE -eq 0)" + Log-Info "Command exited successfully" Log-Info (sc.exe qc rke2 | Out-String) $dependencies = (Get-Service -Name rke2).ServicesDependedOn @@ -107,10 +110,11 @@ Describe "SUC rancher-wins Service Configurator" { # Dependency will be disabled whenever CATTLE_ENABLE_WINS_SERVICE_DEPENDENCY is # set to a value other than "true" (including not being set at all) $env:CATTLE_ENABLE_WINS_SERVICE_DEPENDENCY = "" + $env:CATTLE_WINS_SKIP_BINARY_UPGRADE = "true" Execute-Binary -FilePath "bin\wins-suc.exe" $LASTEXITCODE | Should -Be -ExpectedValue 0 - Log-Info "Command exited successfully: $($LASTEXITCODE -eq 0)" + Log-Info "Command exited successfully" Log-Info (sc.exe qc rke2 | Out-String) $dependencies = (Get-Service -Name rke2).ServicesDependedOn @@ -124,10 +128,11 @@ Describe "SUC rancher-wins Service Configurator" { It "Updates rancher-wins config file while rke2 dependency exists" { Log-Info "TEST: Enabling rke2 service dependency" $env:CATTLE_ENABLE_WINS_SERVICE_DEPENDENCY = "true" + $env:CATTLE_WINS_SKIP_BINARY_UPGRADE = "true" Execute-Binary -FilePath "bin\wins-suc.exe" $LASTEXITCODE | Should -Be -ExpectedValue 0 - Log-Info "Command exited successfully: $($LASTEXITCODE -eq 0)" + Log-Info "Command exited successfully" Log-Info (sc.exe qc rke2 | Out-String) Log-Info "Confirming rancher-wins service dependency has been added..." @@ -138,9 +143,8 @@ Describe "SUC rancher-wins Service Configurator" { Log-Info "Updating rancher-wins config file" $env:CATTLE_WINS_DEBUG="false" Execute-Binary -FilePath "bin\wins-suc.exe" - - Log-Info "Command exited successfully: $?" $LASTEXITCODE | Should -Be -ExpectedValue 0 + Log-Info "Command exited successfully" $out = $(Get-Content $env:CATTLE_AGENT_CONFIG_DIR/config | out-string) Log-Info $out diff --git a/tests/integration/suc_upgrade_test.ps1 b/tests/integration/suc_upgrade_test.ps1 new file mode 100644 index 00000000..a798d664 --- /dev/null +++ b/tests/integration/suc_upgrade_test.ps1 @@ -0,0 +1,88 @@ +$ErrorActionPreference = "Stop" + +Import-Module -Name @( + "$PSScriptRoot\utils.psm1" +) -WarningAction Ignore + +# clean interferences +try { + Get-Process -Name "rancher-wins-*" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore + Get-NetFirewallRule -PolicyStore ActiveStore -Name "rancher-wins-*" -ErrorAction Ignore | ForEach-Object { Remove-NetFirewallRule -Name $_.Name -PolicyStore ActiveStore -ErrorAction Ignore } | Out-Null + Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore +} +catch { + Log-Warn $_.Exception.Message +} + +Describe "install" { + BeforeEach { + Log-Info "Running install script" + # note: Simply running the install script does not do anything. During normal provisioning, + # Rancher will mutate the install script to both add environment variables, and to call + # the primary function 'Invoke-WinsInstaller'. As this is an integration test, we need to manually + # update the install script ourselves. + Add-Content -Path ./install.ps1 -Value '$env:CATTLE_REMOTE_ENABLED = "false"' + Add-Content -Path ./install.ps1 -Value '$env:CATTLE_LOCAL_ENABLED = "true"' + Add-Content -Path ./install.ps1 -Value Invoke-WinsInstaller + + .\install.ps1 + } + + AfterEach { + Log-Info "Running uninstall script" + try + { + # note: since this script may not be run by an administrator, it's possible that it might fail + # on trying to delete certain files with ACLs attached to them. + # If you are running this locally, make sure you run with admin privileges. + .\uninstall.ps1 + } + catch + { + Log-Warn "You need to manually run uninstall.ps1, encountered error: $( $_.Exception.Message )" + } + } + + It "Installs and upgrades" { + # We currently have the latest release installed, we now need to test upgrading to our version. + # Get the expected version of the new wins.exe binary. On PR's this + # will be a commit hash, and on tag runs it should be a full version (v0.x.y[-rc.z]). + $CIVersion = Get-LatestCommitOrTag + Log-Info "Incoming wins.exe CI version: $CIVersion" + + # Get the currently installed version string + $fullVersion = $(c:\Windows\wins.exe --version) + Log-Info "Current wins.exe version installed: $fullVersion" + $initialVersion = $fullVersion.Split(" ")[2] + $initialVersion -eq "" | Should -BeFalse + + # Run the suc image manually + Log-Info "Executing wins-suc.exe" + $env:CATTLE_WINS_SKIP_BINARY_UPGRADE = "false" + $env:CATTLE_WINS_DEBUG = "true" + Execute-Binary -FilePath "bin\wins-suc.exe" + $LASTEXITCODE | Should -Be -ExpectedValue 0 + + # Get the updated version string + $currentVersion = $(c:\Windows\wins.exe --version).Split(" ")[2] + Log-Info "wins.exe version after suc execution: $currentVersion" + $initialVersion -ne $currentVersion | Should -BeTrue + $currentVersion -eq $CIVersion | Should -BeTrue + + # Ensure that the updated file was moved + Test-Path "c:/etc/rancher/wins/wins-$currentVersion.exe" | Should -BeFalse + + Log-Info "Testing updated binaries..." + # Ensure that both paths were updated + $windowsDirVersion = $(c:\Windows\wins.exe --version).Split(" ")[2] + Log-Info "c:\Windows\wins.exe version: $windowsDirVersion" + $usrBinVersion = $(c:\usr\local\bin\wins.exe --version).Split(" ")[2] + Log-Info "c:\usr\local\bin\wins.exe version: $usrBinVersion" + + # Ensure that the version matches what we expect + $windowsDirVersion -eq $CIVersion | Should -BeTrue + $usrBinVersion -eq $CIVersion | Should -BeTrue + + Log-Info "Succesfully Tested Binary Upgrade" + } +} diff --git a/tests/integration/utils.psm1 b/tests/integration/utils.psm1 index b3aade1d..576904a7 100644 --- a/tests/integration/utils.psm1 +++ b/tests/integration/utils.psm1 @@ -340,6 +340,15 @@ function Ensure-DependencyExistsForService { return $false } +function Get-LatestCommitOrTag { + $currentTag = $(git tag -l --contains HEAD) + if ($null -ne $currentTag) { + return $currentTag + } + + return $(git rev-parse --short HEAD) +} + Export-ModuleMember -Function Log-Info Export-ModuleMember -Function Log-Warn Export-ModuleMember -Function Log-Error @@ -355,4 +364,5 @@ Export-ModuleMember -Function Add-RancherWinsService Export-ModuleMember -Function Remove-RancherWinsService Export-ModuleMember -Function Get-Permissions Export-ModuleMember -Function Test-Permissions -Export-ModuleMember -Function Ensure-DependencyExistsForService \ No newline at end of file +Export-ModuleMember -Function Ensure-DependencyExistsForService +Export-ModuleMember -Function Get-LatestCommitOrTag