storj/cmd/storagenode-updater/cmd_test.go
Yaroslav Vorobiov 516b8cf2be storagenode-updater: add recovery for windows service restart
Reimplement windows service restart part using svc, add recovery
for failed service startup. Added restart-service cmd, to execute
self restart in a separate process.

Addressed issues:
https://storjlabs.atlassian.net/browse/SG-49
https://storjlabs.atlassian.net/browse/SG-136
https://storjlabs.atlassian.net/browse/SG-137

Change-Id: Ic51d9a99e8c1c10800c6c60ff4e218321c674fea
2020-05-01 09:07:05 +00:00

282 lines
7.5 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package main_test
import (
"archive/zip"
"compress/flate"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"storj.io/common/identity"
"storj.io/common/identity/testidentity"
"storj.io/common/storj"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/private/version"
"storj.io/storj/versioncontrol"
)
const (
oldVersion = "v0.19.0"
newVersion = "v0.19.5"
)
func TestAutoUpdater(t *testing.T) {
// TODO cleanup `.exe` extension for different OS
ctx := testcontext.New(t)
defer ctx.Cleanup()
oldSemVer, err := version.NewSemVer(oldVersion)
require.NoError(t, err)
newSemVer, err := version.NewSemVer(newVersion)
require.NoError(t, err)
oldInfo := version.Info{
Timestamp: time.Now(),
CommitHash: "",
Version: oldSemVer,
Release: false,
}
// build real bin with old version, will be used for both storagenode and updater
oldBin := CompileWithVersion(ctx, "storj.io/storj/cmd/storagenode-updater", oldInfo)
storagenodePath := ctx.File("fake", "storagenode.exe")
copyBin(ctx, t, oldBin, storagenodePath)
updaterPath := ctx.File("fake", "storagenode-updater.exe")
move(t, oldBin, updaterPath)
// build real storagenode and updater with new version
newInfo := version.Info{
Timestamp: time.Now(),
CommitHash: "",
Version: newSemVer,
Release: false,
}
newBin := CompileWithVersion(ctx, "storj.io/storj/cmd/storagenode-updater", newInfo)
updateBins := map[string]string{
"storagenode": newBin,
"storagenode-updater": newBin,
}
// run versioncontrol and update zips http servers
versionControlPeer, cleanupVersionControl := testVersionControlWithUpdates(ctx, t, updateBins)
defer cleanupVersionControl()
logPath := ctx.File("storagenode-updater.log")
// write identity files to disk for use in rollout calculation
identConfig := testIdentityFiles(ctx, t)
// run updater (update)
args := []string{"run",
"--config-dir", ctx.Dir(),
"--server-address", "http://" + versionControlPeer.Addr(),
"--binary-location", storagenodePath,
"--check-interval", "0s",
"--identity.cert-path", identConfig.CertPath,
"--identity.key-path", identConfig.KeyPath,
"--log", logPath,
}
// NB: updater currently uses `log.SetOutput` so all output after that call
// only goes to the log file.
out, err := exec.Command(updaterPath, args...).CombinedOutput()
logData, logErr := ioutil.ReadFile(logPath)
if assert.NoError(t, logErr) {
logStr := string(logData)
t.Log(logStr)
if !assert.Contains(t, logStr, `Service restarted successfully. {"Service": "storagenode"}`) {
t.Log(logStr)
}
if !assert.Contains(t, logStr, `Service restarted successfully. {"Service": "storagenode-updater"}`) {
t.Log(logStr)
}
} else {
t.Log(string(out))
}
if !assert.NoError(t, err) {
t.FailNow()
}
oldStoragenode := ctx.File("fake", "storagenode"+".old."+oldVersion+".exe")
oldStoragenodeInfo, err := os.Stat(oldStoragenode)
require.NoError(t, err)
require.NotNil(t, oldStoragenodeInfo)
require.NotZero(t, oldStoragenodeInfo.Size())
backupUpdater := ctx.File("fake", "storagenode-updater.old.exe")
backupUpdaterInfo, err := os.Stat(backupUpdater)
require.NoError(t, err)
require.NotNil(t, backupUpdaterInfo)
require.NotZero(t, backupUpdaterInfo.Size())
}
// CompileWithVersion compiles the specified package with the version variables set
// to the passed version info values and returns the executable name.
func CompileWithVersion(ctx *testcontext.Context, pkg string, info version.Info) string {
ldFlagsX := map[string]string{
"storj.io/private/version.buildTimestamp": strconv.Itoa(int(info.Timestamp.Unix())),
"storj.io/private/version.buildCommitHash": info.CommitHash,
"storj.io/private/version.buildVersion": info.Version.String(),
"storj.io/private/version.buildRelease": strconv.FormatBool(info.Release),
}
return ctx.CompileWithLDFlagsX(pkg, ldFlagsX)
}
func move(t *testing.T, src, dst string) {
err := os.Rename(src, dst)
require.NoError(t, err)
}
func copyBin(ctx *testcontext.Context, t *testing.T, src, dst string) {
s, err := os.Open(src)
require.NoError(t, err)
defer ctx.Check(s.Close)
d, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, 0755)
require.NoError(t, err)
defer ctx.Check(d.Close)
_, err = io.Copy(d, s)
require.NoError(t, err)
}
func testIdentityFiles(ctx *testcontext.Context, t *testing.T) identity.Config {
t.Helper()
ident, err := testidentity.PregeneratedIdentity(0, storj.LatestIDVersion())
require.NoError(t, err)
identConfig := identity.Config{
CertPath: ctx.File("identity", "identity.cert"),
KeyPath: ctx.File("identity", "identity.Key"),
}
err = identConfig.Save(ident)
require.NoError(t, err)
configData := fmt.Sprintf(
"identity.cert-path: %s\nidentity.key-path: %s",
identConfig.CertPath,
identConfig.KeyPath,
)
err = ioutil.WriteFile(ctx.File("config.yaml"), []byte(configData), 0644)
require.NoError(t, err)
return identConfig
}
func testVersionControlWithUpdates(ctx *testcontext.Context, t *testing.T, updateBins map[string]string) (peer *versioncontrol.Peer, cleanup func()) {
t.Helper()
var mux http.ServeMux
for name, src := range updateBins {
dst := ctx.File("updates", name+".zip")
zipBin(ctx, t, dst, src)
zipData, err := ioutil.ReadFile(dst)
require.NoError(t, err)
mux.HandleFunc("/"+name, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write(zipData)
require.NoError(t, err)
}))
}
ts := httptest.NewServer(&mux)
var randSeed version.RolloutBytes
testrand.Read(randSeed[:])
storagenodeSeed := fmt.Sprintf("%x", randSeed)
testrand.Read(randSeed[:])
updaterSeed := fmt.Sprintf("%x", randSeed)
config := &versioncontrol.Config{
Address: "127.0.0.1:0",
// NB: this config field is required for versioncontrol to run.
Versions: versioncontrol.OldVersionConfig{
Satellite: "v0.0.1",
Storagenode: "v0.0.1",
Uplink: "v0.0.1",
Gateway: "v0.0.1",
Identity: "v0.0.1",
},
Binary: versioncontrol.ProcessesConfig{
Storagenode: versioncontrol.ProcessConfig{
Suggested: versioncontrol.VersionConfig{
Version: newVersion,
URL: ts.URL + "/storagenode",
},
Rollout: versioncontrol.RolloutConfig{
Seed: storagenodeSeed,
Cursor: 100,
},
},
StoragenodeUpdater: versioncontrol.ProcessConfig{
Suggested: versioncontrol.VersionConfig{
Version: newVersion,
URL: ts.URL + "/storagenode-updater",
},
Rollout: versioncontrol.RolloutConfig{
Seed: updaterSeed,
Cursor: 100,
},
},
},
}
peer, err := versioncontrol.New(zaptest.NewLogger(t), config)
require.NoError(t, err)
ctx.Go(func() error {
return peer.Run(ctx)
})
return peer, func() {
ts.Close()
ctx.Check(peer.Close)
}
}
func zipBin(ctx *testcontext.Context, t *testing.T, dst, src string) {
t.Helper()
zipFile, err := os.Create(dst)
require.NoError(t, err)
defer ctx.Check(zipFile.Close)
base := filepath.Base(dst)
base = base[:len(base)-len(".zip")]
writer := zip.NewWriter(zipFile)
defer ctx.Check(writer.Close)
writer.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
return flate.NewWriter(out, flate.NoCompression)
})
contents, err := writer.Create(base)
require.NoError(t, err)
data, err := ioutil.ReadFile(src)
require.NoError(t, err)
_, err = contents.Write(data)
require.NoError(t, err)
}