mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Restore Docker update preflight through unified agent
This commit is contained in:
@@ -1526,6 +1526,16 @@ func (b *lateBoundDockerUpdater) TypedContainerUpdate(ctx context.Context, runti
|
||||
return updater.TypedContainerUpdate(ctx, runtime, containerID, expectedImageDigest, progress)
|
||||
}
|
||||
|
||||
func (b *lateBoundDockerUpdater) TypedContainerUpdatePreflight(ctx context.Context, runtime, containerID, expectedImageDigest string) error {
|
||||
b.mu.RLock()
|
||||
updater := b.updater
|
||||
b.mu.RUnlock()
|
||||
if updater == nil {
|
||||
return agentexec.NewActionPreflightError(agentexec.ActionRefusalCapabilityUnavailable, fmt.Errorf("docker module is not running on this agent"))
|
||||
}
|
||||
return updater.TypedContainerUpdatePreflight(ctx, runtime, containerID, expectedImageDigest)
|
||||
}
|
||||
|
||||
func initDockerWithRetry(ctx context.Context, cfg dockeragent.Config, logger *zerolog.Logger) RunnableCloser {
|
||||
return initModuleWithRetry(ctx, logger, "docker_agent", "Docker", "Docker not available, will retry", func() (RunnableCloser, error) {
|
||||
return newDockerAgent(cfg)
|
||||
|
||||
@@ -2301,10 +2301,16 @@ func TestSecureAgentStateDir(t *testing.T) {
|
||||
|
||||
type stubTypedContainerUpdater struct {
|
||||
calls int
|
||||
preflightCalls int
|
||||
lifecycleInspects int
|
||||
lifecycleMutations int
|
||||
}
|
||||
|
||||
func (s *stubTypedContainerUpdater) TypedContainerUpdatePreflight(context.Context, string, string, string) error {
|
||||
s.preflightCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubTypedContainerUpdater) TypedContainerUpdate(context.Context, string, string, string, func(string)) (agentexec.DockerContainerUpdateOutcome, error) {
|
||||
s.calls++
|
||||
return agentexec.DockerContainerUpdateOutcome{Success: true}, nil
|
||||
@@ -2322,19 +2328,35 @@ func (s *stubTypedContainerUpdater) MutateDockerContainerLifecycle(context.Conte
|
||||
|
||||
func TestLateBoundDockerUpdaterBridgesModuleWhenItComesUp(t *testing.T) {
|
||||
bridge := &lateBoundDockerUpdater{}
|
||||
containerID := strings.Repeat("a", 12)
|
||||
expectedImageDigest := "sha256:" + strings.Repeat("1", 64)
|
||||
|
||||
if _, err := bridge.TypedContainerUpdate(context.Background(), "docker", strings.Repeat("a", 12), "sha256:"+strings.Repeat("1", 64), nil); err == nil {
|
||||
if err := bridge.TypedContainerUpdatePreflight(context.Background(), "docker", containerID, expectedImageDigest); err == nil {
|
||||
t.Fatal("bridge without a docker module accepted a preflight")
|
||||
} else if got := agentexec.ActionPreflightReasonCode(err, agentexec.ActionRefusalTargetPreconditionFailed); got != agentexec.ActionRefusalCapabilityUnavailable {
|
||||
t.Fatalf("bridge without a docker module refusal = %q, want %q", got, agentexec.ActionRefusalCapabilityUnavailable)
|
||||
}
|
||||
if _, err := bridge.TypedContainerUpdate(context.Background(), "docker", containerID, expectedImageDigest, nil); err == nil {
|
||||
t.Fatal("bridge without a docker module accepted an update")
|
||||
}
|
||||
|
||||
bridge.set(struct{}{}) // non-implementing candidates must not install
|
||||
if _, err := bridge.TypedContainerUpdate(context.Background(), "docker", strings.Repeat("a", 12), "sha256:"+strings.Repeat("1", 64), nil); err == nil {
|
||||
if err := bridge.TypedContainerUpdatePreflight(context.Background(), "docker", containerID, expectedImageDigest); err == nil {
|
||||
t.Fatal("bridge accepted a preflight after a non-implementing candidate was offered")
|
||||
}
|
||||
if _, err := bridge.TypedContainerUpdate(context.Background(), "docker", containerID, expectedImageDigest, nil); err == nil {
|
||||
t.Fatal("bridge accepted an update after a non-implementing candidate was offered")
|
||||
}
|
||||
|
||||
stub := &stubTypedContainerUpdater{}
|
||||
bridge.set(stub)
|
||||
if _, err := bridge.TypedContainerUpdate(context.Background(), "docker", strings.Repeat("a", 12), "sha256:"+strings.Repeat("1", 64), nil); err != nil {
|
||||
if err := bridge.TypedContainerUpdatePreflight(context.Background(), "docker", containerID, expectedImageDigest); err != nil {
|
||||
t.Fatalf("bridge preflight with an installed module refused: %v", err)
|
||||
}
|
||||
if stub.preflightCalls != 1 {
|
||||
t.Fatalf("expected one delegated preflight call, got %d", stub.preflightCalls)
|
||||
}
|
||||
if _, err := bridge.TypedContainerUpdate(context.Background(), "docker", containerID, expectedImageDigest, nil); err != nil {
|
||||
t.Fatalf("bridge with an installed module refused: %v", err)
|
||||
}
|
||||
if stub.calls != 1 {
|
||||
|
||||
@@ -2623,6 +2623,18 @@ Agent` secondary handoff against the live setup wizard instead of relying
|
||||
|
||||
## Current State
|
||||
|
||||
### Docker update preflight and execution share one late-bound capability
|
||||
|
||||
The unified agent's late-bound Docker updater now carries the read-only typed
|
||||
container-update preflight and the corresponding execution method in one
|
||||
required interface. The bridge delegates both methods to the running Docker /
|
||||
Podman module and fails closed while that module is unavailable. This prevents
|
||||
the command client from advertising a false `agent_capability_unavailable`
|
||||
refusal merely because the process-lifetime bridge omitted the preflight
|
||||
method, and makes future bridge drift a compile-time failure. The bridge proof
|
||||
in `cmd/pulse-agent/main_test.go` covers unavailable, invalid-candidate, and
|
||||
connected-module states for both preflight and execution.
|
||||
|
||||
### Monitoring task limits do not change lifecycle authority
|
||||
|
||||
The monitoring-owned process-wide scheduled-task limiter may bound host and
|
||||
|
||||
@@ -690,6 +690,16 @@ tokens, and path-normalization variants.
|
||||
|
||||
## Current State
|
||||
|
||||
### Docker update preflight remains read-only across the unified-agent bridge
|
||||
|
||||
The `pulse-agent` late-bound Docker updater delegates the typed, read-only
|
||||
container-update preflight through the same required capability as execution.
|
||||
The preflight checks runtime identity, container identity, and expected image
|
||||
digest without mutating the workload; adding it to the bridge restores the
|
||||
existing approval boundary and grants no new command, inventory, environment,
|
||||
mount, file, process, or guest-inspection authority. An unavailable Docker /
|
||||
Podman module continues to fail closed.
|
||||
|
||||
### Historical credential containment is prerelease-blocking
|
||||
|
||||
The `historical-credential-containment` release gate now owns the cross-repo
|
||||
|
||||
@@ -751,11 +751,10 @@ func (c *CommandClient) preflightDockerLifecycle(ctx context.Context, payload ag
|
||||
}
|
||||
|
||||
func (c *CommandClient) preflightDockerUpdate(ctx context.Context, payload agentexec.DockerContainerUpdatePayload) (bool, string) {
|
||||
preflighter, ok := c.dockerUpdater.(DockerContainerUpdatePreflighter)
|
||||
if !ok || preflighter == nil {
|
||||
if c.dockerUpdater == nil {
|
||||
return false, agentexec.ActionRefusalCapabilityUnavailable
|
||||
}
|
||||
if err := preflighter.TypedContainerUpdatePreflight(ctx, payload.Runtime, payload.ContainerID, payload.ExpectedImageDigest); err != nil {
|
||||
if err := c.dockerUpdater.TypedContainerUpdatePreflight(ctx, payload.Runtime, payload.ContainerID, payload.ExpectedImageDigest); err != nil {
|
||||
return false, agentexec.ActionPreflightReasonCode(err, agentexec.ActionRefusalTargetPreconditionFailed)
|
||||
}
|
||||
return true, ""
|
||||
|
||||
@@ -160,6 +160,10 @@ type stubDockerUpdater struct {
|
||||
calls *int
|
||||
}
|
||||
|
||||
func (stubDockerUpdater) TypedContainerUpdatePreflight(context.Context, string, string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s stubDockerUpdater) TypedContainerUpdate(_ context.Context, _, _, _ string, _ func(string)) (agentexec.DockerContainerUpdateOutcome, error) {
|
||||
if s.calls != nil {
|
||||
*s.calls++
|
||||
|
||||
@@ -18,11 +18,8 @@ import (
|
||||
// missing, runtime mismatch, preflight drift) as opposed to update failures,
|
||||
// which arrive inside the outcome.
|
||||
type DockerContainerUpdater interface {
|
||||
TypedContainerUpdate(ctx context.Context, runtime, containerID, expectedImageDigest string, progress func(string)) (agentexec.DockerContainerUpdateOutcome, error)
|
||||
}
|
||||
|
||||
type DockerContainerUpdatePreflighter interface {
|
||||
TypedContainerUpdatePreflight(ctx context.Context, runtime, containerID, expectedImageDigest string) error
|
||||
TypedContainerUpdate(ctx context.Context, runtime, containerID, expectedImageDigest string, progress func(string)) (agentexec.DockerContainerUpdateOutcome, error)
|
||||
}
|
||||
|
||||
func (c *CommandClient) handleDockerContainerUpdate(ctx context.Context, conn *websocket.Conn, payload agentexec.DockerContainerUpdatePayload) {
|
||||
|
||||
Reference in New Issue
Block a user