Added tentacle updater.
This commit is contained in:
1 parent
f2cc47c8a0
commit
7591f65b58
17 files changed
+948
-22
No files matched your search
@@ -34,7 +34,7 @@ func (worker *preferencesWorker) manage_tentacle(ctx context.Context, request pr
|
||||
return response
|
||||
}
|
||||
timeout := 6 * time.Second
|
||||
if strings.HasPrefix(request.Tentacle.Action, "repository-") || strings.HasPrefix(request.Tentacle.Action, "install") {
|
||||
if strings.HasPrefix(request.Tentacle.Action, "repository-") || strings.HasPrefix(request.Tentacle.Action, "install") || request.Tentacle.Action == "upgrade" {
|
||||
timeout = 50 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
|
||||
@@ -43,7 +43,12 @@ func install_fixture(t *testing.T) (tentacle.Repository, []byte) {
|
||||
marker := filepath.Join(t.TempDir(), "called")
|
||||
manifest := fmt.Sprintf("format-version=1\nid='echo'\nname='Echo'\nversion='0.1'\nruntime='native'\nplatforms=['linux']\nentry-point=['bin/echo','-test.run=^TestTentacleReaderPeer$','--','reader-peer',%q]\napis=['commands']\ncapabilities=[]\n[protocol]\nmajor=1\nminor=0\n", marker)
|
||||
var data bytes.Buffer
|
||||
compressed := gzip.NewWriter(&data)
|
||||
// These fixtures test package installation, not compression ratios. This
|
||||
// also keeps repeated update fixtures cheap under the race detector.
|
||||
compressed, err := gzip.NewWriterLevel(&data, gzip.BestSpeed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archive := tar.NewWriter(compressed)
|
||||
for _, item := range []struct {
|
||||
name string
|
||||
|
||||
@@ -30,6 +30,13 @@ type tentacleStartupCall struct {
|
||||
call tentacleManagementCall
|
||||
saveError error
|
||||
stop func() bool
|
||||
previous *tentacle.Entry
|
||||
}
|
||||
|
||||
// A retired package stays on disk until the session using it has joined.
|
||||
type tentacleRetiredPackage struct {
|
||||
entry tentacle.Entry
|
||||
session *tentacle.Session
|
||||
}
|
||||
|
||||
func (runtime *tentacleRuntime) Snapshot() tentacle.Snapshot {
|
||||
@@ -43,6 +50,7 @@ func (runtime *tentacleRuntime) Snapshot() tentacle.Snapshot {
|
||||
defer runtime.mutex.Unlock()
|
||||
result := runtime.snapshot
|
||||
result.Repositories = tentacle.CloneRepositories(result.Repositories)
|
||||
result.Updates = slices.Clone(result.Updates)
|
||||
result.Entries = append([]tentacle.EntryStatus(nil), result.Entries...)
|
||||
var bindings []input.BindingOverride
|
||||
if runtime.input != nil && !runtime.disabled {
|
||||
@@ -124,7 +132,7 @@ func (runtime *tentacleRuntime) Manage(ctx context.Context, request tentacle.Man
|
||||
if disabled {
|
||||
return tentacle.ErrDisabled
|
||||
}
|
||||
if request.Action == "install" || request.Action == "install-enable" {
|
||||
if request.Action == "install" || request.Action == "install-enable" || request.Action == "upgrade" {
|
||||
if request.Entry != nil {
|
||||
return errors.New("unexpected tentacle entry in installation request")
|
||||
}
|
||||
@@ -138,9 +146,37 @@ func (runtime *tentacleRuntime) Manage(ctx context.Context, request tentacle.Man
|
||||
}
|
||||
repository := snapshot.Repositories[index]
|
||||
id := tentacle.PackageID(repository, request.ID)
|
||||
if slices.ContainsFunc(snapshot.Entries, func(value tentacle.EntryStatus) bool { return value.Entry.ID == id }) {
|
||||
installed := slices.ContainsFunc(snapshot.Entries, func(value tentacle.EntryStatus) bool { return value.Entry.ID == id })
|
||||
if request.Action == "upgrade" && !installed {
|
||||
return errors.New("tentacle is not installed")
|
||||
}
|
||||
if request.Action != "upgrade" && installed {
|
||||
return errors.New("tentacle is already installed")
|
||||
}
|
||||
if request.Action == "upgrade" {
|
||||
for _, status := range snapshot.Entries {
|
||||
if status.Entry.ID != id {
|
||||
continue
|
||||
}
|
||||
for _, manifest := range repository.Tentacles {
|
||||
if manifest.ID != request.ID {
|
||||
continue
|
||||
}
|
||||
if manifest.Version == status.Entry.Version {
|
||||
return errors.New("tentacle is already at the repository version")
|
||||
}
|
||||
newer, err := tentacle.IsNewerVersion(manifest.Version, status.Entry.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot compare tentacle versions: %w", err)
|
||||
}
|
||||
if !newer {
|
||||
return errors.New("repository tentacle version is not newer than installed version")
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
runtime.mutex.Lock()
|
||||
if runtime.repositoryBusy {
|
||||
runtime.mutex.Unlock()
|
||||
@@ -162,9 +198,15 @@ func (runtime *tentacleRuntime) Manage(ctx context.Context, request tentacle.Man
|
||||
return err
|
||||
}
|
||||
defer prepared.Discard()
|
||||
prepared.Entry.Enabled = request.Action == "install-enable"
|
||||
operation := "add"
|
||||
if request.Action == "upgrade" {
|
||||
operation = "update"
|
||||
// The lifecycle owner preserves enablement at commit time.
|
||||
} else {
|
||||
prepared.Entry.Enabled = request.Action == "install-enable"
|
||||
}
|
||||
call.packageFiles, call.repository = prepared, &repository
|
||||
call.request = tentacle.ManagementRequest{Action: "add", ID: prepared.Entry.ID, Entry: &prepared.Entry}
|
||||
call.request = tentacle.ManagementRequest{Action: operation, ID: prepared.Entry.ID, Entry: &prepared.Entry}
|
||||
}
|
||||
if request.Action == "repository-add" || request.Action == "repository-refresh" {
|
||||
if request.Entry != nil || request.ID != "" {
|
||||
@@ -240,6 +282,7 @@ func change_tentacle_entries(entries []tentacle.Entry, request tentacle.Manageme
|
||||
next = append(next, *request.Entry)
|
||||
case "update":
|
||||
next[index] = *request.Entry
|
||||
next[index].Enabled = entries[index].Enabled
|
||||
case "remove":
|
||||
next = slices.Delete(next, index, index+1)
|
||||
case "enable":
|
||||
@@ -268,7 +311,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
sessions := make(map[string]*tentacle.Session)
|
||||
problems := make(map[string]string)
|
||||
startups := make(map[string]tentacleStartupCall)
|
||||
removedPackages := make(map[string]tentacle.Entry)
|
||||
removedPackages := make(map[string]tentacleRetiredPackage)
|
||||
for _, entry := range entries {
|
||||
pending[entry.ID] = entry.Enabled
|
||||
}
|
||||
@@ -315,6 +358,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
}
|
||||
result.Entries = append(result.Entries, status)
|
||||
}
|
||||
result.Updates = tentacle.AvailableUpdates(result)
|
||||
runtime.mutex.Lock()
|
||||
runtime.snapshot = result
|
||||
runtime.mutex.Unlock()
|
||||
@@ -338,7 +382,8 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
if manager != nil {
|
||||
managerError = manager.Shutdown(cleanup)
|
||||
}
|
||||
for _, entry := range removedPackages {
|
||||
for _, retired := range removedPackages {
|
||||
entry := retired.entry
|
||||
if managerError == nil {
|
||||
if err := tentacle.RemovePackage(tentacle.PackageRoot(runtime.registry.Path), entry); err != nil {
|
||||
runtime.logger.Warn("remove tentacle package", "error", err)
|
||||
@@ -382,6 +427,11 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
delete(startups, id)
|
||||
startup.stop()
|
||||
if err == nil {
|
||||
if startup.previous != nil {
|
||||
previous := *startup.previous
|
||||
// Starting the replacement already waited for the old session.
|
||||
removedPackages[previous.PackageDirectory] = tentacleRetiredPackage{entry: previous}
|
||||
}
|
||||
err = startup.saveError
|
||||
}
|
||||
setSnapshot()
|
||||
@@ -389,7 +439,8 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
}
|
||||
failStartup := func(id, message string) {
|
||||
problems[id] = message
|
||||
if _, exists := startups[id]; !exists {
|
||||
startup, exists := startups[id]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
revoke(id)
|
||||
@@ -400,15 +451,31 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
next := tentacle.CloneEntries(entries)
|
||||
for index := range next {
|
||||
if next[index].ID == id {
|
||||
next[index].Enabled = false
|
||||
if startup.previous != nil {
|
||||
next[index] = *startup.previous
|
||||
} else {
|
||||
next[index].Enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
err := runtime.registry.Save(next)
|
||||
if err == nil || errors.Is(err, tentacle.ErrRegistrySync) {
|
||||
if startup.previous != nil {
|
||||
failed := slices.IndexFunc(entries, func(entry tentacle.Entry) bool { return entry.ID == id })
|
||||
if failed >= 0 {
|
||||
entry := entries[failed]
|
||||
removedPackages[entry.PackageDirectory] = tentacleRetiredPackage{entry: entry, session: sessions[id]}
|
||||
}
|
||||
runtime.mutex.Lock()
|
||||
disabled := runtime.disabled
|
||||
runtime.mutex.Unlock()
|
||||
pending[id] = startup.previous.Enabled && !disabled && ctx.Err() == nil
|
||||
message += " The previous package was restored."
|
||||
}
|
||||
entries = next
|
||||
}
|
||||
if err != nil {
|
||||
message += " The disabled state could not be saved durably: " + err.Error()
|
||||
message += " The recovery state could not be saved durably: " + err.Error()
|
||||
}
|
||||
replyStartup(id, errors.New(message))
|
||||
}
|
||||
@@ -467,15 +534,26 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
if current == nil {
|
||||
for id, entry := range removedPackages {
|
||||
if sessions[id] != nil {
|
||||
for directory, retired := range removedPackages {
|
||||
entry := retired.entry
|
||||
if retired.session != nil {
|
||||
select {
|
||||
case <-retired.session.Done():
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
if slices.ContainsFunc(entries, func(active tentacle.Entry) bool {
|
||||
return active.PackageDirectory == directory
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
if err := tentacle.RemovePackage(tentacle.PackageRoot(runtime.registry.Path), entry); err != nil {
|
||||
runtime.logger.Warn("remove tentacle package", "error", err)
|
||||
runtime.report("Tentacle was removed, but its package files could not be deleted.")
|
||||
continue
|
||||
}
|
||||
delete(removedPackages, id)
|
||||
delete(removedPackages, directory)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !pending[entry.ID] || sessions[entry.ID] != nil {
|
||||
@@ -572,6 +650,9 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
call.result <- tentacle.ErrBusy
|
||||
continue
|
||||
}
|
||||
if startup, exists := startups[call.request.ID]; exists && startup.previous != nil {
|
||||
failStartup(call.request.ID, "Tentacle update was cancelled by another operation.")
|
||||
}
|
||||
next, err := change_tentacle_entries(entries, call.request)
|
||||
if err == nil && call.request.Action != "retry" && reflect.DeepEqual(next, entries) {
|
||||
// Saved enablement is intent, not proof of a live session. An
|
||||
@@ -591,15 +672,28 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
}
|
||||
if err == nil || errors.Is(err, tentacle.ErrRegistrySync) {
|
||||
id := call.request.ID
|
||||
var previous *tentacle.Entry
|
||||
if call.packageFiles != nil && call.request.Action == "update" {
|
||||
index := slices.IndexFunc(entries, func(entry tentacle.Entry) bool { return entry.ID == id })
|
||||
if index >= 0 && entries[index].Enabled {
|
||||
saved := tentacle.CloneEntries(entries[index : index+1])[0]
|
||||
previous = &saved
|
||||
}
|
||||
}
|
||||
replyStartup(id, errors.New("Tentacle startup was cancelled by another operation."))
|
||||
if call.packageFiles != nil {
|
||||
call.packageFiles.Retain()
|
||||
}
|
||||
if call.request.Action == "remove" {
|
||||
if call.request.Action == "remove" || call.request.Action == "update" {
|
||||
for _, entry := range entries {
|
||||
if entry.ID == id && entry.PackageDirectory != "" {
|
||||
removedPackages[id] = entry
|
||||
if entry.ID != id || entry.PackageDirectory == "" {
|
||||
continue
|
||||
}
|
||||
if call.request.Action == "update" && (previous != nil ||
|
||||
entry.PackageDirectory == call.request.Entry.PackageDirectory) {
|
||||
continue
|
||||
}
|
||||
removedPackages[entry.PackageDirectory] = tentacleRetiredPackage{entry: entry, session: sessions[id]}
|
||||
}
|
||||
}
|
||||
revoke(id)
|
||||
@@ -619,7 +713,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
runtime.publishCommands(stillActive)
|
||||
setSnapshot()
|
||||
if pending[id] {
|
||||
startups[id] = tentacleStartupCall{call: call, saveError: err, stop: context.AfterFunc(call.ctx, notify)}
|
||||
startups[id] = tentacleStartupCall{call: call, saveError: err, stop: context.AfterFunc(call.ctx, notify), previous: previous}
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -665,6 +759,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
active = candidate
|
||||
delete(problems, session.TentacleID())
|
||||
runtime.publishCommands(active)
|
||||
replyStartup(session.TentacleID(), nil)
|
||||
runtime.logger.Info("configured tentacle active", "tentacle", session.TentacleID(), "session", session.Identity())
|
||||
|
||||
@@ -404,3 +404,25 @@ func TestManagedTentacleCancellationDuringBindingsRollsBack(t *testing.T) {
|
||||
t.Fatalf("incorrect persisted rollback: %+v %v", disk, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Updating an entry must preserve the user's enabled preference.
|
||||
func TestUpdatePreservesEnabledState(t *testing.T) {
|
||||
for _, enabled := range []bool{false, true} {
|
||||
current := tentacle.Entry{ID: "example", Program: []string{"/bin/true"}, Enabled: enabled, Version: "0.1.3"}
|
||||
replacement := tentacle.Entry{ID: "example", Program: []string{"/bin/true"}, Enabled: !enabled, Version: "0.1.4"}
|
||||
next, err := change_tentacle_entries(
|
||||
[]tentacle.Entry{current},
|
||||
tentacle.ManagementRequest{
|
||||
Action: "update",
|
||||
ID: current.ID,
|
||||
Entry: &replacement,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(next) != 1 || next[0].Enabled != enabled || next[0].Version != "0.1.4" {
|
||||
t.Fatalf("enabled=%v: unexpected result: %+v", enabled, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/tentacle"
|
||||
)
|
||||
|
||||
func TestUpgradePreparationFailurePreservesInstallation(t *testing.T) {
|
||||
repository, data := install_fixture(t)
|
||||
|
||||
set_install_transport(t, func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewReader(data)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})
|
||||
|
||||
registryPath := filepath.Join(t.TempDir(), "tentacles.toml")
|
||||
owner, _ := 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)
|
||||
}
|
||||
|
||||
manage := func(action string) error {
|
||||
return owner.Manage(context.Background(), tentacle.ManagementRequest{
|
||||
Action: action,
|
||||
URL: repository.URL,
|
||||
ID: func() string {
|
||||
if action == "install" || action == "upgrade" {
|
||||
return "echo"
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := manage("repository-add"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manage("install"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id := tentacle.PackageID(repository, "echo")
|
||||
wait_managed_state(t, owner, id, "disabled")
|
||||
|
||||
before := owner.Snapshot().Entries[0].Entry
|
||||
|
||||
repository.Revision = strings.Repeat("b", 40)
|
||||
repository.Tentacles[0].Version = "0.2"
|
||||
|
||||
if err := manage("repository-refresh"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
owner.preparePackage = func(context.Context, string, tentacle.Repository, string) (*tentacle.PreparedPackage, error) {
|
||||
return nil, errors.New("simulated download failure")
|
||||
}
|
||||
|
||||
if err := manage("upgrade"); err == nil {
|
||||
t.Fatal("upgrade unexpectedly succeeded")
|
||||
}
|
||||
|
||||
after := owner.Snapshot().Entries[0].Entry
|
||||
|
||||
if after.Version != before.Version ||
|
||||
after.Enabled != before.Enabled ||
|
||||
after.PackageDirectory != before.PackageDirectory {
|
||||
t.Fatalf("failed upgrade changed installation: before=%+v after=%+v", before, after)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(before.PackageDirectory); err != nil {
|
||||
t.Fatalf("failed upgrade removed installed package: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -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