use hecate intent API for peer guard checks
This commit is contained in:
parent
ad4361322d
commit
73b1c2063b
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@ -481,7 +480,7 @@ func coordinatorAllowsPeerFallbackStartup(ctx context.Context, cfg config.Config
|
||||
if user != "" {
|
||||
target = user + "@" + host
|
||||
}
|
||||
remoteCmd := "sudo -n sh -lc 'if systemctl is-active --quiet hecate-bootstrap.service; then echo __HECATE_BOOTSTRAP_ACTIVE__; else echo __HECATE_BOOTSTRAP_IDLE__; fi; if [ -s /var/lib/hecate/intent.json ]; then cat /var/lib/hecate/intent.json; else echo \"{}\"; fi'"
|
||||
remoteCmd := "if sudo -n /usr/bin/systemctl is-active --quiet hecate-bootstrap.service; then echo __HECATE_BOOTSTRAP_ACTIVE__; else echo __HECATE_BOOTSTRAP_IDLE__; fi; sudo -n /usr/local/bin/hecate intent --config /etc/hecate/hecate.yaml"
|
||||
args := append(buildSSHBaseArgs(cfg), target, remoteCmd)
|
||||
out, err := runSSHWithRecovery(ctx, logger, cfg, args, []string{coordinator, host, cfg.SSHJumpHost})
|
||||
if err != nil {
|
||||
@ -492,15 +491,9 @@ func coordinatorAllowsPeerFallbackStartup(ctx context.Context, cfg config.Config
|
||||
if strings.Contains(trimmed, "__HECATE_BOOTSTRAP_ACTIVE__") {
|
||||
return false, "coordinator bootstrap service is active", nil
|
||||
}
|
||||
start := strings.Index(trimmed, "{")
|
||||
end := strings.LastIndex(trimmed, "}")
|
||||
if start < 0 || end < start {
|
||||
return false, "coordinator intent payload missing", nil
|
||||
}
|
||||
rawIntent := trimmed[start : end+1]
|
||||
var remoteIntent state.Intent
|
||||
if err := json.Unmarshal([]byte(rawIntent), &remoteIntent); err != nil {
|
||||
return false, "", fmt.Errorf("decode coordinator intent: %w", err)
|
||||
remoteIntent, parseErr := state.ParseIntentOutput(trimmed)
|
||||
if parseErr != nil {
|
||||
return false, "", fmt.Errorf("decode coordinator intent: %w", parseErr)
|
||||
}
|
||||
if remoteIntent.State == "" || remoteIntent.State == state.IntentNormal {
|
||||
return true, "coordinator intent is normal", nil
|
||||
|
||||
@ -1022,18 +1022,13 @@ func (o *Orchestrator) readRemoteIntent(ctx context.Context, node string) (state
|
||||
if !o.sshManaged(node) {
|
||||
return state.Intent{}, fmt.Errorf("%s is not in ssh_managed_nodes", node)
|
||||
}
|
||||
out, err := o.ssh(ctx, node, "sudo -n sh -lc 'if [ -s /var/lib/hecate/intent.json ]; then cat /var/lib/hecate/intent.json; else echo \"{}\"; fi'")
|
||||
out, err := o.ssh(ctx, node, "sudo -n /usr/local/bin/hecate intent --config /etc/hecate/hecate.yaml")
|
||||
if err != nil {
|
||||
return state.Intent{}, err
|
||||
}
|
||||
start := strings.Index(out, "{")
|
||||
end := strings.LastIndex(out, "}")
|
||||
if start < 0 || end < start {
|
||||
return state.Intent{}, fmt.Errorf("remote intent payload missing json object")
|
||||
}
|
||||
var in state.Intent
|
||||
if err := json.Unmarshal([]byte(out[start:end+1]), &in); err != nil {
|
||||
return state.Intent{}, fmt.Errorf("decode remote intent json: %w", err)
|
||||
in, err := state.ParseIntentOutput(out)
|
||||
if err != nil {
|
||||
return state.Intent{}, fmt.Errorf("parse remote intent output: %w", err)
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
56
internal/state/intent_parse.go
Normal file
56
internal/state/intent_parse.go
Normal file
@ -0,0 +1,56 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParseIntentOutput parses `hecate intent` CLI output from local/remote commands.
|
||||
func ParseIntentOutput(raw string) (Intent, error) {
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
idx := strings.Index(line, "intent=")
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
payload := strings.TrimSpace(line[idx:])
|
||||
fields := strings.Fields(payload)
|
||||
if len(fields) == 0 || !strings.HasPrefix(fields[0], "intent=") {
|
||||
continue
|
||||
}
|
||||
stateValue := strings.TrimSpace(strings.TrimPrefix(fields[0], "intent="))
|
||||
if stateValue == "" || stateValue == "none" {
|
||||
return Intent{}, nil
|
||||
}
|
||||
in := Intent{State: stateValue}
|
||||
if strings.Contains(payload, `reason="`) {
|
||||
parts := strings.SplitN(payload, `reason="`, 2)
|
||||
if len(parts) == 2 {
|
||||
if end := strings.Index(parts[1], `"`); end >= 0 {
|
||||
in.Reason = parts[1][:end]
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, field := range fields[1:] {
|
||||
if strings.HasPrefix(field, "source=") {
|
||||
in.Source = strings.TrimSpace(strings.TrimPrefix(field, "source="))
|
||||
}
|
||||
if strings.HasPrefix(field, "updated_at=") {
|
||||
ts := strings.TrimSpace(strings.TrimPrefix(field, "updated_at="))
|
||||
if ts != "" {
|
||||
parsed, err := time.Parse(time.RFC3339, ts)
|
||||
if err != nil {
|
||||
return Intent{}, fmt.Errorf("parse updated_at %q: %w", ts, err)
|
||||
}
|
||||
in.UpdatedAt = parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
return Intent{}, fmt.Errorf("intent output missing intent= token")
|
||||
}
|
||||
@ -59,3 +59,33 @@ func TestReadIntentAutoHealsCorruptJSON(t *testing.T) {
|
||||
t.Fatalf("expected 1 backup file, got %d (%v)", len(matches), matches)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntentOutputParsesStructuredLine(t *testing.T) {
|
||||
raw := `[hecate] 2026/04/05 11:24:49 intent=normal reason="guard-test-clear-2" source=drill updated_at=2026-04-05T16:24:33Z`
|
||||
in, err := ParseIntentOutput(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parse intent output: %v", err)
|
||||
}
|
||||
if in.State != IntentNormal {
|
||||
t.Fatalf("expected state=%q got=%q", IntentNormal, in.State)
|
||||
}
|
||||
if in.Reason != "guard-test-clear-2" {
|
||||
t.Fatalf("expected reason parsed, got %q", in.Reason)
|
||||
}
|
||||
if in.Source != "drill" {
|
||||
t.Fatalf("expected source parsed, got %q", in.Source)
|
||||
}
|
||||
if in.UpdatedAt.IsZero() {
|
||||
t.Fatalf("expected updated_at parsed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntentOutputHandlesNone(t *testing.T) {
|
||||
in, err := ParseIntentOutput(`[hecate] 2026/04/05 11:24:49 intent=none`)
|
||||
if err != nil {
|
||||
t.Fatalf("parse none intent output: %v", err)
|
||||
}
|
||||
if in.State != "" {
|
||||
t.Fatalf("expected empty intent for none, got state=%q", in.State)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user