fix(ops): make server diagnostics release-safe
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user