331 lines
9.4 KiB
Go
331 lines
9.4 KiB
Go
package app
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/tentacle"
|
|
)
|
|
|
|
func prepare_upgrade_fixture(t *testing.T, mode string) (*tentacleRuntime, *tentacleBindingRecorder, tentacle.Repository, tentacle.Entry) {
|
|
t.Helper()
|
|
repository, data := install_fixture(t)
|
|
|
|
set_install_transport(t, func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: 200,
|
|
ContentLength: int64(len(data)),
|
|
Body: io.NopCloser(bytes.NewReader(data)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})
|
|
|
|
registryPath := filepath.Join(t.TempDir(), "tentacles.toml")
|
|
owner, bindings := managed_tentacle_fixture(t, registryPath, false)
|
|
|
|
owner.fetchRepository = func(context.Context, string) (tentacle.Repository, error) {
|
|
return repository, nil
|
|
}
|
|
|
|
if err := owner.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := owner.Manage(context.Background(), tentacle.ManagementRequest{
|
|
Action: "repository-add", URL: repository.URL,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := owner.Manage(context.Background(), tentacle.ManagementRequest{
|
|
Action: "install-enable", ID: "echo", URL: repository.URL,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
id := tentacle.PackageID(repository, "echo")
|
|
wait_managed_state(t, owner, id, "active")
|
|
before := owner.Snapshot().Entries[0].Entry
|
|
|
|
// Rebuild the downloaded archive with a matching version 0.2 manifest.
|
|
var updated bytes.Buffer
|
|
gzipWriter, err := gzip.NewWriterLevel(&updated, gzip.BestSpeed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tarWriter := tar.NewWriter(gzipWriter)
|
|
tarReader, err := gzip.NewReader(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reader := tar.NewReader(tarReader)
|
|
for {
|
|
header, err := reader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if header.Name == "tentacle.toml" {
|
|
content = bytes.Replace(content, []byte("version='0.1'"), []byte("version='0.2'"), 1)
|
|
if mode == "stall" {
|
|
content = bytes.Replace(content, []byte(before.Program[len(before.Program)-1]), []byte(filepath.Join(t.TempDir(), "stall")), 1)
|
|
}
|
|
}
|
|
if header.Name == "bin/echo" && mode == "fail" {
|
|
content, err = os.ReadFile("/bin/false")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
copyHeader := *header
|
|
copyHeader.Size = int64(len(content))
|
|
if err := tarWriter.WriteHeader(©Header); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tarWriter.Write(content); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := tarWriter.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := gzipWriter.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tarReader.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
updatedData := updated.Bytes()
|
|
digest := sha256.Sum256(updatedData)
|
|
repository.Revision = strings.Repeat("b", 40)
|
|
repository.Tentacles[0].Version = "0.2"
|
|
repository.Tentacles[0].Releases = []tentacle.Release{{
|
|
Platform: "linux",
|
|
Architecture: runtime.GOARCH,
|
|
URL: "https://example.org/release-0.2.tar.gz",
|
|
SHA256: fmt.Sprintf("%x", digest),
|
|
}}
|
|
data = updatedData
|
|
|
|
if err := owner.Manage(context.Background(), tentacle.ManagementRequest{
|
|
Action: "repository-refresh", URL: repository.URL,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return owner, bindings, repository, before
|
|
}
|
|
|
|
func TestUpgradeReplacesRunningPackageAndCleansOldFiles(t *testing.T) {
|
|
owner, _, repository, before := prepare_upgrade_fixture(t, "called")
|
|
id := before.ID
|
|
|
|
if err := owner.Manage(context.Background(), tentacle.ManagementRequest{
|
|
Action: "upgrade", ID: "echo", URL: repository.URL,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wait_managed_state(t, owner, id, "active")
|
|
after := owner.Snapshot().Entries[0].Entry
|
|
|
|
if !after.Enabled {
|
|
t.Fatal("upgrade disabled previously enabled tentacle")
|
|
}
|
|
if after.PackageDirectory == before.PackageDirectory {
|
|
t.Fatal("upgrade reused old package directory")
|
|
}
|
|
|
|
waitTentacle(t, func() bool {
|
|
_, err := os.Stat(before.PackageDirectory)
|
|
return os.IsNotExist(err)
|
|
})
|
|
|
|
if _, err := os.Stat(after.PackageDirectory); err != nil {
|
|
t.Fatalf("replacement package missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpgradeFailureAndCancellationRestorePreviousPackage(t *testing.T) {
|
|
for _, operation := range []string{"fail", "cancel", "disable", "remove", "shutdown"} {
|
|
t.Run(operation, func(t *testing.T) {
|
|
mode := "stall"
|
|
if operation == "fail" {
|
|
mode = "fail"
|
|
}
|
|
owner, bindings, repository, before := prepare_upgrade_fixture(t, mode)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
result := make(chan error, 1)
|
|
go func() {
|
|
result <- owner.Manage(ctx, tentacle.ManagementRequest{Action: "upgrade", ID: "echo", URL: repository.URL})
|
|
}()
|
|
if operation != "fail" {
|
|
waitTentacle(t, func() bool {
|
|
snapshot := owner.Snapshot()
|
|
return len(snapshot.Entries) == 1 && snapshot.Entries[0].Entry.Version == "0.2" && snapshot.Entries[0].State == "starting"
|
|
})
|
|
if _, err := os.Stat(before.PackageDirectory); err != nil {
|
|
t.Fatalf("old package lost during startup: %v", err)
|
|
}
|
|
switch operation {
|
|
case "cancel":
|
|
cancel()
|
|
case "disable", "remove":
|
|
manage_tentacle(t, owner, operation, before.ID, nil)
|
|
case "shutdown":
|
|
if err := owner.Stop(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
select {
|
|
case err := <-result:
|
|
if err == nil {
|
|
t.Fatal("failed or cancelled update reported success")
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("update did not finish")
|
|
}
|
|
if operation == "fail" || operation == "cancel" {
|
|
wait_managed_state(t, owner, before.ID, "active")
|
|
owner.Invoke(context.Background(), bindings.TentacleBindings()[0].Command)
|
|
waitTentacle(t, func() bool { _, err := os.Stat(before.Program[len(before.Program)-1]); return err == nil })
|
|
}
|
|
if operation == "disable" {
|
|
wait_managed_state(t, owner, before.ID, "disabled")
|
|
}
|
|
if operation == "shutdown" {
|
|
restarted, _ := managed_tentacle_fixture(t, owner.registry.Path, false)
|
|
if err := restarted.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_managed_state(t, restarted, before.ID, "active")
|
|
}
|
|
disk, err := (&tentacle.Registry{Path: owner.registry.Path}).Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if operation == "remove" {
|
|
if len(disk) != 0 {
|
|
t.Fatal(disk)
|
|
}
|
|
} else {
|
|
expected := before
|
|
if operation == "disable" {
|
|
expected.Enabled = false
|
|
}
|
|
if len(disk) != 1 || !reflect.DeepEqual(disk[0], expected) {
|
|
t.Fatalf("previous installation not restored: %+v", disk)
|
|
}
|
|
if _, err := os.Stat(before.PackageDirectory); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
waitTentacle(t, func() bool {
|
|
packages, err := filepath.Glob(filepath.Join(tentacle.PackageRoot(owner.registry.Path), "package-*"))
|
|
expected := 1
|
|
if operation == "remove" {
|
|
expected = 0
|
|
}
|
|
return err == nil && len(packages) == expected
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpgradePreparationCancellationLeavesRunningPackage(t *testing.T) {
|
|
owner, _, repository, before := prepare_upgrade_fixture(t, "called")
|
|
preparing := make(chan struct{})
|
|
owner.preparePackage = func(ctx context.Context, _ string, _ tentacle.Repository, _ string) (*tentacle.PreparedPackage, error) {
|
|
close(preparing)
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
result := make(chan error, 1)
|
|
go func() {
|
|
result <- owner.Manage(ctx, tentacle.ManagementRequest{Action: "upgrade", ID: "echo", URL: repository.URL})
|
|
}()
|
|
select {
|
|
case <-preparing:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("preparation did not start")
|
|
}
|
|
cancel()
|
|
select {
|
|
case err := <-result:
|
|
if err == nil {
|
|
t.Fatal("cancelled preparation succeeded")
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("preparation did not cancel")
|
|
}
|
|
status := owner.Snapshot().Entries[0]
|
|
if status.State != "active" || !reflect.DeepEqual(status.Entry, before) {
|
|
t.Fatalf("cancelled preparation changed running package: %+v", status)
|
|
}
|
|
}
|
|
|
|
func TestUpgradeUsesEnablementAtCommit(t *testing.T) {
|
|
owner, _, repository, before := prepare_upgrade_fixture(t, "called")
|
|
preparing, release := make(chan struct{}), make(chan struct{})
|
|
owner.preparePackage = func(ctx context.Context, root string, repository tentacle.Repository, id string) (*tentacle.PreparedPackage, error) {
|
|
close(preparing)
|
|
select {
|
|
case <-release:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
return tentacle.PreparePackage(ctx, root, repository, id)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
result := make(chan error, 1)
|
|
go func() {
|
|
result <- owner.Manage(ctx, tentacle.ManagementRequest{Action: "upgrade", ID: "echo", URL: repository.URL})
|
|
}()
|
|
select {
|
|
case <-preparing:
|
|
case <-ctx.Done():
|
|
t.Fatal("preparation did not start")
|
|
}
|
|
manage_tentacle(t, owner, "disable", before.ID, nil)
|
|
close(release)
|
|
select {
|
|
case err := <-result:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("upgrade did not finish")
|
|
}
|
|
wait_managed_state(t, owner, before.ID, "disabled")
|
|
status := owner.Snapshot().Entries[0]
|
|
if status.Entry.Version != "0.2" || status.Entry.Enabled {
|
|
t.Fatal(status)
|
|
}
|
|
waitTentacle(t, func() bool { _, err := os.Stat(before.PackageDirectory); return os.IsNotExist(err) })
|
|
}
|