package cmd import ( "fmt" "log/slog" "github.com/portainer/kubesolo-os/update/pkg/state" ) // Activate switches the boot target to the passive partition. // After activation, the next reboot will boot from the new partition // with boot_counter=3. If health checks fail 3 times, GRUB auto-rolls back. // // State transition: Staged → Activated. On failure → Failed. func Activate(args []string) error { opts := parseOpts(args) env := opts.NewBootEnv() st, err := state.Load(opts.StatePath) if err != nil { slog.Warn("state file unreadable, starting fresh", "error", err) st = state.New() } // Get passive slot (the one we want to boot into) passiveSlot, err := env.PassiveSlot() if err != nil { _ = st.RecordError(opts.StatePath, fmt.Errorf("reading passive slot: %w", err)) return fmt.Errorf("reading passive slot: %w", err) } activeSlot, err := env.ActiveSlot() if err != nil { _ = st.RecordError(opts.StatePath, fmt.Errorf("reading active slot: %w", err)) return fmt.Errorf("reading active slot: %w", err) } slog.Info("activating slot", "from", activeSlot, "to", passiveSlot) // Set the passive slot as active with fresh boot counter if err := env.ActivateSlot(passiveSlot); err != nil { _ = st.RecordError(opts.StatePath, fmt.Errorf("activating slot %s: %w", passiveSlot, err)) return fmt.Errorf("activating slot %s: %w", passiveSlot, err) } if err := st.Transition(opts.StatePath, state.PhaseActivated, "", ""); err != nil { slog.Warn("state transition failed", "phase", state.PhaseActivated, "error", err) } fmt.Printf("Slot %s activated (was %s)\n", passiveSlot, activeSlot) fmt.Println("Boot counter set to 3. Reboot to start the new version.") fmt.Println("The system will automatically roll back if health checks fail 3 times.") return nil }