Files
stalwart-migrator/cmd/stalwart-migrate/status.go
T
jcoffey-dev 12ec0c3fd4 Rename the module to the Coffey-Labs organisation
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.
2026-08-30 15:24:18 -07:00

115 lines
3.1 KiB
Go

// SPDX-FileCopyrightText: 2026 Coffey Labs
// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"flag"
"fmt"
"strings"
"github.com/Coffey-Labs/stalwart-migrator/internal/checkpoint"
)
func runStatus(args []string) error {
fs := flag.NewFlagSet("status", flag.ExitOnError)
stateDir := fs.String("state-dir", checkpoint.DefaultBaseDir, "directory runs are checkpointed in")
// Go's flag package stops parsing at the first positional argument, so
// without this `status <run-id> --state-dir X` would look the run up in
// the default directory and report it missing.
runID, rest := splitRunID(fs, args)
if err := fs.Parse(rest); err != nil {
return err
}
if fs.NArg() != 0 {
return fmt.Errorf("usage: stalwart-migrate status [run-id] [flags]")
}
store := checkpoint.NewStore(*stateDir)
if runID == "" {
ids, err := store.List()
if err != nil {
return fmt.Errorf("list runs: %w", err)
}
if len(ids) == 0 {
fmt.Println("no runs found in", *stateDir)
return nil
}
fmt.Println("runs (newest first):")
for _, id := range ids {
fmt.Println(" ", id)
}
return nil
}
rs, err := store.Load(runID)
if err != nil {
return fmt.Errorf("load run %s: %w", runID, err)
}
fmt.Printf("run: %s\n", rs.RunID)
fmt.Printf("source: %s\n", rs.SourceVersion)
fmt.Printf("target: %s\n", rs.TargetVersion)
fmt.Printf("topology: deployment=%s store=%s\n", rs.Topology.DeploymentKind, rs.Topology.StoreBackend)
fmt.Println("steps:")
for _, step := range rs.Steps {
tag := string(step.Status)
if step.Verdict != "" {
tag = step.Verdict
}
line := fmt.Sprintf(" [%-4s] %s/%s", tag, step.Phase, step.Name)
if step.Detail != "" {
line += " - " + step.Detail
}
if step.Error != "" {
line += " (error: " + step.Error + ")"
}
fmt.Println(line)
}
return nil
}
// splitRunID pulls the run-id out of args wherever it appears, so
// `status <run-id> --state-dir X` works as naturally as
// `status --state-dir X <run-id>`. Go's flag package stops parsing at the
// first positional argument, which would otherwise make the obvious
// invocation order silently drop every flag after the run-id. Tokens
// consumed as a flag's value are skipped by asking the FlagSet itself which
// flags take one, rather than by maintaining a second list of them here.
func splitRunID(fs *flag.FlagSet, args []string) (runID string, rest []string) {
rest = make([]string, 0, len(args))
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" {
rest = append(rest, args[i:]...)
break
}
if strings.HasPrefix(arg, "-") {
rest = append(rest, arg)
name := strings.TrimLeft(arg, "-")
if !strings.Contains(arg, "=") && takesValue(fs, name) && i+1 < len(args) {
i++
rest = append(rest, args[i])
}
continue
}
if runID == "" {
runID = arg
continue
}
rest = append(rest, arg) // a second positional: let Parse report it
}
return runID, rest
}
func takesValue(fs *flag.FlagSet, name string) bool {
f := fs.Lookup(name)
if f == nil {
return false
}
boolFlag, ok := f.Value.(interface{ IsBoolFlag() bool })
return !ok || !boolFlag.IsBoolFlag()
}