fix(ops): make server diagnostics release-safe
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
func TestRestoreAuthBearingArchiveRefusesNonRootBeforeTransactionOrWrite(t *testing.T) {
|
||||
installation, archive := projectedRestoreFixture(t)
|
||||
deps := restoreTestDependencies(t, newBackupRunner(installation, false))
|
||||
deps.requireAuthProjection = requireAuthProjectionRestorePrivilege
|
||||
checkpointCalled := false
|
||||
beginCalled := false
|
||||
writeCalled := false
|
||||
|
||||
@@ -373,27 +373,97 @@ func filePermissions(installation config.Installation) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateVolumes checks the eight persistent volumes required by a local ThothII installation.
|
||||
type renderedMount struct {
|
||||
Type string `json:"type"`
|
||||
Target string `json:"target"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
type renderedService struct {
|
||||
Volumes []renderedMount `json:"volumes"`
|
||||
}
|
||||
|
||||
type renderedPersistence struct {
|
||||
Volumes map[string]json.RawMessage `json:"volumes"`
|
||||
Services map[string]renderedService `json:"services"`
|
||||
}
|
||||
|
||||
var requiredNamedVolumes = []string{
|
||||
"settings", "pi-state", "workspace-registry", "workspace-secrets",
|
||||
"sessions", "qdrant-data", "embedding-models", "auth-state",
|
||||
}
|
||||
|
||||
var requiredPersistentMounts = []struct {
|
||||
service string
|
||||
target string
|
||||
label string
|
||||
}{
|
||||
{service: "core", target: "/data/settings", label: "settings"},
|
||||
{service: "core", target: "/home/thoth/.pi", label: "pi-state"},
|
||||
{service: "core", target: "/data/workspace-registry", label: "workspace-registry"},
|
||||
{service: "core", target: "/data/workspace-secrets", label: "workspace-secrets"},
|
||||
{service: "core", target: "/data/sessions", label: "sessions"},
|
||||
{service: "qdrant", target: "/qdrant/storage", label: "qdrant-data"},
|
||||
{service: "embedding", target: "/root/.ollama", label: "embedding-models"},
|
||||
{service: "core", target: "/data/auth", label: "auth-state"},
|
||||
}
|
||||
|
||||
// ValidateVolumes accepts either the portable named-volume layout or the server layout where a
|
||||
// writable bind root owns several nested persistence paths. Docker Compose omits unused top-level
|
||||
// volume declarations after a server override, so declarations alone cannot validate that profile.
|
||||
func ValidateVolumes(rendered string) error {
|
||||
var document struct {
|
||||
Volumes map[string]json.RawMessage `json:"volumes"`
|
||||
}
|
||||
var document renderedPersistence
|
||||
if err := json.Unmarshal([]byte(rendered), &document); err != nil {
|
||||
return errors.New("Compose returned invalid rendered configuration")
|
||||
}
|
||||
for _, name := range []string{"settings", "pi-state", "workspace-registry", "workspace-secrets", "sessions", "qdrant-data", "embedding-models", "auth-state"} {
|
||||
missing := ""
|
||||
for _, name := range requiredNamedVolumes {
|
||||
if _, exists := document.Volumes[name]; !exists {
|
||||
return fmt.Errorf("rendered Compose configuration is missing required volume %s", name)
|
||||
missing = name
|
||||
break
|
||||
}
|
||||
}
|
||||
if missing == "" {
|
||||
return nil
|
||||
}
|
||||
if len(document.Services) == 0 {
|
||||
return fmt.Errorf("rendered Compose configuration is missing required volume %s", missing)
|
||||
}
|
||||
for _, required := range requiredPersistentMounts {
|
||||
service, exists := document.Services[required.service]
|
||||
if !exists || !hasWritableMountCovering(service.Volumes, required.target) {
|
||||
return fmt.Errorf("rendered Compose configuration is missing persistent mount %s", required.label)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func workspaceRegistryDeclared(rendered string) bool {
|
||||
var document struct {
|
||||
Volumes map[string]json.RawMessage `json:"volumes"`
|
||||
var document renderedPersistence
|
||||
if json.Unmarshal([]byte(rendered), &document) != nil {
|
||||
return false
|
||||
}
|
||||
return json.Unmarshal([]byte(rendered), &document) == nil && document.Volumes["workspace-registry"] != nil
|
||||
if document.Volumes["workspace-registry"] != nil {
|
||||
return true
|
||||
}
|
||||
return hasWritableMountCovering(document.Services["core"].Volumes, "/data/workspace-registry")
|
||||
}
|
||||
|
||||
func hasWritableMountCovering(mounts []renderedMount, target string) bool {
|
||||
for _, mount := range mounts {
|
||||
if mount.ReadOnly || (mount.Type != "bind" && mount.Type != "volume") {
|
||||
continue
|
||||
}
|
||||
mountTarget := filepath.Clean(mount.Target)
|
||||
if !filepath.IsAbs(mountTarget) {
|
||||
continue
|
||||
}
|
||||
relative, err := filepath.Rel(mountTarget, target)
|
||||
if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func commandDetail(label string, result compose.Result, err error, secrets []string) string {
|
||||
|
||||
@@ -89,6 +89,38 @@ func TestValidateVolumesRequiresAuthState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Catches rejecting a server installation whose durable state is provided by bind mounts.
|
||||
func TestRunAcceptsServerBindMountPersistence(t *testing.T) {
|
||||
installation := doctorInstallation(t, "")
|
||||
installation.Profile = "server"
|
||||
runner := &doctorRunner{services: healthyServices, rendered: serverRenderedConfig}
|
||||
|
||||
report, err := Run(context.Background(), installation, runner)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !report.OK {
|
||||
t.Fatalf("Run() report = %#v, want healthy server bind-mount installation", report)
|
||||
}
|
||||
if checkStatus(report, "configuration") != StatusPassed ||
|
||||
checkStatus(report, "authentication") != StatusPassed ||
|
||||
checkStatus(report, "workspace-registry") != StatusPassed {
|
||||
t.Fatalf("Run() report = %#v, want server configuration and dependent checks passed", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateVolumesRejectsIncompleteServerBindMountPersistence(t *testing.T) {
|
||||
withoutPiState := strings.Replace(
|
||||
serverRenderedConfig,
|
||||
`{"type": "bind", "source": "/srv/thothii/pi-state", "target": "/home/thoth/.pi"}`,
|
||||
`{"type": "bind", "source": "/srv/thothii/pi-state", "target": "/tmp/pi-state"}`,
|
||||
1,
|
||||
)
|
||||
if err := ValidateVolumes(withoutPiState); err == nil || !strings.Contains(err.Error(), "pi-state") {
|
||||
t.Fatalf("ValidateVolumes() error = %v, want missing pi-state persistence", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Catches Docker availability short-circuiting a host file-permission failure.
|
||||
func TestRunChecksUnsafeFilesEvenWhenDockerIsUnavailable(t *testing.T) {
|
||||
installation := doctorInstallation(t, "")
|
||||
@@ -302,6 +334,7 @@ type doctorRunner struct {
|
||||
calls []string
|
||||
dockerUnavailable bool
|
||||
services string
|
||||
rendered string
|
||||
workflowFailure string
|
||||
registryInvalid bool
|
||||
}
|
||||
@@ -320,6 +353,9 @@ func (r *doctorRunner) Run(_ context.Context, args []string, _ io.Reader) (compo
|
||||
case strings.Contains(call, "config --quiet"):
|
||||
return compose.Result{}, nil
|
||||
case strings.Contains(call, "config --format json"):
|
||||
if r.rendered != "" {
|
||||
return compose.Result{Stdout: r.rendered}, nil
|
||||
}
|
||||
return compose.Result{Stdout: renderedConfig}, nil
|
||||
case strings.Contains(call, "ps --all --format json"):
|
||||
if r.services == "" {
|
||||
@@ -395,6 +431,24 @@ func assertChecklist(t *testing.T, report Report, want []string) {
|
||||
|
||||
const renderedConfig = `{"volumes":{"settings":{},"pi-state":{},"workspace-registry":{},"workspace-secrets":{},"sessions":{},"qdrant-data":{},"embedding-models":{},"auth-state":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}`
|
||||
|
||||
const serverRenderedConfig = `{
|
||||
"volumes": {"catalog-data": {}, "qdrant-data": {}, "embedding-models": {}},
|
||||
"services": {
|
||||
"core": {
|
||||
"image": "thothii-core:server",
|
||||
"environment": {"THT_LLM_URL": "https://llm.example.invalid"},
|
||||
"volumes": [
|
||||
{"type": "bind", "source": "/srv/thothii/data", "target": "/data"},
|
||||
{"type": "bind", "source": "/srv/thothii/pi-state", "target": "/home/thoth/.pi"},
|
||||
{"type": "bind", "source": "/srv/thothii/workspace-registry", "target": "/data/workspace-registry"}
|
||||
]
|
||||
},
|
||||
"catalog-db": {"volumes": [{"type": "volume", "source": "catalog-data", "target": "/var/lib/postgresql/data"}]},
|
||||
"qdrant": {"volumes": [{"type": "volume", "source": "qdrant-data", "target": "/qdrant/storage"}]},
|
||||
"embedding": {"volumes": [{"type": "volume", "source": "embedding-models", "target": "/root/.ollama"}]}
|
||||
}
|
||||
}`
|
||||
|
||||
const healthyServices = `[
|
||||
{"Service":"core","State":"running","Health":"healthy"},
|
||||
{"Service":"frontend","State":"running","Health":"healthy"},
|
||||
|
||||
Reference in New Issue
Block a user