The repositories moved off LINUXexpert-org. Here that is not a documentation change: the old organisation was the module path, so it is declared in go.mod and repeated in every internal import. Leaving it would have been worse than a stale link. GitHub redirects the repository, but a go.mod whose module line disagrees with the path it was fetched from is an error rather than a redirect, so `go get` on the new address would have failed against the old declaration. go.mod, 34 files of imports, and the repository links in README and ARCHITECTURE. go mod tidy leaves go.sum untouched -- no dependency moved, only our own path.
96 lines
3.1 KiB
Go
96 lines
3.1 KiB
Go
// SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
|
|
"github.com/Coffey-Labs/stalwart-migrator/internal/checkpoint"
|
|
"github.com/Coffey-Labs/stalwart-migrator/internal/validate"
|
|
)
|
|
|
|
// runReport prints what validation found for a run, from the checkpoint the
|
|
// run already wrote. It re-reads rather than re-checks: the comparison is
|
|
// against a pre-migration snapshot, so running it again later would answer a
|
|
// different question — how the instance looks now, not how it looked when it
|
|
// was migrated.
|
|
func runReport(args []string) error {
|
|
fs := flag.NewFlagSet("report", flag.ExitOnError)
|
|
stateDir := fs.String("state-dir", checkpoint.DefaultBaseDir, "directory runs are checkpointed in")
|
|
|
|
runID, rest := splitRunID(fs, args)
|
|
if err := fs.Parse(rest); err != nil {
|
|
return err
|
|
}
|
|
if fs.NArg() != 0 || runID == "" {
|
|
return fmt.Errorf("usage: stalwart-migrate report <run-id> [flags]")
|
|
}
|
|
|
|
store := checkpoint.NewStore(*stateDir)
|
|
rs, err := store.Load(runID)
|
|
if err != nil {
|
|
return fmt.Errorf("load run %s: %w", runID, err)
|
|
}
|
|
|
|
report := reportFromSteps(rs)
|
|
fmt.Printf("run: %s\n", rs.RunID)
|
|
fmt.Printf("source: %s\n", rs.SourceVersion)
|
|
fmt.Printf("target: %s\n", rs.TargetVersion)
|
|
|
|
if len(report.Results) == 0 {
|
|
fmt.Println("\nno validation has been recorded for this run.")
|
|
if rs.PreflightSnapshot == nil {
|
|
fmt.Println("preflight captured no pre-migration snapshot, so there was nothing to compare against;")
|
|
fmt.Println("pass --admin-url to preflight next time and the comparison becomes possible.")
|
|
} else {
|
|
fmt.Println("the run did not reach the validate phase - see `stalwart-migrate status " + runID + "`.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("\nvalidation:")
|
|
fmt.Print(report.String())
|
|
if report.Blocking() {
|
|
// Non-zero so this is usable in a script that gates on it, and so a
|
|
// failed migration cannot look successful to anything watching.
|
|
return fmt.Errorf("validation recorded a failure for run %s", runID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// reportFromSteps rebuilds the validation report from the checkpointed
|
|
// steps, so the report survives the process that produced it.
|
|
func reportFromSteps(rs *checkpoint.RunState) validate.Report {
|
|
var report validate.Report
|
|
for _, step := range rs.Steps {
|
|
if step.Phase != checkpoint.PhaseValidate {
|
|
continue
|
|
}
|
|
status := validate.StatusOK
|
|
switch {
|
|
case step.Error != "":
|
|
status = validate.StatusFail
|
|
case step.Verdict == string(validate.StatusFail):
|
|
status = validate.StatusFail
|
|
case step.Verdict == string(validate.StatusSkip):
|
|
status = validate.StatusSkip
|
|
case step.Verdict == string(validate.StatusWarn):
|
|
status = validate.StatusWarn
|
|
case step.Status != checkpoint.StepDone:
|
|
// Recorded but never completed: the run stopped partway.
|
|
status = validate.StatusFail
|
|
}
|
|
detail := step.Detail
|
|
if step.Error != "" {
|
|
if detail != "" {
|
|
detail += " "
|
|
}
|
|
detail += "(error: " + step.Error + ")"
|
|
}
|
|
report.Results = append(report.Results, validate.CheckResult{Name: step.Name, Status: status, Detail: detail})
|
|
}
|
|
return report
|
|
}
|