2026-04-04 12:44:15 -03:00
|
|
|
package state
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
IntentNormal = "normal"
|
|
|
|
|
IntentStartupInProgress = "startup_in_progress"
|
|
|
|
|
IntentShuttingDown = "shutting_down"
|
|
|
|
|
IntentShutdownComplete = "shutdown_complete"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Intent struct {
|
|
|
|
|
State string `json:"state"`
|
|
|
|
|
Reason string `json:"reason,omitempty"`
|
|
|
|
|
Source string `json:"source,omitempty"`
|
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 17:33:26 -03:00
|
|
|
var (
|
|
|
|
|
readIntentImpl = readIntentDefault
|
|
|
|
|
writeIntentImpl = writeIntentDefault
|
|
|
|
|
)
|
2026-04-09 04:56:41 -03:00
|
|
|
|
2026-04-09 01:38:06 -03:00
|
|
|
// ReadIntent runs one orchestration or CLI step.
|
|
|
|
|
// Signature: ReadIntent(path string) (Intent, error).
|
|
|
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
2026-04-04 12:44:15 -03:00
|
|
|
func ReadIntent(path string) (Intent, error) {
|
2026-04-21 17:33:26 -03:00
|
|
|
return readIntentImpl(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// readIntentDefault runs one orchestration or CLI step.
|
|
|
|
|
// Signature: readIntentDefault(path string) (Intent, error).
|
|
|
|
|
// Why: keeps production read behavior available while tests can override intent
|
|
|
|
|
// reads deterministically without racing background file mutations.
|
|
|
|
|
func readIntentDefault(path string) (Intent, error) {
|
2026-04-04 12:44:15 -03:00
|
|
|
b, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
return Intent{}, nil
|
|
|
|
|
}
|
|
|
|
|
return Intent{}, err
|
|
|
|
|
}
|
|
|
|
|
if len(b) == 0 {
|
|
|
|
|
return Intent{}, nil
|
|
|
|
|
}
|
|
|
|
|
var in Intent
|
|
|
|
|
if err := json.Unmarshal(b, &in); err != nil {
|
2026-04-04 22:24:56 -03:00
|
|
|
if healErr := quarantineCorruptFile(path, b, []byte("{}\n"), 0o640); healErr != nil {
|
|
|
|
|
return Intent{}, fmt.Errorf("decode intent: %w (auto-heal failed: %v)", err, healErr)
|
|
|
|
|
}
|
|
|
|
|
return Intent{}, nil
|
2026-04-04 12:44:15 -03:00
|
|
|
}
|
|
|
|
|
return in, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 01:38:06 -03:00
|
|
|
// WriteIntent runs one orchestration or CLI step.
|
|
|
|
|
// Signature: WriteIntent(path string, in Intent) error.
|
|
|
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
2026-04-04 12:44:15 -03:00
|
|
|
func WriteIntent(path string, in Intent) error {
|
2026-04-09 04:56:41 -03:00
|
|
|
return writeIntentImpl(path, in)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeIntentDefault runs one orchestration or CLI step.
|
|
|
|
|
// Signature: writeIntentDefault(path string, in Intent) error.
|
|
|
|
|
// Why: keeps the production write behavior available while tests can override
|
|
|
|
|
// WriteIntent deterministically without root-only permission tricks.
|
|
|
|
|
func writeIntentDefault(path string, in Intent) error {
|
2026-04-04 12:44:15 -03:00
|
|
|
if in.UpdatedAt.IsZero() {
|
|
|
|
|
in.UpdatedAt = time.Now().UTC()
|
|
|
|
|
}
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-04-09 01:38:06 -03:00
|
|
|
b, _ := json.MarshalIndent(in, "", " ")
|
2026-04-04 12:44:15 -03:00
|
|
|
return os.WriteFile(path, b, 0o640)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 01:38:06 -03:00
|
|
|
// MustWriteIntent runs one orchestration or CLI step.
|
|
|
|
|
// Signature: MustWriteIntent(path string, state string, reason string, source string) error.
|
|
|
|
|
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
|
2026-04-04 12:44:15 -03:00
|
|
|
func MustWriteIntent(path string, state string, reason string, source string) error {
|
|
|
|
|
switch state {
|
|
|
|
|
case IntentNormal, IntentStartupInProgress, IntentShuttingDown, IntentShutdownComplete:
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("invalid intent state: %s", state)
|
|
|
|
|
}
|
|
|
|
|
return WriteIntent(path, Intent{
|
|
|
|
|
State: state,
|
|
|
|
|
Reason: reason,
|
|
|
|
|
Source: source,
|
|
|
|
|
UpdatedAt: time.Now().UTC(),
|
|
|
|
|
})
|
|
|
|
|
}
|