From df52e8d620296325cc5c933540f2060975519930 Mon Sep 17 00:00:00 2001 From: Angel Knutsen Aune Date: Tue, 18 Aug 2026 17:40:33 +0200 Subject: [PATCH 1/2] Check installation script exit codes --- server/install.go | 38 +++++++++++++++++-------- server/install_test.go | 63 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 server/install_test.go diff --git a/server/install.go b/server/install.go index e1237b8e..e1292184 100644 --- a/server/install.go +++ b/server/install.go @@ -177,7 +177,7 @@ func (s *Server) SetRestoring(state bool) { } func (s *Server) IsInProtectedState() bool { - return s.IsInstalling() || s.IsTransferring() || s.IsRestoring() + return s.IsInstalling() || s.IsTransferring() || s.IsRestoring() } // RemoveContainer removes the installation container for the server. @@ -214,10 +214,10 @@ func (ip *InstallationProcess) Run() error { return err } - cID, err := ip.Execute() - if err != nil { + cID, executeErr := ip.Execute() + if cID == "" { _ = ip.RemoveContainer() - return err + return executeErr } // If this step fails, log a warning but don't exit out of the process. This is completely @@ -226,7 +226,7 @@ func (ip *InstallationProcess) Run() error { ip.Server.Log().WithField("error", err).Warn("failed to complete after-execute step of installation process") } - return nil + return executeErr } // Returns the location of the temporary data for the installation process. @@ -531,18 +531,34 @@ func (ip *InstallationProcess) Execute() (string, error) { sChan, eChan := ip.client.ContainerWait(ctx, r.ID, container.WaitConditionNotRunning) select { case err := <-eChan: - // Once the container has stopped running we can mark the install process as being completed. - if err == nil { - ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.") - } else { - return "", err + return r.ID, err + case response := <-sChan: + if err := installationWaitError(response); err != nil { + ip.Server.Events().Publish(DaemonMessageEvent, "Installation process failed: "+err.Error()) + return r.ID, err } - case <-sChan: } + ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.") return r.ID, nil } +func installationWaitError(response container.WaitResponse) error { + if response.Error != nil { + message := strings.TrimSpace(response.Error.Message) + if message == "" { + message = "unknown container wait error" + } + + return errors.Errorf("install: installation container wait failed: %s", message) + } + if response.StatusCode != 0 { + return errors.Errorf("install: installation script exited with code %d", response.StatusCode) + } + + return nil +} + // StreamOutput streams the output of the installation process to a log file in // the server configuration directory, as well as to a websocket listener so // that the process can be viewed in the panel by administrators. diff --git a/server/install_test.go b/server/install_test.go new file mode 100644 index 00000000..eb652e9f --- /dev/null +++ b/server/install_test.go @@ -0,0 +1,63 @@ +package server + +import ( + "testing" + + "github.com/docker/docker/api/types/container" +) + +func TestInstallationWaitError(t *testing.T) { + tests := []struct { + name string + response container.WaitResponse + wantError string + }{ + { + name: "successful installation", + response: container.WaitResponse{StatusCode: 0}, + }, + { + name: "installation script failure", + response: container.WaitResponse{StatusCode: 1}, + wantError: "install: installation script exited with code 1", + }, + { + name: "command not found", + response: container.WaitResponse{StatusCode: 127}, + wantError: "install: installation script exited with code 127", + }, + { + name: "container wait failure", + response: container.WaitResponse{ + Error: &container.WaitExitError{Message: "daemon disconnected"}, + }, + wantError: "install: installation container wait failed: daemon disconnected", + }, + { + name: "container wait failure without message", + response: container.WaitResponse{ + Error: &container.WaitExitError{}, + }, + wantError: "install: installation container wait failed: unknown container wait error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := installationWaitError(tt.response) + if tt.wantError == "" { + if err != nil { + t.Fatalf("installationWaitError() returned unexpected error: %v", err) + } + return + } + + if err == nil { + t.Fatalf("installationWaitError() returned nil, expected %q", tt.wantError) + } + if err.Error() != tt.wantError { + t.Fatalf("installationWaitError() error = %q, expected %q", err.Error(), tt.wantError) + } + }) + } +} From 49127ede2445c86074da1713b814c0a0e71f2b83 Mon Sep 17 00:00:00 2001 From: Angel Knutsen Aune Date: Tue, 18 Aug 2026 18:49:34 +0200 Subject: [PATCH 2/2] Publish installer wait failures --- server/install.go | 15 ++++++++++++--- server/install_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/server/install.go b/server/install.go index e1292184..bf0cd1d0 100644 --- a/server/install.go +++ b/server/install.go @@ -529,18 +529,27 @@ func (ip *InstallationProcess) Execute() (string, error) { }(r.ID) sChan, eChan := ip.client.ContainerWait(ctx, r.ID, container.WaitConditionNotRunning) + if err := ip.waitForInstallationContainer(sChan, eChan); err != nil { + return r.ID, err + } + + return r.ID, nil +} + +func (ip *InstallationProcess) waitForInstallationContainer(sChan <-chan container.WaitResponse, eChan <-chan error) error { select { case err := <-eChan: - return r.ID, err + ip.Server.Events().Publish(DaemonMessageEvent, "Installation process failed: "+err.Error()) + return err case response := <-sChan: if err := installationWaitError(response); err != nil { ip.Server.Events().Publish(DaemonMessageEvent, "Installation process failed: "+err.Error()) - return r.ID, err + return err } } ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.") - return r.ID, nil + return nil } func installationWaitError(response container.WaitResponse) error { diff --git a/server/install_test.go b/server/install_test.go index eb652e9f..c2784fe5 100644 --- a/server/install_test.go +++ b/server/install_test.go @@ -1,9 +1,12 @@ package server import ( + "errors" "testing" "github.com/docker/docker/api/types/container" + + "github.com/pelican/wings/events" ) func TestInstallationWaitError(t *testing.T) { @@ -61,3 +64,37 @@ func TestInstallationWaitError(t *testing.T) { }) } } + +func TestWaitForInstallationContainerPublishesWaitFailure(t *testing.T) { + s, err := New(nil) + if err != nil { + t.Fatalf("New() returned unexpected error: %v", err) + } + + listener := make(chan []byte, 1) + s.Events().On(listener) + defer s.Events().Off(listener) + + waitErr := errors.New("daemon disconnected") + sChan := make(chan container.WaitResponse) + eChan := make(chan error, 1) + eChan <- waitErr + + ip := &InstallationProcess{Server: s} + if err := ip.waitForInstallationContainer(sChan, eChan); !errors.Is(err, waitErr) { + t.Fatalf("waitForInstallationContainer() error = %v, expected %v", err, waitErr) + } + + select { + case raw := <-listener: + event := events.MustDecode(raw) + if event.Topic != DaemonMessageEvent { + t.Fatalf("event topic = %q, expected %q", event.Topic, DaemonMessageEvent) + } + if event.Data != "Installation process failed: daemon disconnected" { + t.Fatalf("event data = %q, expected failure message", event.Data) + } + default: + t.Fatal("waitForInstallationContainer() did not publish a failure event") + } +}