Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

installer/windows: installer automated testing #3558

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
69480a7
add installer_test and fix typo in Product.wxs
bryanchriswhite Nov 12, 2019
b26b776
wip2
bryanchriswhite Nov 13, 2019
b369b57
add todo
bryanchriswhite Nov 13, 2019
523fac1
download binaries instead of building
bryanchriswhite Nov 15, 2019
2e98b40
wip3
bryanchriswhite Nov 15, 2019
9b3a649
add todos and cleanup
bryanchriswhite Nov 15, 2019
a7883c1
testing
bryanchriswhite Nov 15, 2019
235b453
more testing
bryanchriswhite Nov 20, 2019
6436269
wip
bryanchriswhite Nov 20, 2019
812756c
add build ignore
bryanchriswhite Nov 25, 2019
9df12b3
add missing import
bryanchriswhite Nov 25, 2019
1331c5d
wip config test
bryanchriswhite Nov 25, 2019
90292e2
fix path after move
bryanchriswhite Nov 25, 2019
3bc54ec
fix installerDir var
bryanchriswhite Nov 25, 2019
4c52c40
comment before uninstall
bryanchriswhite Nov 25, 2019
79afb92
fix assertions
bryanchriswhite Nov 25, 2019
f053602
add server address assertion
bryanchriswhite Nov 25, 2019
d0c157e
cleanup
bryanchriswhite Nov 25, 2019
2263a15
Merge remote-tracking branch 'storj/master' into bryan/installer-testing
bryanchriswhite Nov 25, 2019
ae34ded
uninstall tweaks
bryanchriswhite Nov 25, 2019
b2c9bae
add identitydir property
bryanchriswhite Nov 25, 2019
f9518e2
fix uninstall
bryanchriswhite Nov 25, 2019
7b42d0c
comment identity file
bryanchriswhite Nov 25, 2019
60c9e4f
close open file
bryanchriswhite Nov 25, 2019
ca7258e
test inprovements
bryanchriswhite Nov 25, 2019
2fec37c
wip testing
bryanchriswhite Nov 25, 2019
d3104a7
change version
bryanchriswhite Nov 25, 2019
51c497b
download installer
bryanchriswhite Nov 25, 2019
09f5aef
wait for services to stop
bryanchriswhite Nov 25, 2019
839935e
revert build.bat
bryanchriswhite Nov 25, 2019
515769a
use pre-downloaded/unpacked msi
bryanchriswhite Nov 25, 2019
6972204
use quiet msiexec
bryanchriswhite Nov 25, 2019
e16e702
cleanup
bryanchriswhite Nov 25, 2019
b8d77fe
refactor and wip upgrade testing
bryanchriswhite Nov 26, 2019
5379fd2
wip
bryanchriswhite Nov 26, 2019
f155b3f
fix upgrade test and cleanup
bryanchriswhite Nov 27, 2019
1a9fc9e
review improvements
bryanchriswhite Nov 27, 2019
40cafd6
add start/stop service test and refactor
bryanchriswhite Nov 27, 2019
3f72551
review improvements
bryanchriswhite Nov 27, 2019
88bba79
remove unused func
bryanchriswhite Nov 27, 2019
d0d8ef5
remove unnecesary flag check
bryanchriswhite Dec 2, 2019
a4e1aa8
remove unused variadic arg
bryanchriswhite Dec 2, 2019
7f65930
fix comment
bryanchriswhite Dec 3, 2019
1d7495f
add todo
bryanchriswhite Dec 3, 2019
a45c25f
add min storage property
bryanchriswhite Dec 4, 2019
958c5a6
use min storage prop
bryanchriswhite Dec 4, 2019
4d7a70f
skip upgrade
bryanchriswhite Dec 4, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion installer/windows/Product.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
<InstallExecuteSequence>
<Custom Action='StoragenodeSetup' Before='InstallServices'>NOT Installed AND NOT WIX_UPGRADE_DETECTED</Custom>
<Custom Action='DeleteConfigFile' After='RemoveFiles'>(REMOVE="ALL") AND NOT WIX_UPGRADE_DETECTED</Custom>
<!-- legacy: save config file as old versions of the installer will remove it -->
<!-- legacy: save config file as old versions or the installer will remove it -->
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
<Custom Action='ExtractInstallDir' Before='SetInstallFolder'>WIX_UPGRADE_DETECTED</Custom>
<Custom Action='BackupConfigFile' Before='InstallExecute'>WIX_UPGRADE_DETECTED</Custom>
<Custom Action='RestoreConfigFile' After='RemoveExistingProducts'>WIX_UPGRADE_DETECTED</Custom>
Expand Down
373 changes: 373 additions & 0 deletions scripts/installer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.

// +build windows
// +build ignore

package main

import (
"archive/zip"
"bufio"
"bytes"
"context"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zeebo/errs"
"golang.org/x/sync/errgroup"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"

"storj.io/storj/private/testcontext"
)

const (
// TODO: make this more dynamic and/or use versioncontrol server?
// (NB: can't use versioncontrol server until updater process is added to response)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exists now and could be used.

oldRelease = "v0.26.2"

testWalletAddr = "0x0000000000000000000000000000000000000000"
testEmail = "user@mail.test"
testPublicAddr = "127.0.0.1:10000"
)

var (
releaseMSIPath string

msiBaseArgs = []string{
"/quiet", "/qn",
"/norestart",
}
serviceNames = []string{"storagenode", "storagenode-updater"}

msiPathFlag = flag.String("msi", "", "path to the msi to use")
)

func TestMain(m *testing.M) {
flag.Parse()
if *msiPathFlag == "" {
log.Fatal("no msi passed, use `go test ... -args -msi \"<msi path (using \\ seperator)>\"`")
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
}

tmp, err := ioutil.TempDir("", "release")
kaloyan-raev marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
panic(err)
}

releaseMSIPath = filepath.Join(tmp, oldRelease+".msi")
if err = downloadInstaller(oldRelease, releaseMSIPath); err != nil {
panic(err)
}
defer func() {
err = errs.Combine(os.RemoveAll(tmp))
}()

viper.SetConfigType("yaml")

os.Exit(m.Run())
}

func TestService_StopStart(t *testing.T) {
ctx := testcontext.NewWithTimeout(t, 30*time.Second)
defer ctx.Cleanup()

installDir := ctx.Dir("install")
testInstall(t, ctx, *msiPathFlag, installDir)
defer requireUninstall(t, ctx)

// NB: wait for install to complete / services to be running
noop := func(*mgr.Service) error { return nil }
err := controlServices(ctx, svc.Running, noop)
require.NoError(t, err)

require.NoError(t, stopServices(ctx))
require.NoError(t, startServices(ctx))
}

func TestInstaller_Config(t *testing.T) {
ctx := testcontext.NewWithTimeout(t, 30*time.Second)
defer ctx.Cleanup()

installDir := ctx.Dir("install")
testInstall(t, ctx, *msiPathFlag, installDir)
defer requireUninstall(t, ctx)

configFile, err := os.Open(ctx.File("install", "config.yaml"))
require.NoError(t, err)
defer ctx.Check(configFile.Close)

err = viper.ReadConfig(configFile)
require.NoError(t, err)

require.Equal(t, testEmail, viper.GetString("operator.email"))
require.Equal(t, testWalletAddr, viper.GetString("operator.wallet"))
require.Equal(t, testPublicAddr, viper.GetString("contact.external-address"))
}

func TestUpgrade_Config(t *testing.T) {
if *msiPathFlag == "" {
t.Fatal("no msi passed, use `go test ... -args -msi <msi path>`")
}

ctx := testcontext.New(t)
defer ctx.Cleanup()

installDir := ctx.Dir("install")
testInstall(t, ctx, releaseMSIPath, installDir)

// upgrade using test msi
install(t, ctx, *msiPathFlag, "")
defer requireUninstall(t, ctx)

configFile, err := os.Open(ctx.File("install", "config.yaml"))
require.NoError(t, err)
defer ctx.Check(configFile.Close)

err = viper.ReadConfig(configFile)
require.NoError(t, err)

require.Equal(t, testEmail, viper.GetString("operator.email"))
require.Equal(t, testWalletAddr, viper.GetString("operator.wallet"))
require.Equal(t, testPublicAddr, viper.GetString("contact.external-address"))
}

func testInstall(t *testing.T, ctx *testcontext.Context, msiPath, installDir string) {
args := []string{
fmt.Sprintf("STORJ_WALLET=%s", testWalletAddr),
fmt.Sprintf("STORJ_EMAIL=%s", testEmail),
fmt.Sprintf("STORJ_PUBLIC_ADDRESS=%s", testPublicAddr),
}
install(t, ctx, msiPath, installDir, args...)
}

func install(t *testing.T, ctx *testcontext.Context, msiPath, installDir string, args ...string) {
t.Logf("installing from %s\n", msiPath)
logPath := ctx.File("log", "install.log")

baseArgs := msiBaseArgs
if installDir != "" {
baseArgs = append(baseArgs, "INSTALLFOLDER="+installDir)
}
args = append(append([]string{
"/i", msiPath,
"/log", logPath,
}, baseArgs...), args...)

installOut, err := exec.Command("msiexec", args...).CombinedOutput()
if !assert.NoError(t, err) {
installLogData, err := ioutil.ReadFile(logPath)
if assert.NoError(t, err) {
t.Logf("MSIExec log:\n============================\n%s", string(installLogData))
}
t.Logf("MSIExec output:\n============================\n%s", string(installOut))
t.Fatal()
}
}

func requireUninstall(t *testing.T, ctx *testcontext.Context) {
logPath := ctx.File("log", "uninstall.log")
uninstallOut, err := uninstall(ctx, logPath)
if !assert.NoError(t, err) {
uninstallLogData, err := ioutil.ReadFile(logPath)
if !assert.NoError(t, err) {
t.Logf("MSIExec log:\n============================\n%s\n", string(uninstallLogData))
}
t.Fatalf("MSIExec output:\n============================\n%s", string(uninstallOut))
}
}

func uninstall(ctx context.Context, logPath string) ([]byte, error) {
if err := stopServices(ctx); err != nil {
return []byte{}, err
}

args := append([]string{"/uninstall", *msiPathFlag, "/log", logPath}, msiBaseArgs...)
return exec.Command("msiexec", args...).CombinedOutput()
}

func startServices(ctx context.Context, names ...string) (err error) {
return controlServices(ctx, svc.Running, func(service *mgr.Service) error {
return service.Start()
})
}

func stopServices(ctx context.Context) (err error) {
return controlServices(ctx, svc.Stopped, func(service *mgr.Service) error {
_, err := service.Control(svc.Stop)
return err
})
}

func controlServices(ctx context.Context, waitState svc.State, controlF func(*mgr.Service) error) error {
serviceMgr, err := mgr.Connect()
if err != nil {
return err
}

group := new(errgroup.Group)
for _, name := range serviceNames {
name := name
group.Go(func() (err error) {
service, err := serviceMgr.OpenService(name)
if err != nil {
return err
}
defer func() {
err = errs.Combine(err, service.Close())
}()

if err := controlF(service); err != nil {
return err
}
return waitForState(ctx, service, waitState)
})
}

return group.Wait()
}

func waitForState(ctx context.Context, service *mgr.Service, state svc.State) error {
for {
status, err := service.Query()
if err != nil {
return err
}

if err := ctx.Err(); err != nil {
return err
}

switch status.State {
case state:
return nil
default:
time.Sleep(500 * time.Millisecond)
}
}
}

func readConfigLines(t *testing.T, ctx *testcontext.Context, configPath string) string {
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
configFile, err := os.Open(configPath)
require.NoError(t, err)
defer ctx.Check(configFile.Close)

// NB: strip empty lines and comments
configBuf := new(bytes.Buffer)
scanner := bufio.NewScanner(configFile)
for scanner.Scan() {
line := scanner.Text()
line = strings.Trim(line, " \t\n")
out := append(scanner.Bytes(), byte('\n'))
if len(line) == 0 {
continue
}
if !strings.HasPrefix(line, "#") {
_, err := configBuf.Write(out)
require.NoError(t, err)
}
}
if err := scanner.Err(); err != nil {
require.NoError(t, err)
}
return configBuf.String()
}

func releaseUrl(version, name, ext string) string {
urlTemplate := "https://github.com/storj/storj/releases/download/{version}/{service}_{os}_{arch}{ext}.zip"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should rely on an external source like github for testing but I'm not sure how to handle this in different way right now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an integration test. So I am not negative to using the production download site.


url := strings.Replace(urlTemplate, "{version}", version, 1)
url = strings.Replace(url, "{service}", name, 1)
url = strings.Replace(url, "{os}", runtime.GOOS, 1)
url = strings.Replace(url, "{arch}", runtime.GOARCH, 1)
url = strings.Replace(url, "{ext}", ext, 1)
return url
}

func downloadInstaller(version, dst string) error {
zipDir, err := ioutil.TempDir("", "archive")
if err != nil {
return err
}
zipPath := filepath.Join(zipDir, version+".msi.zip")

url := releaseUrl(version, "storagenode", ".msi")

if err := downloadArchive(url, zipPath); err != nil {
return err
}
if err := unpackBinary(zipPath, releaseMSIPath); err != nil {
return err
}
return nil
}

func downloadArchive(url, dst string) (err error) {
resp, err := http.Get(url)
defer func() {
err = errs.Combine(err, resp.Body.Close())
}()

if resp.StatusCode != http.StatusOK {
return errs.New("error downloading %s; non-success status: %s", url, resp.Status)
}

dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0755)
if err != nil {
return err
}
defer func() {
err = errs.Combine(err, dstFile.Close())
}()

if _, err = io.Copy(dstFile, resp.Body); err != nil {
return err
}
return nil
}

func unpackBinary(archive, dst string) (err error) {
zipReader, err := zip.OpenReader(archive)
if err != nil {
return err
}
defer func() {
err = errs.Combine(err, zipReader.Close())
}()

zipedExec, err := zipReader.File[0].Open()
if err != nil {
return err
}
defer func() {
err = errs.Combine(err, zipedExec.Close())
}()

newExec, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0755)
if err != nil {
return err
}
defer func() {
err = errs.Combine(err, newExec.Close())
}()

_, err = io.Copy(newExec, zipedExec)
if err != nil {
return err
}
return nil
}