mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix: Remove duplicate AI chat response streaming (issue #947)
Content was being streamed twice: 1. During each iteration of the tool loop (intended for intermediate feedback) 2. Again after the loop ended with finalContent (redundant) This caused duplicate responses when using Ollama and other providers.
This commit is contained in:
@@ -1730,8 +1730,8 @@ Always execute the commands rather than telling the user how to do it.`
|
||||
}
|
||||
}
|
||||
|
||||
// Stream the final content
|
||||
callback(StreamEvent{Type: "content", Data: finalContent})
|
||||
// Don't stream finalContent here - it was already streamed in the iteration above
|
||||
// Sending it again causes duplicate responses (issue #947)
|
||||
callback(StreamEvent{Type: "done"})
|
||||
|
||||
return &ExecuteResponse{
|
||||
|
||||
@@ -1248,12 +1248,12 @@ func (a *Agent) disableSelf(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Remove Unraid startup script if present to prevent restart on reboot.
|
||||
if err := removeFileIfExists("/boot/config/go.d/pulse-docker-agent.sh"); err != nil {
|
||||
if err := removeFileIfExists(unraidStartupScriptPath); err != nil {
|
||||
a.logger.Warn().Err(err).Msg("Failed to remove Unraid startup script")
|
||||
}
|
||||
|
||||
// Best-effort log cleanup (ignore errors).
|
||||
_ = removeFileIfExists("/var/log/pulse-docker-agent.log")
|
||||
_ = removeFileIfExists(agentLogPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1632,7 +1632,7 @@ func randomDuration(max time.Duration) time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||
n, err := randIntFn(rand.Reader, big.NewInt(int64(max)))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestCollectContainer(t *testing.T) {
|
||||
}
|
||||
|
||||
summary := containertypes.Summary{
|
||||
ID: "container1",
|
||||
ID: "container-123456",
|
||||
Names: []string{"/app"},
|
||||
Image: "nginx@sha256:abc123",
|
||||
ImageID: "sha256:local",
|
||||
@@ -119,7 +119,7 @@ func TestCollectContainer(t *testing.T) {
|
||||
agent := &Agent{
|
||||
logger: logger,
|
||||
prevContainerCPU: map[string]cpuSample{
|
||||
"container1": {totalUsage: 1},
|
||||
"container-123456": {totalUsage: 1},
|
||||
},
|
||||
docker: &fakeDockerClient{
|
||||
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
|
||||
@@ -130,7 +130,7 @@ func TestCollectContainer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
summary := containertypes.Summary{ID: "container1", Names: []string{"/app"}, State: "exited"}
|
||||
summary := containertypes.Summary{ID: "container-123456", Names: []string{"/app"}, State: "exited"}
|
||||
if _, err := agent.collectContainer(context.Background(), summary); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func TestCollectContainer(t *testing.T) {
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container1"}); err == nil {
|
||||
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container-123456"}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
@@ -169,7 +169,7 @@ func TestCollectContainer(t *testing.T) {
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container1"}); err == nil {
|
||||
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container-123456"}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
@@ -189,7 +189,7 @@ func TestCollectContainer(t *testing.T) {
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container1"}); err == nil {
|
||||
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container-123456"}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,11 +31,11 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
got := agent.calculateContainerCPUPercent("container1", stats)
|
||||
got := agent.calculateContainerCPUPercent("container-123456", stats)
|
||||
if got <= 0 {
|
||||
t.Fatalf("expected percent > 0, got %f", got)
|
||||
}
|
||||
if _, ok := agent.prevContainerCPU["container1"]; !ok {
|
||||
if _, ok := agent.prevContainerCPU["container-123456"]; !ok {
|
||||
t.Fatal("expected current sample to be stored")
|
||||
}
|
||||
})
|
||||
@@ -57,11 +57,11 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
got := agent.calculateContainerCPUPercent("container1", stats)
|
||||
got := agent.calculateContainerCPUPercent("container-123456", stats)
|
||||
if got != 0 {
|
||||
t.Fatalf("expected 0, got %f", got)
|
||||
}
|
||||
if _, ok := agent.prevContainerCPU["container1"]; !ok {
|
||||
if _, ok := agent.prevContainerCPU["container-123456"]; !ok {
|
||||
t.Fatal("expected sample to be stored")
|
||||
}
|
||||
})
|
||||
@@ -70,7 +70,7 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
agent := &Agent{
|
||||
logger: logger,
|
||||
prevContainerCPU: map[string]cpuSample{
|
||||
"container1": {
|
||||
"container-123456": {
|
||||
totalUsage: 100,
|
||||
systemUsage: 1000,
|
||||
onlineCPUs: 2,
|
||||
@@ -89,7 +89,7 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
PreCPUStats: containertypes.CPUStats{},
|
||||
}
|
||||
|
||||
got := agent.calculateContainerCPUPercent("container1", stats)
|
||||
got := agent.calculateContainerCPUPercent("container-123456", stats)
|
||||
if got <= 0 {
|
||||
t.Fatalf("expected percent > 0, got %f", got)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
logger: logger,
|
||||
cpuCount: 4,
|
||||
prevContainerCPU: map[string]cpuSample{
|
||||
"container1": {
|
||||
"container-123456": {
|
||||
totalUsage: 100,
|
||||
systemUsage: 1000,
|
||||
onlineCPUs: 0,
|
||||
@@ -119,7 +119,7 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
PreCPUStats: containertypes.CPUStats{},
|
||||
}
|
||||
|
||||
got := agent.calculateContainerCPUPercent("container1", stats)
|
||||
got := agent.calculateContainerCPUPercent("container-123456", stats)
|
||||
if got <= 0 {
|
||||
t.Fatalf("expected percent > 0, got %f", got)
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
agent := &Agent{
|
||||
logger: logger,
|
||||
prevContainerCPU: map[string]cpuSample{
|
||||
"container1": {
|
||||
"container-123456": {
|
||||
totalUsage: 100,
|
||||
systemUsage: 1000,
|
||||
onlineCPUs: 0,
|
||||
@@ -148,7 +148,7 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
PreCPUStats: containertypes.CPUStats{},
|
||||
}
|
||||
|
||||
got := agent.calculateContainerCPUPercent("container1", stats)
|
||||
got := agent.calculateContainerCPUPercent("container-123456", stats)
|
||||
if got != 0 {
|
||||
t.Fatalf("expected 0, got %f", got)
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func TestCalculateContainerCPUPercent(t *testing.T) {
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
_ = agent.calculateContainerCPUPercent("container1", stats)
|
||||
_ = agent.calculateContainerCPUPercent("container-123456", stats)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ func TestStopTimer(t *testing.T) {
|
||||
|
||||
t.Run("timer fired and drained", func(t *testing.T) {
|
||||
timer := time.NewTimer(0)
|
||||
time.Sleep(time.Millisecond)
|
||||
stopTimer(timer)
|
||||
select {
|
||||
case <-timer.C:
|
||||
@@ -220,3 +221,74 @@ func TestCollectOnce(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
t.Run("stop requested on startup", func(t *testing.T) {
|
||||
swap(t, &connectRuntimeFn, func(_ RuntimeKind, _ *zerolog.Logger) (dockerClient, systemtypes.Info, RuntimeKind, error) {
|
||||
return &fakeDockerClient{
|
||||
infoFunc: func(context.Context) (systemtypes.Info, error) {
|
||||
return systemtypes.Info{}, ErrStopRequested
|
||||
},
|
||||
}, systemtypes.Info{}, RuntimeDocker, nil
|
||||
})
|
||||
|
||||
agent := &Agent{
|
||||
cfg: Config{
|
||||
Interval: 10 * time.Millisecond,
|
||||
},
|
||||
docker: &fakeDockerClient{
|
||||
infoFunc: func(context.Context) (systemtypes.Info, error) {
|
||||
return systemtypes.Info{}, ErrStopRequested
|
||||
},
|
||||
},
|
||||
logger: zerolog.Nop(),
|
||||
}
|
||||
|
||||
if err := agent.Run(context.Background()); err != nil {
|
||||
t.Fatalf("expected nil, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ticker and update timer", func(t *testing.T) {
|
||||
swap(t, &randomDurationFn, func(time.Duration) time.Duration {
|
||||
return -5 * time.Second
|
||||
})
|
||||
swap(t, &hostmetricsCollect, func(context.Context, []string) (hostmetrics.Snapshot, error) {
|
||||
return hostmetrics.Snapshot{}, nil
|
||||
})
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
agent := &Agent{
|
||||
cfg: Config{
|
||||
Interval: 5 * time.Millisecond,
|
||||
DisableAutoUpdate: true,
|
||||
},
|
||||
docker: &fakeDockerClient{
|
||||
infoFunc: func(context.Context) (systemtypes.Info, error) {
|
||||
return systemtypes.Info{ID: "daemon", ServerVersion: "24.0.0"}, nil
|
||||
},
|
||||
},
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: server.URL, Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{false: server.Client()},
|
||||
reportBuffer: buffer.New[agentsdocker.Report](10),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- agent.Run(ctx)
|
||||
}()
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
if err := <-done; !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("expected context canceled, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,6 +195,12 @@ func TestSendReportToTarget(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("stop command", func(t *testing.T) {
|
||||
prevPath := os.Getenv("PATH")
|
||||
_ = os.Setenv("PATH", "")
|
||||
t.Cleanup(func() {
|
||||
_ = os.Setenv("PATH", prevPath)
|
||||
})
|
||||
|
||||
var ackBody bytes.Buffer
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
|
||||
@@ -34,7 +34,7 @@ func baseInspect() containertypes.InspectResponse {
|
||||
Config: &containertypes.Config{
|
||||
Image: "nginx:latest",
|
||||
},
|
||||
NetworkSettings: &network.NetworkSettings{
|
||||
NetworkSettings: &containertypes.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"net1": {Aliases: []string{"app"}},
|
||||
"net2": {Aliases: []string{"app2"}},
|
||||
@@ -189,9 +189,6 @@ func TestUpdateContainer_Errors(t *testing.T) {
|
||||
containerStopFn: func(context.Context, string, containertypes.StopOptions) error {
|
||||
return nil
|
||||
},
|
||||
containerRenameFn: func(context.Context, string, string) error {
|
||||
return nil
|
||||
},
|
||||
containerCreateFn: func(context.Context, *containertypes.Config, *containertypes.HostConfig, *network.NetworkingConfig, *v1.Platform, string) (containertypes.CreateResponse, error) {
|
||||
return containertypes.CreateResponse{ID: "new123"}, nil
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ package dockeragent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -14,15 +15,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
connectRuntimeFn = connectRuntime
|
||||
hostmetricsCollect = hostmetrics.Collect
|
||||
newTickerFn = time.NewTicker
|
||||
newTimerFn = time.NewTimer
|
||||
randomDurationFn = randomDuration
|
||||
nowFn = time.Now
|
||||
sleepFn = time.Sleep
|
||||
connectRuntimeFn = connectRuntime
|
||||
hostmetricsCollect = hostmetrics.Collect
|
||||
newTickerFn = time.NewTicker
|
||||
newTimerFn = time.NewTimer
|
||||
randomDurationFn = randomDuration
|
||||
nowFn = time.Now
|
||||
sleepFn = time.Sleep
|
||||
buildRuntimeCandidatesFn = buildRuntimeCandidates
|
||||
tryRuntimeCandidateFn = tryRuntimeCandidate
|
||||
randIntFn = rand.Int
|
||||
osExecutableFn = os.Executable
|
||||
osCreateTempFn = os.CreateTemp
|
||||
closeFileFn = func(f *os.File) error { return f.Close() }
|
||||
@@ -45,8 +47,10 @@ var (
|
||||
"/etc/machine-id",
|
||||
"/var/lib/dbus/machine-id",
|
||||
}
|
||||
unraidVersionPath = "/etc/unraid-version"
|
||||
unraidPersistPath = "/boot/config/plugins/pulse-docker-agent/pulse-docker-agent"
|
||||
unraidVersionPath = "/etc/unraid-version"
|
||||
unraidPersistPath = "/boot/config/plugins/pulse-docker-agent/pulse-docker-agent"
|
||||
unraidStartupScriptPath = "/boot/config/go.d/pulse-docker-agent.sh"
|
||||
agentLogPath = "/var/log/pulse-docker-agent.log"
|
||||
openProcUptime = func() (io.ReadCloser, error) {
|
||||
return os.Open("/proc/uptime")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,912 @@
|
||||
package dockeragent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func TestDetermineSelfUpdateArch_Coverage(t *testing.T) {
|
||||
t.Run("known arches", func(t *testing.T) {
|
||||
swap(t, &goArch, "amd64")
|
||||
if got := determineSelfUpdateArch(); got != "linux-amd64" {
|
||||
t.Fatalf("expected linux-amd64, got %q", got)
|
||||
}
|
||||
|
||||
swap(t, &goArch, "arm64")
|
||||
if got := determineSelfUpdateArch(); got != "linux-arm64" {
|
||||
t.Fatalf("expected linux-arm64, got %q", got)
|
||||
}
|
||||
|
||||
swap(t, &goArch, "arm")
|
||||
if got := determineSelfUpdateArch(); got != "linux-armv7" {
|
||||
t.Fatalf("expected linux-armv7, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uname fallback", func(t *testing.T) {
|
||||
swap(t, &goArch, "other")
|
||||
swap(t, &unameMachine, func() (string, error) {
|
||||
return "x86_64", nil
|
||||
})
|
||||
if got := determineSelfUpdateArch(); got != "linux-amd64" {
|
||||
t.Fatalf("expected linux-amd64, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uname error", func(t *testing.T) {
|
||||
swap(t, &goArch, "other")
|
||||
swap(t, &unameMachine, func() (string, error) {
|
||||
return "", errors.New("boom")
|
||||
})
|
||||
if got := determineSelfUpdateArch(); got != "" {
|
||||
t.Fatalf("expected empty result, got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveSymlink(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
target := filepath.Join(dir, "target")
|
||||
if err := os.WriteFile(target, []byte("data"), 0600); err != nil {
|
||||
t.Fatalf("write target: %v", err)
|
||||
}
|
||||
link := filepath.Join(dir, "link")
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatalf("symlink: %v", err)
|
||||
}
|
||||
|
||||
got, err := resolveSymlink(link)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != target {
|
||||
t.Fatalf("expected %q, got %q", target, got)
|
||||
}
|
||||
|
||||
if _, err := resolveSymlink(filepath.Join(dir, "missing")); err == nil {
|
||||
t.Fatal("expected error for missing symlink")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyELFMagic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
valid := filepath.Join(dir, "valid")
|
||||
if err := os.WriteFile(valid, []byte{0x7f, 'E', 'L', 'F', 0x01}, 0600); err != nil {
|
||||
t.Fatalf("write valid: %v", err)
|
||||
}
|
||||
if err := verifyELFMagic(valid); err != nil {
|
||||
t.Fatalf("expected valid ELF, got %v", err)
|
||||
}
|
||||
|
||||
invalid := filepath.Join(dir, "invalid")
|
||||
if err := os.WriteFile(invalid, []byte("nope"), 0600); err != nil {
|
||||
t.Fatalf("write invalid: %v", err)
|
||||
}
|
||||
if err := verifyELFMagic(invalid); err == nil {
|
||||
t.Fatal("expected error for invalid magic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForUpdates(t *testing.T) {
|
||||
t.Run("dev version skips", func(t *testing.T) {
|
||||
swap(t, &Version, "dev")
|
||||
agent := &Agent{logger: zerolog.Nop()}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
|
||||
t.Run("no target skips", func(t *testing.T) {
|
||||
swap(t, &Version, "1.0.0")
|
||||
agent := &Agent{logger: zerolog.Nop()}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
|
||||
t.Run("request creation error", func(t *testing.T) {
|
||||
swap(t, &Version, "1.0.0")
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com/\x7f"}},
|
||||
}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
|
||||
t.Run("http error", func(t *testing.T) {
|
||||
swap(t, &Version, "1.0.0")
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("boom")
|
||||
})}
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
|
||||
t.Run("non-200 status", func(t *testing.T) {
|
||||
swap(t, &Version, "1.0.0")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: server.URL, Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: server.Client(),
|
||||
},
|
||||
}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
|
||||
t.Run("decode error", func(t *testing.T) {
|
||||
swap(t, &Version, "1.0.0")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("{"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: server.URL, Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: server.Client(),
|
||||
},
|
||||
}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
|
||||
t.Run("server dev version", func(t *testing.T) {
|
||||
swap(t, &Version, "1.0.0")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"version":"dev"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: server.URL, Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: server.Client(),
|
||||
},
|
||||
}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
|
||||
t.Run("up to date", func(t *testing.T) {
|
||||
swap(t, &Version, "v1.2.3")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"version":"1.2.3"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: server.URL, Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: server.Client(),
|
||||
},
|
||||
}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
|
||||
t.Run("update success", func(t *testing.T) {
|
||||
swap(t, &Version, "1.2.3")
|
||||
called := false
|
||||
swap(t, &selfUpdateFunc, func(*Agent, context.Context) error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"version":"1.2.4"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: server.URL, Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: server.Client(),
|
||||
},
|
||||
}
|
||||
agent.checkForUpdates(context.Background())
|
||||
if !called {
|
||||
t.Fatal("expected selfUpdate to be called")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update error", func(t *testing.T) {
|
||||
swap(t, &Version, "1.2.3")
|
||||
swap(t, &selfUpdateFunc, func(*Agent, context.Context) error {
|
||||
return errors.New("update failed")
|
||||
})
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"version":"1.2.4"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: server.URL, Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: server.Client(),
|
||||
},
|
||||
}
|
||||
agent.checkForUpdates(context.Background())
|
||||
})
|
||||
}
|
||||
|
||||
type sizeReadCloser struct {
|
||||
remaining int64
|
||||
}
|
||||
|
||||
func (s *sizeReadCloser) Read(p []byte) (int, error) {
|
||||
if s.remaining <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(p)) > s.remaining {
|
||||
p = p[:s.remaining]
|
||||
}
|
||||
for i := range p {
|
||||
p[i] = 0
|
||||
}
|
||||
s.remaining -= int64(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (s *sizeReadCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func elfBytes() []byte {
|
||||
return []byte{0x7f, 'E', 'L', 'F', 0x01, 0x02, 0x03}
|
||||
}
|
||||
|
||||
func sha256Hex(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func TestSelfUpdate(t *testing.T) {
|
||||
t.Run("no target", func(t *testing.T) {
|
||||
agent := &Agent{logger: zerolog.Nop()}
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("executable error", func(t *testing.T) {
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return "", errors.New("no exec")
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("request creation error", func(t *testing.T) {
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com/\x7f", Token: "token"}},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("request error", func(t *testing.T) {
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: {Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("send failed")
|
||||
})},
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("status error", func(t *testing.T) {
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Status: http.StatusText(http.StatusInternalServerError),
|
||||
Body: io.NopCloser(strings.NewReader("fail")),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create temp error", func(t *testing.T) {
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
body := elfBytes()
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return filepath.Join(t.TempDir(), "missing", "exec"), nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("copy error", func(t *testing.T) {
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: errReadCloser{err: errors.New("read failed")},
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{"ignored"}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("too large", func(t *testing.T) {
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: &sizeReadCloser{remaining: (100 * 1024 * 1024) + 1},
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{"ignored"}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("close error", func(t *testing.T) {
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
body := elfBytes()
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
swap(t, &closeFileFn, func(*os.File) error {
|
||||
return errors.New("close failed")
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid elf", func(t *testing.T) {
|
||||
body := []byte("bad")
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing checksum", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("checksum mismatch", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{"bad"}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("chmod error", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
swap(t, &osChmodFn, func(string, os.FileMode) error {
|
||||
return errors.New("chmod failed")
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rename backup error", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
swap(t, &osRenameFn, func(string, string) error {
|
||||
return errors.New("rename failed")
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rename replace error", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
|
||||
calls := 0
|
||||
swap(t, &osRenameFn, func(old, new string) error {
|
||||
calls++
|
||||
if calls == 2 {
|
||||
return errors.New("rename failed")
|
||||
}
|
||||
return os.Rename(old, new)
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unraid read error", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
unraidPath := filepath.Join(dir, "unraid-version")
|
||||
if err := os.WriteFile(unraidPath, []byte("1"), 0600); err != nil {
|
||||
t.Fatalf("write unraid: %v", err)
|
||||
}
|
||||
swap(t, &unraidVersionPath, unraidPath)
|
||||
persist := filepath.Join(dir, "persist")
|
||||
if err := os.WriteFile(persist, []byte("old"), 0600); err != nil {
|
||||
t.Fatalf("write persist: %v", err)
|
||||
}
|
||||
swap(t, &unraidPersistPath, persist)
|
||||
swap(t, &osReadFileFn, func(string) ([]byte, error) {
|
||||
return nil, errors.New("read failed")
|
||||
})
|
||||
swap(t, &syscallExecFn, func(string, []string, []string) error {
|
||||
return errors.New("exec failed")
|
||||
})
|
||||
|
||||
_ = agent.selfUpdate(context.Background())
|
||||
})
|
||||
|
||||
t.Run("unraid write error", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
unraidPath := filepath.Join(dir, "unraid-version")
|
||||
if err := os.WriteFile(unraidPath, []byte("1"), 0600); err != nil {
|
||||
t.Fatalf("write unraid: %v", err)
|
||||
}
|
||||
swap(t, &unraidVersionPath, unraidPath)
|
||||
persist := filepath.Join(dir, "persist")
|
||||
if err := os.WriteFile(persist, []byte("old"), 0600); err != nil {
|
||||
t.Fatalf("write persist: %v", err)
|
||||
}
|
||||
swap(t, &unraidPersistPath, persist)
|
||||
swap(t, &osWriteFileFn, func(string, []byte, os.FileMode) error {
|
||||
return errors.New("write failed")
|
||||
})
|
||||
swap(t, &syscallExecFn, func(string, []string, []string) error {
|
||||
return errors.New("exec failed")
|
||||
})
|
||||
|
||||
_ = agent.selfUpdate(context.Background())
|
||||
})
|
||||
|
||||
t.Run("unraid rename error", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
unraidPath := filepath.Join(dir, "unraid-version")
|
||||
if err := os.WriteFile(unraidPath, []byte("1"), 0600); err != nil {
|
||||
t.Fatalf("write unraid: %v", err)
|
||||
}
|
||||
swap(t, &unraidVersionPath, unraidPath)
|
||||
persist := filepath.Join(dir, "persist")
|
||||
if err := os.WriteFile(persist, []byte("old"), 0600); err != nil {
|
||||
t.Fatalf("write persist: %v", err)
|
||||
}
|
||||
swap(t, &unraidPersistPath, persist)
|
||||
swap(t, &osRenameFn, func(old, new string) error {
|
||||
if strings.HasSuffix(new, ".tmp") {
|
||||
return os.Rename(old, new)
|
||||
}
|
||||
return errors.New("rename failed")
|
||||
})
|
||||
swap(t, &syscallExecFn, func(string, []string, []string) error {
|
||||
return errors.New("exec failed")
|
||||
})
|
||||
|
||||
_ = agent.selfUpdate(context.Background())
|
||||
})
|
||||
|
||||
t.Run("exec error", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
swap(t, &syscallExecFn, func(string, []string, []string) error {
|
||||
return errors.New("exec failed")
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exec success", func(t *testing.T) {
|
||||
body := elfBytes()
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"X-Checksum-Sha256": []string{sha256Hex(body)}},
|
||||
}, nil
|
||||
})}
|
||||
|
||||
agent := &Agent{
|
||||
logger: zerolog.Nop(),
|
||||
targets: []TargetConfig{{URL: "http://example.com", Token: "token"}},
|
||||
httpClients: map[bool]*http.Client{
|
||||
false: client,
|
||||
},
|
||||
}
|
||||
dir := t.TempDir()
|
||||
execPath := filepath.Join(dir, "exec")
|
||||
if err := os.WriteFile(execPath, elfBytes(), 0700); err != nil {
|
||||
t.Fatalf("write exec: %v", err)
|
||||
}
|
||||
swap(t, &osExecutableFn, func() (string, error) {
|
||||
return execPath, nil
|
||||
})
|
||||
swap(t, &syscallExecFn, func(string, []string, []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := agent.selfUpdate(context.Background()); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -24,6 +24,7 @@ func TestMapSwarmService(t *testing.T) {
|
||||
"com.docker.stack.namespace": "stack",
|
||||
},
|
||||
},
|
||||
Mode: swarmtypes.ServiceMode{Replicated: &swarmtypes.ReplicatedService{}},
|
||||
TaskTemplate: swarmtypes.TaskSpec{
|
||||
ContainerSpec: &swarmtypes.ContainerSpec{
|
||||
Image: "nginx:latest",
|
||||
@@ -128,7 +129,7 @@ func TestMapSwarmTask(t *testing.T) {
|
||||
ID: "task2",
|
||||
ServiceID: "svc2",
|
||||
Status: swarmtypes.TaskStatus{
|
||||
State: swarmtypes.TaskStateCompleted,
|
||||
State: swarmtypes.TaskStateComplete,
|
||||
ContainerStatus: &swarmtypes.ContainerStatus{
|
||||
ContainerID: "container-full",
|
||||
},
|
||||
@@ -169,7 +170,7 @@ func TestCollectSwarmDataFromManager(t *testing.T) {
|
||||
}
|
||||
|
||||
info := systemtypes.Info{
|
||||
Swarm: systemtypes.SwarmInfo{
|
||||
Swarm: swarmtypes.Info{
|
||||
NodeID: "node1",
|
||||
},
|
||||
}
|
||||
@@ -198,8 +199,8 @@ func TestCollectSwarmData(t *testing.T) {
|
||||
t.Run("inactive swarm returns info only", func(t *testing.T) {
|
||||
agent := &Agent{supportsSwarm: true, cfg: Config{SwarmScope: swarmScopeNode}}
|
||||
info := systemtypes.Info{
|
||||
Swarm: systemtypes.SwarmInfo{
|
||||
NodeID: "node1",
|
||||
Swarm: swarmtypes.Info{
|
||||
NodeID: "node1",
|
||||
LocalNodeState: swarmtypes.LocalNodeStatePending,
|
||||
},
|
||||
}
|
||||
@@ -250,7 +251,7 @@ func TestCollectSwarmData(t *testing.T) {
|
||||
}
|
||||
|
||||
info := systemtypes.Info{
|
||||
Swarm: systemtypes.SwarmInfo{
|
||||
Swarm: swarmtypes.Info{
|
||||
NodeID: "node1",
|
||||
ControlAvailable: true,
|
||||
LocalNodeState: swarmtypes.LocalNodeStateActive,
|
||||
@@ -291,7 +292,7 @@ func TestCollectSwarmData(t *testing.T) {
|
||||
}
|
||||
|
||||
info := systemtypes.Info{
|
||||
Swarm: systemtypes.SwarmInfo{
|
||||
Swarm: swarmtypes.Info{
|
||||
NodeID: "node1",
|
||||
ControlAvailable: true,
|
||||
LocalNodeState: swarmtypes.LocalNodeStateActive,
|
||||
|
||||
Reference in New Issue
Block a user