diff --git a/.gitignore b/.gitignore index 6b232f1f..8aad16e8 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ tools/replay/web/ # === Secrets — NEVER commit === .env harness/.env +harness/workspaces/psd.yaml deploy/thothii.env *.pem ca-chain.pem diff --git a/harness/workspaces/psd.yaml b/harness/workspaces/psd.yaml deleted file mode 120000 index ba9c830f..00000000 --- a/harness/workspaces/psd.yaml +++ /dev/null @@ -1 +0,0 @@ -/Users/mp/projects/tht-workspace-psd/psd.yaml \ No newline at end of file diff --git a/tools/thothctl/cmd/thothctl/main.go b/tools/thothctl/cmd/thothctl/main.go index a9f3d415..3cb32bea 100644 --- a/tools/thothctl/cmd/thothctl/main.go +++ b/tools/thothctl/cmd/thothctl/main.go @@ -660,7 +660,7 @@ func requireLF(root string) error { if walkErr != nil { return walkErr } - if entry.IsDir() || !requiresLF(entry.Name()) { + if entry.IsDir() || entry.Type()&os.ModeSymlink != 0 || !requiresLF(entry.Name()) { return nil } contents, err := os.ReadFile(path) @@ -700,13 +700,26 @@ func requireVolumes(renderedConfig string) error { } func requireHealthyServices(status string) error { - var services []struct { + type serviceStatus struct { Service string `json:"Service"` State string `json:"State"` Health string `json:"Health"` } + var services []serviceStatus if err := json.Unmarshal([]byte(status), &services); err != nil { - return errors.New("Compose returned invalid service status") + decoder := json.NewDecoder(strings.NewReader(status)) + for { + var service serviceStatus + if err := decoder.Decode(&service); errors.Is(err, io.EOF) { + break + } else if err != nil { + return errors.New("Compose returned invalid service status") + } + services = append(services, service) + } + if len(services) == 0 { + return errors.New("Compose returned invalid service status") + } } seen := map[string]bool{} for _, service := range services { diff --git a/tools/thothctl/cmd/thothctl/main_test.go b/tools/thothctl/cmd/thothctl/main_test.go index aa36a43f..2be78950 100644 --- a/tools/thothctl/cmd/thothctl/main_test.go +++ b/tools/thothctl/cmd/thothctl/main_test.go @@ -591,6 +591,46 @@ func TestRunDoctorValidatesTheRenderedInstallation(t *testing.T) { } } +func TestRunDoctorDoesNotDereferenceSymlinksDuringLineEndingCheck(t *testing.T) { + fixture := newCLIFixture(t, "SAFE_VALUE=1\n") + fixture.setEnvironment(t) + testsupport.SymlinkOrSkip( + t, + filepath.Join(fixture.projectDirectory, "missing-workspace.yaml"), + filepath.Join(fixture.projectDirectory, "legacy-workspace.yaml"), + ) + + var stdout, stderr bytes.Buffer + exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr) + + if exitCode != 0 { + t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) + } + if stdout.String() != "Doctor checks passed.\n" { + t.Errorf("stdout = %q, want doctor success", stdout.String()) + } +} + +func TestRunDoctorAcceptsComposeJSONLinesServiceStatus(t *testing.T) { + fixture := newCLIFixture(t, "SAFE_VALUE=1\n") + fixture.setEnvironment(t) + t.Setenv( + "THOTHCTL_FAKE_PS", + "{\"Service\":\"core\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+ + "{\"Service\":\"frontend\",\"State\":\"running\",\"Health\":\"healthy\"}", + ) + + var stdout, stderr bytes.Buffer + exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr) + + if exitCode != 0 { + t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String()) + } + if stdout.String() != "Doctor checks passed.\n" { + t.Errorf("stdout = %q, want doctor success", stdout.String()) + } +} + func TestRunPreservesChildExitCodes(t *testing.T) { fixture := newCLIFixture(t, "SAFE_VALUE=1\n") fixture.setEnvironment(t) @@ -991,7 +1031,7 @@ case " $* " in exit "$THOTHCTL_FAKE_MIGRATION_EXIT" fi printf '%s\n' '{"applied":[],"drifted":[],"pending":[]}' ;; - *" ps --format json "*) printf '%s\n' '[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"}]' ;; + *" ps --format json "*) printf '%s\n' "$THOTHCTL_FAKE_PS" ;; *" ps -q core "*) printf '%s\n' 'core-id' ;; *" image inspect --format {{.Id}} "*) printf '%s\n' "${THOTHCTL_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;; *"inspect --format {{.Image}} core-id"*) printf '%s\n' "${THOTHCTL_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;; @@ -1046,6 +1086,7 @@ func (f cliFixture) setEnvContents(t *testing.T, env string) { t.Setenv("THOTHCTL_FAKE_FAILURE", "") t.Setenv("THOTHCTL_FAKE_FAIL_ON", "") t.Setenv("THOTHCTL_FAKE_STOPPED_PS", "[]") + t.Setenv("THOTHCTL_FAKE_PS", `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"}]`) t.Setenv("THOTHCTL_FAKE_CONFIG", "") t.Setenv("THOTHCTL_FAKE_MIGRATION_FAILURE", "") t.Setenv("THOTHCTL_FAKE_MIGRATION_EXIT", "0")