Added tentacle updater.
This commit is contained in:
1 parent
f2cc47c8a0
commit
7591f65b58
17 files changed
+948
-22
No files matched your search
@@ -0,0 +1,330 @@
|
||||
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) })
|
||||
}
|
||||
Reference in new issue
Block a user