host: a moves source that does not open logs its cause once per agent start (Task 8 fix round 1, item 2)
platform-1e ruled the refusal stays fail closed: with the source unopened, the claim refuses every wake on the box, serving included. The cause (for example an unusable CARLOS_SECRETS_KEY) is now logged once per process, so an operator can see why the box serves nothing. Regression: TestMovesSourceOpenRefusalIsLoggedOnce (three refused opens, the refusal kept each time, one line naming the cause).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+42
−0
internal/host/claimmoveparks.go+17 −0internal/host/moveswakesource_test.go+25 −0
diff --git a/internal/host/claimmoveparks.go b/internal/host/claimmoveparks.go| index 26b056aed..e93c9c6dc 100644 |
| --- a/internal/host/claimmoveparks.go |
| +++ b/internal/host/claimmoveparks.go |
| @@ -3,6 +3,9 @@ package host |
| import ( |
| "context" |
| "errors" |
| + "log" |
| + "sync" |
| + |
| "github.com/carlosframework/platform/internal/activator" |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| @@ -40,6 +43,14 @@ func RegisterProductionClaimMoveParks(view func(context.Context, hostclaim.Obser |
| return nil |
| } |
| +// claimMoveWakeLogf and claimMoveWakeLogOnce log a moves source that did |
| +// not open, once per process (per agent start); variables so the tests can |
| +// read and reset them. |
| +var ( |
| + claimMoveWakeLogf = log.Printf |
| + claimMoveWakeLogOnce = new(sync.Once) |
| +) |
| + |
| // openProductionClaimMoveWake opens the registered moves source over the |
| // claim's data bucket as the serving owners see it (moves Task 8 fix round |
| // 1): through hostbox.RequestStore, so every bucket request the moves |
| @@ -59,6 +70,12 @@ func openProductionClaimMoveWake(ctx context.Context, store release.Store, actor |
| owners, files, err := external.Open(ctx, store, data, actor, out.moveRead) |
| if err != nil { |
| out.wakeUnavailable = errors.Join(&ClaimDependencyError{Contracts: []string{"moves wake owners and C8 origin-qualified files manifest provider"}}, err) |
| + // Fail closed (platform-1e): every wake on this box is refused. Say |
| + // why once, so an operator can see it (for example, an unusable |
| + // CARLOS_SECRETS_KEY with CARLOS_MOVES_PHASE1 on). |
| + claimMoveWakeLogOnce.Do(func() { |
| + claimMoveWakeLogf("host claims: the moves wake source did not open, so every wake on this box is refused, serving included: %v", err) |
| + }) |
| return |
| } |
| out.Moves, out.Serving.Files = owners, files |
diff --git a/internal/host/moveswakesource_test.go b/internal/host/moveswakesource_test.go| index fed015a73..948b9ca1f 100644 |
| --- a/internal/host/moveswakesource_test.go |
| +++ b/internal/host/moveswakesource_test.go |
| @@ -4,8 +4,10 @@ import ( |
| "context" |
| "encoding/base64" |
| "errors" |
| + "fmt" |
| "reflect" |
| "strings" |
| + "sync" |
| "testing" |
| "time" |
| @@ -454,3 +456,26 @@ func TestMovesOwnersReadTheBucketThroughTheRequestStore(t *testing.T) { |
| t.Fatal("the operation's cancellation did not reach the request") |
| } |
| } |
| + |
| +// TestMovesSourceOpenRefusalIsLoggedOnce (Task 8 fix round 1, |
| +// platform-1e): a moves source that does not open keeps every wake on the |
| +// box refused (fail closed), and the cause is logged once per agent start, |
| +// so an operator can see why the box serves nothing. |
| +func TestMovesSourceOpenRefusalIsLoggedOnce(t *testing.T) { |
| + var lines []string |
| + oldLogf, oldOnce := claimMoveWakeLogf, claimMoveWakeLogOnce |
| + claimMoveWakeLogf = func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) } |
| + claimMoveWakeLogOnce = new(sync.Once) |
| + t.Cleanup(func() { claimMoveWakeLogf, claimMoveWakeLogOnce = oldLogf, oldOnce }) |
| + cause := errors.New("moves: secrets key: not a 32-byte key") |
| + for i := 0; i < 3; i++ { |
| + out := ClaimOwnerBindings{} |
| + openProductionClaimMoveWake(t.Context(), nil, hostbox.Actor{}, &parkRegistrationWake{failure: cause}, &out, func(context.Context) error { return nil }) |
| + if !errors.Is(out.wakeUnavailable, cause) { |
| + t.Fatalf("open %d: the refusal is not kept: %v", i, out.wakeUnavailable) |
| + } |
| + } |
| + if len(lines) != 1 || !strings.Contains(lines[0], cause.Error()) || !strings.Contains(lines[0], "every wake") { |
| + t.Fatalf("logged %q, want the cause once", lines) |
| + } |
| +} |