100 lines
3.0 KiB
Go

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"`
}
var (
readIntentImpl = readIntentDefault
writeIntentImpl = writeIntentDefault
)
// 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.
func ReadIntent(path string) (Intent, error) {
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) {
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 {
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
}
return in, nil
}
// 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.
func WriteIntent(path string, in Intent) error {
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 {
if in.UpdatedAt.IsZero() {
in.UpdatedAt = time.Now().UTC()
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return err
}
b, _ := json.MarshalIndent(in, "", " ")
return os.WriteFile(path, b, 0o640)
}
// 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.
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(),
})
}