-
Notifications
You must be signed in to change notification settings - Fork 387
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
base: main
Are you sure you want to change the base?
Changes from all commits
69480a7
b26b776
b369b57
523fac1
2e98b40
9b3a649
a7883c1
235b453
6436269
812756c
9df12b3
1331c5d
90292e2
3bc54ec
4c52c40
79afb92
f053602
d0c157e
2263a15
ae34ded
b2c9bae
f9518e2
7b42d0c
60c9e4f
ca7258e
2fec37c
d3104a7
51c497b
09f5aef
839935e
515769a
6972204
e16e702
b8d77fe
5379fd2
f155b3f
1a9fc9e
40cafd6
3f72551
88bba79
d0d8ef5
a4e1aa8
7f65930
1d7495f
a45c25f
958c5a6
4d7a70f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,344 @@ | ||
// Copyright (C) 2019 Storj Labs, Inc. | ||
// See LICENSE for copying information. | ||
|
||
// +build windows | ||
// +build ignore | ||
|
||
package main | ||
|
||
import ( | ||
"archive/zip" | ||
"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) | ||
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) { | ||
t.Skipf("upgrade test requires binaries to have greater version than \"old release\"") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure whats the issue here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Upgrade fails if the "new" msi is for an older version. Currently, unless HEAD is tagged with a version number, it's version is 0.0.0 which causes the upgrade test to fail when it uses a release msi for the "old" installation. |
||
|
||
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 := append(msiBaseArgs, "STORJ_MIN_STORAGE=1GB") | ||
if installDir != "" { | ||
baseArgs = append(baseArgs, "INSTALLFOLDER="+installDir) | ||
} | ||
args = append(append([]string{ | ||
"/i", msiPath, | ||
"/log", logPath, | ||
}, baseArgs...), args...) | ||
|
||
// TODO: fix ugly error message (maybe caused by msiexe's `/qn` flag?) | ||
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) (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 releaseUrl(version, name, ext string) string { | ||
urlTemplate := "https://github.com/storj/storj/releases/download/{version}/{service}_{os}_{arch}{ext}.zip" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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.