package cmd import ( "encoding/json" "fmt" "os" "github.com/portainer/kubesolo-os/update/pkg/state" ) // statusReport is the JSON-emitted shape of `kubesolo-update status --json`. // Combines the bootloader-level A/B view with the update-agent state machine. type statusReport struct { ActiveSlot string `json:"active_slot"` PassiveSlot string `json:"passive_slot"` BootCounter int `json:"boot_counter"` BootSuccess bool `json:"boot_success"` State *state.UpdateState `json:"state"` } // Status displays the current A/B slot configuration and boot state. // With --json, emits the full state report to stdout for orchestration // tooling. func Status(args []string) error { opts := parseOpts(args) env := opts.NewBootEnv() activeSlot, err := env.ActiveSlot() if err != nil { return fmt.Errorf("reading active slot: %w", err) } passiveSlot, err := env.PassiveSlot() if err != nil { return fmt.Errorf("reading passive slot: %w", err) } bootCounter, err := env.BootCounter() if err != nil { return fmt.Errorf("reading boot counter: %w", err) } bootSuccess, err := env.BootSuccess() if err != nil { return fmt.Errorf("reading boot success: %w", err) } // State file is non-fatal: present means we have an update lifecycle // recorded; absent means no update has run yet. st, _ := state.Load(opts.StatePath) if opts.JSON { report := statusReport{ ActiveSlot: activeSlot, PassiveSlot: passiveSlot, BootCounter: bootCounter, BootSuccess: bootSuccess, State: st, } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") return enc.Encode(report) } fmt.Println("KubeSolo OS — A/B Partition Status") fmt.Println("───────────────────────────────────") fmt.Printf(" Active slot: %s\n", activeSlot) fmt.Printf(" Passive slot: %s\n", passiveSlot) fmt.Printf(" Boot counter: %d\n", bootCounter) if bootSuccess { fmt.Printf(" Boot success: 1\n") } else { fmt.Printf(" Boot success: 0\n") } if bootSuccess { fmt.Println("\n ✓ System is healthy (boot confirmed)") } else if bootCounter == 0 { fmt.Println("\n ✗ Boot counter exhausted — rollback will occur on next reboot") } else { fmt.Printf("\n ⚠ Boot pending verification (%d attempts remaining)\n", bootCounter) } if st != nil && st.Phase != state.PhaseIdle { fmt.Println("\nUpdate Lifecycle") fmt.Println("───────────────────────────────────") fmt.Printf(" Phase: %s\n", st.Phase) if st.FromVersion != "" { fmt.Printf(" From version: %s\n", st.FromVersion) } if st.ToVersion != "" { fmt.Printf(" To version: %s\n", st.ToVersion) } if !st.StartedAt.IsZero() { fmt.Printf(" Started: %s\n", st.StartedAt.Format("2006-01-02 15:04:05 MST")) } fmt.Printf(" Updated: %s\n", st.UpdatedAt.Format("2006-01-02 15:04:05 MST")) fmt.Printf(" Attempts: %d\n", st.AttemptCount) if st.LastError != "" { fmt.Printf(" Last error: %s\n", st.LastError) } } return nil }