This commit is contained in:
@@ -6,11 +6,13 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ import (
|
|||||||
type fakeDocker struct {
|
type fakeDocker struct {
|
||||||
err error
|
err error
|
||||||
inspection agent.DockerInspection
|
inspection agent.DockerInspection
|
||||||
|
logs string
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeDisk struct{}
|
type fakeDisk struct{}
|
||||||
@@ -53,7 +56,7 @@ func (d fakeDocker) Inspect(_ context.Context, id string) (agent.DockerInspectio
|
|||||||
}
|
}
|
||||||
return inspection, nil
|
return inspection, nil
|
||||||
}
|
}
|
||||||
func (d fakeDocker) Logs(context.Context, string, int) (string, error) { return "", d.err }
|
func (d fakeDocker) Logs(context.Context, string, int) (string, error) { return d.logs, d.err }
|
||||||
func (d fakeDocker) Stats(context.Context, string) (agentwire.InstanceStats, error) {
|
func (d fakeDocker) Stats(context.Context, string) (agentwire.InstanceStats, error) {
|
||||||
return agentwire.InstanceStats{MemoryBytes: 42}, d.err
|
return agentwire.InstanceStats{MemoryBytes: 42}, d.err
|
||||||
}
|
}
|
||||||
@@ -162,6 +165,39 @@ func TestAgentCreatesOnlyValidatedBoundInstances(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentStartExitReturnsDockerDiagnosticWithoutLogs(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
secret := bytes.Repeat([]byte{0x31}, 32)
|
||||||
|
instanceID := "abcdefghijklmnopqrstuvwx"
|
||||||
|
plan := testPlan(t, instanceID, root)
|
||||||
|
docker := fakeDocker{inspection: agent.DockerInspection{
|
||||||
|
ContainerID: "container-" + instanceID, Status: "exited", ExitCode: 23,
|
||||||
|
StartedAt: "2026-01-01T00:00:00Z", FinishedAt: "2026-01-01T00:00:01Z",
|
||||||
|
OOMKilled: true, Health: "unhealthy", Error: "permission denied password=never-leak",
|
||||||
|
Labels: map[string]string{"io.dogama.managed": "true", "io.dogama.instance-id": instanceID, "io.dogama.plan-digest": plan.PlanDigest},
|
||||||
|
}}
|
||||||
|
server := httptest.NewServer(newTestHandler(t, root, secret, docker))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
client, err := agentclient.New(server.URL, secret, server.Client())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := client.CreateInstance(context.Background(), plan); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = client.StartInstance(context.Background(), instanceID)
|
||||||
|
var problem *agentclient.ProblemError
|
||||||
|
if !errors.As(err, &problem) || problem.Code != "start_exited" {
|
||||||
|
t.Fatalf("start error = %#v", err)
|
||||||
|
}
|
||||||
|
if problem.Details["exit_code"] != float64(23) || problem.Details["oom_killed"] != true || problem.Details["logs_tail"] != "" {
|
||||||
|
t.Fatalf("diagnostic = %#v", problem.Details)
|
||||||
|
}
|
||||||
|
if strings.Contains(fmt.Sprint(problem.Details), "never-leak") {
|
||||||
|
t.Fatalf("secret leaked in diagnostic: %#v", problem.Details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgentRejectsPlanSubstitutionAndEscapingMount(t *testing.T) {
|
func TestAgentRejectsPlanSubstitutionAndEscapingMount(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
secret := bytes.Repeat([]byte{0x31}, 32)
|
secret := bytes.Repeat([]byte{0x31}, 32)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
catalogdata "git.zaynet.fr/DoGaMa/DoGaMa-serv/catalog"
|
catalogdata "git.zaynet.fr/DoGaMa/DoGaMa-serv/catalog"
|
||||||
|
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/agentclient"
|
||||||
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/agentwire"
|
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/agentwire"
|
||||||
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
|
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
|
||||||
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/instance"
|
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/instance"
|
||||||
@@ -20,6 +21,12 @@ type lifecycleAgent struct {
|
|||||||
created int
|
created int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type failingStartAgent struct{ lifecycleAgent }
|
||||||
|
|
||||||
|
func (a *failingStartAgent) StartInstance(context.Context, string) (agentwire.InstanceState, error) {
|
||||||
|
return agentwire.InstanceState{}, &agentclient.ProblemError{Status: 502, Code: "start_exited", Details: map[string]any{"state": "exited", "exit_code": 42, "started_at": "2026-01-01T00:00:00Z", "finished_at": "2026-01-01T00:00:01Z", "logs_tail": "", "error": "permission denied password=[REDACTED]"}}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *lifecycleAgent) CreateInstance(_ context.Context, plan agentwire.DeploymentPlan) (agentwire.InstanceState, error) {
|
func (a *lifecycleAgent) CreateInstance(_ context.Context, plan agentwire.DeploymentPlan) (agentwire.InstanceState, error) {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
@@ -162,3 +169,50 @@ func TestLifecycleInstallStartStopAndSafeContainerDeletion(t *testing.T) {
|
|||||||
t.Fatalf("instance intent/player paths were removed: %v", err)
|
t.Fatalf("instance intent/player paths were removed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestManualStartPersistsAgentDiagnosticAndStableCode(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := sqlite.Open(ctx, filepath.Join(t.TempDir(), "dogama.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
repository := sqlite.NewRepository(db)
|
||||||
|
snapshots, err := catalog.LoadFS(catalogdata.Files, ".")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := repository.Sync(ctx, snapshots); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
preview, err := instance.BuildPreview(snapshots[0], instance.PreviewRequest{DisplayName: "Failure", Slug: "failure", HostPorts: map[string]int{"game": 38212}, MountPaths: map[string]string{"saved": filepath.Join(t.TempDir(), "saved")}, DataOrigin: "new", BackupRetention: 7})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
id := "abcdefghijklmnopqrstuvwx"
|
||||||
|
if err := repository.CreateDraft(ctx, instance.Draft{ID: id, Preview: preview}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
agent := &failingStartAgent{}
|
||||||
|
service := instance.NewLifecycleService(repository, agent)
|
||||||
|
if _, err := service.Install(ctx, id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result, startErr := service.Start(ctx, id)
|
||||||
|
if startErr == nil || !strings.Contains(startErr.Error(), "DGM-START-001") || result.OperationID == "" {
|
||||||
|
t.Fatalf("result=%#v err=%v", result, startErr)
|
||||||
|
}
|
||||||
|
diagnostics, err := repository.ListDiagnostics(ctx, id, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(diagnostics) != 1 || diagnostics[0].OperationID != result.OperationID || diagnostics[0].ErrorCode != "DGM-START-001" {
|
||||||
|
t.Fatalf("diagnostics=%#v", diagnostics)
|
||||||
|
}
|
||||||
|
if !strings.Contains(diagnostics[0].Details, `"exit_code":42`) || strings.Contains(diagnostics[0].Details, "password=") && !strings.Contains(diagnostics[0].Details, "[REDACTED]") {
|
||||||
|
t.Fatalf("details=%s", diagnostics[0].Details)
|
||||||
|
}
|
||||||
|
if strings.Contains(startErr.Error(), "The instance operation could not be completed") {
|
||||||
|
t.Fatalf("original cause was lost: %v", startErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func TestV1BootstrapAuthenticationAndHTTPBoundary(t *testing.T) {
|
|||||||
"DOGAMA_LISTEN_ADDRESS="+address,
|
"DOGAMA_LISTEN_ADDRESS="+address,
|
||||||
"DOGAMA_DATABASE_PATH="+filepath.Join(t.TempDir(), "dogama.db"),
|
"DOGAMA_DATABASE_PATH="+filepath.Join(t.TempDir(), "dogama.db"),
|
||||||
"DOGAMA_MASTER_KEY_FILE="+filepath.Join(t.TempDir(), "master_key"),
|
"DOGAMA_MASTER_KEY_FILE="+filepath.Join(t.TempDir(), "master_key"),
|
||||||
|
"DOGAMA_TEMPLATES_ROOT="+filepath.Join(t.TempDir(), "templates"),
|
||||||
"DOGAMA_IMPORTS_ROOT="+filepath.Join(t.TempDir(), "imports"),
|
"DOGAMA_IMPORTS_ROOT="+filepath.Join(t.TempDir(), "imports"),
|
||||||
"DOGAMA_SERVERS_ROOT="+filepath.Join(t.TempDir(), "servers"),
|
"DOGAMA_SERVERS_ROOT="+filepath.Join(t.TempDir(), "servers"),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user