feat(auth): include authentication in workspace and tht diagnostics

This commit is contained in:
2026-08-17 15:51:16 +02:00
parent cb0e873ed7
commit 3ed00ff086
19 changed files with 1050 additions and 34 deletions
+35 -9
View File
@@ -12,6 +12,7 @@ import (
"strings"
"time"
"github.com/aritmolab/thothii/tools/tht/internal/authconfig"
"github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/output"
@@ -98,7 +99,7 @@ func RunWithProbe(ctx context.Context, installation config.Installation, runner
return Report{}, errors.New("doctor requires an HTTP probe")
}
secretValues := secretValues(installation)
report := Report{Checks: make([]Check, 0, 11)}
report := Report{Checks: make([]Check, 0, 12)}
add := func(name, status, detail string) {
report.Checks = append(report.Checks, Check{Name: name, Status: status, Detail: output.SanitizeDetail(detail, secretValues)})
}
@@ -117,6 +118,7 @@ func RunWithProbe(ctx context.Context, installation config.Installation, runner
if !commandCheck(ctx, runner, []string{"version", "--format", "{{.Client.Version}}"}, secretValues, add, "docker", "Docker Engine") {
add("compose", StatusSkipped, "Docker Engine is unavailable")
add("configuration", StatusSkipped, "Docker Engine is unavailable")
add("authentication", StatusSkipped, "core is unavailable")
add("services", StatusSkipped, "Docker Engine is unavailable")
add("core-http", StatusSkipped, "core is unavailable")
add("frontend-http", StatusSkipped, "frontend is unavailable")
@@ -127,6 +129,7 @@ func RunWithProbe(ctx context.Context, installation config.Installation, runner
}
if !commandCheck(ctx, runner, []string{"compose", "version", "--short"}, secretValues, add, "compose", "Docker Compose") {
add("configuration", StatusSkipped, "Docker Compose is unavailable")
add("authentication", StatusSkipped, "core is unavailable")
add("services", StatusSkipped, "Docker Compose is unavailable")
add("core-http", StatusSkipped, "core is unavailable")
add("frontend-http", StatusSkipped, "frontend is unavailable")
@@ -150,7 +153,7 @@ func RunWithProbe(ctx context.Context, installation config.Installation, runner
add("configuration", StatusPassed, "Compose configuration and required volumes are valid")
}
status, statusAvailable := serviceStatus(ctx, installation, runner, secretValues, add)
status, statusAvailable, servicesCheck := serviceStatus(ctx, installation, runner, secretValues)
coreRunning := false
if statusAvailable {
var err error
@@ -159,6 +162,14 @@ func RunWithProbe(ctx context.Context, installation config.Installation, runner
coreRunning = false
}
}
if !configReady || !coreRunning {
add("authentication", StatusSkipped, "core is unavailable")
} else if authenticationCheck(ctx, installation, runner, secretValues) {
add("authentication", StatusPassed, "container-local authentication diagnostics passed")
} else {
add("authentication", StatusFailed, "container-local authentication diagnostics failed")
}
add(servicesCheck.Name, servicesCheck.Status, servicesCheck.Detail)
if !coreRunning {
add("core-http", StatusSkipped, "core is not running")
add("frontend-http", StatusSkipped, "core is not running")
@@ -226,18 +237,33 @@ func commandCheck(ctx context.Context, runner Runner, args []string, secrets []s
return true
}
func serviceStatus(ctx context.Context, installation config.Installation, runner Runner, secrets []string, add func(string, string, string)) (string, bool) {
func serviceStatus(ctx context.Context, installation config.Installation, runner Runner, secrets []string) (string, bool, Check) {
result, err := runner.Run(ctx, installation.ComposeArgs("ps", "--all", "--format", "json"), nil)
if err != nil {
add("services", StatusFailed, commandDetail("Compose service status", result, err, secrets))
return "", false
return "", false, Check{Name: "services", Status: StatusFailed, Detail: commandDetail("Compose service status", result, err, secrets)}
}
if err := service.Healthy(result.Stdout); err != nil {
add("services", StatusFailed, err.Error())
return result.Stdout, true
return result.Stdout, true, Check{Name: "services", Status: StatusFailed, Detail: err.Error()}
}
add("services", StatusPassed, "required services are running and reachable through Docker health checks")
return result.Stdout, true
return result.Stdout, true, Check{Name: "services", Status: StatusPassed, Detail: "required services are running and reachable through Docker health checks"}
}
func authenticationCheck(ctx context.Context, installation config.Installation, runner Runner, secrets []string) bool {
report, err := authconfig.Check(ctx, installation, runner, false, true)
if err != nil {
return false
}
if !report.Ready {
return false
}
// Check performs stream redaction before decoding; retain this sanitization call as a boundary
// if future report fields are added to the backend machine contract.
for _, check := range report.Checks {
if output.Sanitize(check.Message, secrets) != check.Message {
return false
}
}
return true
}
func workflowCheck(ctx context.Context, installation config.Installation, runner Runner, secrets []string, add func(string, string, string)) {
+8 -3
View File
@@ -52,7 +52,7 @@ func TestRunSkipsContainerDiagnosticsWhenCoreIsStopped(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if report.OK || checkStatus(report, "workflow") != "skipped" || checkStatus(report, "pi") != "skipped" {
if report.OK || checkStatus(report, "authentication") != "skipped" || checkStatus(report, "workflow") != "skipped" || checkStatus(report, "pi") != "skipped" {
t.Fatalf("Run() report = %#v, want stopped-core skips", report)
}
if strings.Contains(strings.Join(runner.calls, "\n"), " exec -T core ") {
@@ -69,11 +69,14 @@ func TestRunUsesOnlyContainerLocalWorkflowAndPiDiagnosticsWhenCoreRuns(t *testin
if err != nil {
t.Fatal(err)
}
if !report.OK || checkStatus(report, "workflow") != "passed" || checkStatus(report, "pi") != "passed" {
if !report.OK || checkStatus(report, "authentication") != "passed" || checkStatus(report, "workflow") != "passed" || checkStatus(report, "pi") != "passed" {
t.Fatalf("Run() report = %#v, want successful container diagnostics", report)
}
assertChecklist(t, report, []string{"descriptor", "files", "docker", "compose", "configuration", "services", "core-http", "frontend-http", "workspace-registry", "workflow", "pi"})
assertChecklist(t, report, []string{"descriptor", "files", "docker", "compose", "configuration", "authentication", "services", "core-http", "frontend-http", "workspace-registry", "workflow", "pi"})
calls := strings.Join(runner.calls, "\n")
if !strings.Contains(calls, "exec -T core node dist/auth/diagnostic-command.js --json") {
t.Fatalf("Run() calls = %s, want core-local authentication diagnostic", calls)
}
if !strings.Contains(calls, "exec -T core tht doctor --json") {
t.Fatalf("Run() calls = %s, want core-local workflow doctor", calls)
}
@@ -203,6 +206,8 @@ func (r *doctorRunner) Run(_ context.Context, args []string, _ io.Reader) (compo
return compose.Result{Stderr: r.workflowFailure, ExitCode: 23}, errors.New("workflow failed")
}
return compose.Result{Stdout: `{"ok":true,"checks":[]}`}, nil
case strings.Contains(call, "dist/auth/diagnostic-command.js --json"):
return compose.Result{Stdout: `{"ready":true,"mode":"oidc","checks":[{"level":"info","code":"auth_ready","message":"Authentication is ready."}]}`}, nil
case strings.Contains(call, "workspace-registry/state/active.json"):
if r.registryInvalid {
return compose.Result{ExitCode: 23, Stderr: "invalid workspace registry"}, errors.New("registry invalid")