Files
pulse/internal/api/docker_container_action_executor.go
T
2026-08-14 01:12:49 +01:00

599 lines
26 KiB
Go

package api
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/rcourtman/pulse-go-rewrite/internal/actionlifecycle"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/safety"
"github.com/rcourtman/pulse-go-rewrite/internal/operationreceipt"
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
const (
dockerContainerLifecycleHandler = "docker.container.lifecycle"
dockerContainerUpdateHandler = "docker.container.update"
)
type dockerContainerActionExecutor struct {
resources *ResourceHandlers
agents actionAgentCommander
observer dockerContainerPostconditionObserver
}
type dockerContainerPostconditionObservation struct {
ObserverID string
TrustDomain string
Method string
Snapshot agentexec.DockerContainerLifecycleSnapshot
ReceivedAt time.Time
}
type dockerContainerPostconditionObserver interface {
ObserveDockerContainer(context.Context, string, string) (dockerContainerPostconditionObservation, error)
}
type dockerContainerLifecycleAgentCommander interface {
ExecuteDockerContainerLifecycle(context.Context, string, agentexec.DockerContainerLifecyclePayload) (*agentexec.DockerContainerLifecycleResultPayload, error)
}
type dockerContainerUpdateAgentCommander interface {
ExecuteDockerContainerUpdate(context.Context, string, agentexec.DockerContainerUpdatePayload) (*agentexec.DockerContainerUpdateResultPayload, error)
}
func isDockerContainerUpdateOperation(operation string) bool {
return strings.TrimSpace(operation) == "update"
}
// dockerContainerOperationHandler returns the internal handler a capability
// must advertise for the requested operation, so an update request can never
// ride a lifecycle capability or vice versa.
func dockerContainerOperationHandler(operation string) string {
if isDockerContainerUpdateOperation(operation) {
return dockerContainerUpdateHandler
}
return dockerContainerLifecycleHandler
}
func (e dockerContainerActionExecutor) BindActionDispatch(ctx context.Context, record unified.ActionAuditRecord, attempt unified.ActionDispatchAttempt) (unified.ActionDispatchAttempt, error) {
if isDockerContainerUpdateOperation(record.Request.CapabilityName) {
resource, err := e.currentDockerContainerResource(ctx, record.Request.ResourceID, record.Request.CapabilityName)
if err != nil {
return unified.ActionDispatchAttempt{}, err
}
runtime, err := dockerContainerRuntime(resource)
if err != nil {
return unified.ActionDispatchAttempt{}, err
}
agentID, err := e.connectedDockerCommandAgentID(ctx, resource)
if err != nil {
return unified.ActionDispatchAttempt{}, err
}
req, err := dockerContainerUpdateRequest(attempt.ID, record.ID, runtime, resource)
if err != nil {
return unified.ActionDispatchAttempt{}, err
}
return unified.BindActionDispatchAttempt(attempt, unified.ActionDispatchBinding{OperationKind: req.Operation, OperationVersion: req.OperationVersion, RequestDigest: req.RequestDigest, AgentID: agentID})
}
operation, err := dockerAgentLifecycleOperation(record.Request.CapabilityName)
if err != nil {
return unified.ActionDispatchAttempt{}, err
}
resource, err := e.currentDockerContainerResource(ctx, record.Request.ResourceID, record.Request.CapabilityName)
if err != nil {
return unified.ActionDispatchAttempt{}, err
}
runtime, err := dockerContainerRuntime(resource)
if err != nil {
return unified.ActionDispatchAttempt{}, err
}
agentID, err := e.connectedDockerCommandAgentID(ctx, resource)
if err != nil {
return unified.ActionDispatchAttempt{}, err
}
req := dockerContainerLifecycleRequest(attempt.ID, record.ID, operation, runtime, resource)
if err := agentexec.BindDockerContainerLifecyclePayload(&req); err != nil {
return unified.ActionDispatchAttempt{}, err
}
return unified.BindActionDispatchAttempt(attempt, unified.ActionDispatchBinding{OperationKind: req.Operation, OperationVersion: req.OperationVersion, RequestDigest: req.RequestDigest, AgentID: agentID})
}
func newDockerContainerActionExecutor(resources *ResourceHandlers, agents actionAgentCommander) ActionExecutor {
if resources == nil || agents == nil {
return nil
}
return dockerContainerActionExecutor{resources: resources, agents: agents}
}
func (e dockerContainerActionExecutor) ActionHandlerNames() []string {
return []string{dockerContainerLifecycleHandler, dockerContainerUpdateHandler}
}
func (e dockerContainerActionExecutor) ActionDispatchOperationKinds() []string {
return []string{
agentexec.DockerContainerOperationStart,
agentexec.DockerContainerOperationStop,
agentexec.DockerContainerOperationRestart,
agentexec.DockerContainerOperationUpdate,
}
}
func (e dockerContainerActionExecutor) ExecuteAction(ctx context.Context, record unified.ActionAuditRecord) (*unified.ExecutionResult, error) {
attempt, ok := actionlifecycle.DispatchAttemptFromContext(ctx)
if !ok || attempt.ActionID != record.ID {
return nil, fmt.Errorf("committed action dispatch authority is required")
}
record, err := unified.NormalizeActionAuditRecord(record)
if err != nil {
return nil, err
}
operation := strings.TrimSpace(record.Request.CapabilityName)
if isDockerContainerUpdateOperation(operation) {
return e.executeDockerContainerUpdate(ctx, record, attempt)
}
if !isDockerContainerLifecycleOperation(operation) {
return nil, fmt.Errorf("unsupported docker container lifecycle capability %q", operation)
}
resource, err := e.currentDockerContainerResource(ctx, record.Request.ResourceID, operation)
if err != nil {
return nil, err
}
runtime, err := dockerContainerRuntime(resource)
if err != nil {
return nil, err
}
if dockerContainerRef(resource) == "" {
return nil, fmt.Errorf("docker container resource %q has no executable container id", record.Request.ResourceID)
}
agentID, err := e.connectedDockerCommandAgentID(ctx, resource)
if err != nil {
return nil, err
}
typedAgents, ok := e.agents.(dockerContainerLifecycleAgentCommander)
if !ok {
return nil, fmt.Errorf("typed docker container lifecycle agent is unavailable")
}
agentOperation, err := dockerAgentLifecycleOperation(operation)
if err != nil {
return nil, err
}
req := dockerContainerLifecycleRequest(attempt.ID, record.ID, agentOperation, runtime, resource)
if err := agentexec.BindDockerContainerLifecyclePayload(&req); err != nil {
return nil, err
}
if agentexec.DockerContainerLifecycleOperationIdentity(agentID, req) != (operationreceipt.Identity{AttemptID: attempt.ID, ActionID: attempt.ActionID, OperationKind: attempt.OperationKind, OperationVersion: attempt.OperationVersion, RequestDigest: attempt.RequestDigest, AgentID: attempt.AgentID}) {
return nil, fmt.Errorf("docker container lifecycle dispatch binding drift")
}
result, err := typedAgents.ExecuteDockerContainerLifecycle(agentCommandContext(ctx), agentID, req)
if err != nil {
return nil, err
}
if result == nil {
return nil, fmt.Errorf("docker container lifecycle agent returned no result")
}
receivedAt := time.Now().UTC()
if err := agentexec.ValidateDockerContainerLifecycleResultForRequest(req, *result); err != nil {
return nil, fmt.Errorf("invalid docker container lifecycle agent result: %w", err)
}
var independent *dockerContainerPostconditionObservation
if e.observer != nil {
if observation, observeErr := e.observer.ObserveDockerContainer(ctx, record.ID, req.ContainerID); observeErr == nil {
independent = &observation
}
}
return dockerContainerExecutionResult(record.Request.ResourceID, agentID, req, *result, independent, receivedAt)
}
func (e dockerContainerActionExecutor) executeDockerContainerUpdate(ctx context.Context, record unified.ActionAuditRecord, attempt unified.ActionDispatchAttempt) (*unified.ExecutionResult, error) {
operation := strings.TrimSpace(record.Request.CapabilityName)
resource, err := e.currentDockerContainerResource(ctx, record.Request.ResourceID, operation)
if err != nil {
return nil, err
}
runtime, err := dockerContainerRuntime(resource)
if err != nil {
return nil, err
}
if dockerContainerRef(resource) == "" {
return nil, fmt.Errorf("docker container resource %q has no executable container id", record.Request.ResourceID)
}
agentID, err := e.connectedDockerCommandAgentID(ctx, resource)
if err != nil {
return nil, err
}
typedAgents, ok := e.agents.(dockerContainerUpdateAgentCommander)
if !ok {
return nil, fmt.Errorf("typed docker container update agent is unavailable")
}
req, err := dockerContainerUpdateRequest(attempt.ID, record.ID, runtime, resource)
if err != nil {
return nil, err
}
if agentexec.DockerContainerUpdateOperationIdentity(agentID, req) != (operationreceipt.Identity{AttemptID: attempt.ID, ActionID: attempt.ActionID, OperationKind: attempt.OperationKind, OperationVersion: attempt.OperationVersion, RequestDigest: attempt.RequestDigest, AgentID: attempt.AgentID}) {
return nil, fmt.Errorf("docker container update dispatch binding drift")
}
result, err := typedAgents.ExecuteDockerContainerUpdate(agentCommandContext(ctx), agentID, req)
if err != nil {
return nil, err
}
if result == nil {
return nil, fmt.Errorf("docker container update agent returned no result")
}
receivedAt := time.Now().UTC()
if err := agentexec.ValidateDockerContainerUpdateResultForRequest(req, *result); err != nil {
return nil, fmt.Errorf("invalid docker container update agent result: %w", err)
}
var independent *dockerContainerPostconditionObservation
if e.observer != nil && result.NewContainerID != "" {
if observation, observeErr := e.observer.ObserveDockerContainer(ctx, record.ID, result.NewContainerID); observeErr == nil {
independent = &observation
}
}
return dockerContainerUpdateExecutionResult(record.Request.ResourceID, agentID, *result, independent, receivedAt)
}
func dockerContainerUpdateRequest(attemptID, actionID, runtime string, resource unified.Resource) (agentexec.DockerContainerUpdatePayload, error) {
expectedDigest := ""
if resource.Docker != nil && resource.Docker.UpdateStatus != nil {
expectedDigest = strings.ToLower(strings.TrimSpace(resource.Docker.UpdateStatus.CurrentDigest))
}
if expectedDigest == "" {
return agentexec.DockerContainerUpdatePayload{}, fmt.Errorf("resource %q has no current image digest to bind the update against", resource.ID)
}
req := agentexec.DockerContainerUpdatePayload{
RequestID: attemptID, ActionID: actionID, Runtime: runtime,
ContainerID: dockerContainerRef(resource), ExpectedImageDigest: expectedDigest,
}
if err := agentexec.BindDockerContainerUpdatePayload(&req); err != nil {
return agentexec.DockerContainerUpdatePayload{}, err
}
return req, nil
}
func (e dockerContainerActionExecutor) ReconcileActionDispatch(ctx context.Context, record unified.ActionAuditRecord, attempt unified.ActionDispatchAttempt) (*unified.ExecutionResult, unified.ActionDispatchReceipt, bool, error) {
querier, ok := e.agents.(operationReceiptQuerier)
if !ok {
return nil, unified.ActionDispatchReceipt{}, false, nil
}
identity := operationreceipt.Identity{AttemptID: attempt.ID, ActionID: attempt.ActionID, OperationKind: attempt.OperationKind, OperationVersion: attempt.OperationVersion, RequestDigest: attempt.RequestDigest, AgentID: attempt.AgentID}
query, err := querier.QueryAgentOperation(agentCommandContext(ctx), attempt.AgentID, identity)
if err != nil {
return nil, unified.ActionDispatchReceipt{}, false, err
}
receivedAt := time.Now().UTC()
if query.Status != operationreceipt.QueryFoundTerminal {
if !strandedActionDispatchClassifiable(query.Status) {
return nil, unified.ActionDispatchReceipt{}, false, nil
}
if err := agentexec.ValidateOperationQueryResultForIdentity(query, identity, receivedAt); err != nil {
return nil, unified.ActionDispatchReceipt{}, false, err
}
result, receipt, stranded := strandedActionDispatch(query, record, attempt, receivedAt)
return result, receipt, stranded, nil
}
if err := agentexec.ValidateOperationQueryResultForIdentity(query, identity, receivedAt); err != nil {
return nil, unified.ActionDispatchReceipt{}, false, err
}
if attempt.OperationKind == agentexec.DockerContainerOperationUpdate {
result, err := agentexec.DecodeDockerContainerUpdateResultPayload(query.Record.Result)
if err != nil {
return nil, unified.ActionDispatchReceipt{}, false, err
}
var independent *dockerContainerPostconditionObservation
if e.observer != nil && result.NewContainerID != "" {
if observation, observeErr := e.observer.ObserveDockerContainer(ctx, record.ID, result.NewContainerID); observeErr == nil {
independent = &observation
}
}
execution, err := dockerContainerUpdateExecutionResult(record.Request.ResourceID, attempt.AgentID, result, independent, receivedAt)
if err != nil {
return nil, unified.ActionDispatchReceipt{}, false, err
}
receipt := unified.ActionDispatchReceipt{AttemptID: attempt.ID, ActionID: record.ID, TransportRequestID: attempt.ID, ReceivedAt: receivedAt}
return execution, receipt, true, nil
}
result, err := agentexec.DecodeDockerContainerLifecycleResultPayload(query.Record.Result)
if err != nil {
return nil, unified.ActionDispatchReceipt{}, false, err
}
req := agentexec.DockerContainerLifecyclePayload{RequestID: attempt.ID, ActionID: record.ID, Operation: attempt.OperationKind, OperationVersion: attempt.OperationVersion, RequestDigest: attempt.RequestDigest, Runtime: "docker", ContainerID: result.ContainerID, ExpectedState: result.Before.State, ExpectedStartedAt: result.Before.StartedAt}
var independent *dockerContainerPostconditionObservation
if e.observer != nil {
if observation, observeErr := e.observer.ObserveDockerContainer(ctx, record.ID, result.ContainerID); observeErr == nil {
independent = &observation
}
}
execution, err := dockerContainerExecutionResult(record.Request.ResourceID, attempt.AgentID, req, result, independent, receivedAt)
if err != nil {
return nil, unified.ActionDispatchReceipt{}, false, err
}
receipt := unified.ActionDispatchReceipt{AttemptID: attempt.ID, ActionID: record.ID, TransportRequestID: attempt.ID, ReceivedAt: receivedAt}
return execution, receipt, true, nil
}
func (e dockerContainerActionExecutor) CheckActionAvailable(ctx context.Context, req unified.ActionRequest, resource unified.Resource) unified.ResourceActionReadiness {
operation := strings.TrimSpace(req.CapabilityName)
capability, ok := findDockerLifecycleCapability(resource.Capabilities, operation)
if !ok || capability.InternalHandler != dockerContainerOperationHandler(operation) {
return unified.ResourceActionReadiness{}
}
readiness := unified.ResourceActionReadiness{Name: operation, Available: true}
if e.agents == nil {
return unavailableDockerActionReadiness(operation, "command_agent_unavailable", "Docker / Podman command execution is not available.")
}
if isDockerContainerUpdateOperation(operation) {
if _, ok := e.agents.(dockerContainerUpdateAgentCommander); !ok {
return unavailableDockerActionReadiness(operation, "typed_operation_unavailable", "Typed Docker / Podman update execution is not available.")
}
} else if _, ok := e.agents.(dockerContainerLifecycleAgentCommander); !ok {
return unavailableDockerActionReadiness(operation, "typed_operation_unavailable", "Typed Docker / Podman lifecycle execution is not available.")
}
if _, err := e.executableDockerContainerResource(ctx, resource, operation); err != nil {
return unavailableDockerActionReadiness(operation, dockerActionUnavailableReasonCode(err), dockerActionUnavailableReason(err))
}
if _, err := e.connectedDockerCommandAgentID(ctx, resource); err != nil {
return unavailableDockerActionReadiness(operation, "command_agent_disconnected", "Docker / Podman command agent is not connected.")
}
agentID, _ := e.connectedDockerCommandAgentID(ctx, resource)
if liveAgentOperationReceiptVersion(ctx, e.agents, agentID) != operationreceipt.ProtocolVersion {
return unavailableDockerActionReadiness(operation, "operation_receipt_unsupported", "The Pulse agent on this host cannot run reviewed actions: it is on an older version, or its durable state directory is unavailable. Update the agent, or check the agent logs if it is already current, then retry.")
}
return readiness
}
func (e dockerContainerActionExecutor) CheckActionFeasible(ctx context.Context, actionID string, req unified.ActionRequest, resource unified.Resource) unified.ResourceActionReadiness {
if readiness := e.CheckActionAvailable(ctx, req, resource); readiness.Name == "" || !readiness.Available {
return readiness
}
commander, ok := e.agents.(actionPreflightAgentCommander)
if !ok {
return unified.ResourceActionReadiness{}
}
runtime, err := dockerContainerRuntime(resource)
if err != nil {
return actionPreflightReadiness(req.CapabilityName, nil, err)
}
agentID, err := e.connectedDockerCommandAgentID(ctx, resource)
if err != nil {
return actionPreflightReadiness(req.CapabilityName, nil, err)
}
requestID := uuid.NewString()
preflight := agentexec.ActionPreflightPayload{RequestID: requestID, ProtocolVersion: agentexec.ActionPreflightProtocolVersion}
if isDockerContainerUpdateOperation(req.CapabilityName) {
typed, bindErr := dockerContainerUpdateRequest(requestID, actionID, runtime, resource)
if bindErr != nil {
return actionPreflightReadiness(req.CapabilityName, nil, bindErr)
}
preflight.DockerUpdate = &typed
} else {
operation, operationErr := dockerAgentLifecycleOperation(req.CapabilityName)
if operationErr != nil {
return actionPreflightReadiness(req.CapabilityName, nil, operationErr)
}
typed := dockerContainerLifecycleRequest(requestID, actionID, operation, runtime, resource)
if bindErr := agentexec.BindDockerContainerLifecyclePayload(&typed); bindErr != nil {
return actionPreflightReadiness(req.CapabilityName, nil, bindErr)
}
preflight.DockerLifecycle = &typed
}
result, err := commander.PreflightAction(agentCommandContext(ctx), agentID, preflight)
return actionPreflightReadiness(req.CapabilityName, result, err)
}
func (e dockerContainerActionExecutor) currentDockerContainerResource(ctx context.Context, resourceID, operation string) (unified.Resource, error) {
if e.resources == nil {
return unified.Resource{}, fmt.Errorf("resource handler unavailable")
}
registry, err := e.resources.buildRegistry(GetOrgID(ctx))
if err != nil {
return unified.Resource{}, err
}
resource, ok := registry.Get(resourceID)
if !ok || resource == nil {
return unified.Resource{}, fmt.Errorf("resource %q is no longer present", resourceID)
}
return e.executableDockerContainerResource(ctx, *resource, operation)
}
func (e dockerContainerActionExecutor) executableDockerContainerResource(_ context.Context, resource unified.Resource, operation string) (unified.Resource, error) {
if resource.Type != unified.ResourceTypeAppContainer || resource.Docker == nil {
return unified.Resource{}, fmt.Errorf("resource %q is not a Docker or Podman container", resource.ID)
}
if status := resource.SourceStatus[unified.SourceDocker]; strings.EqualFold(strings.TrimSpace(status.Status), "stale") || strings.EqualFold(strings.TrimSpace(status.Status), "offline") || strings.EqualFold(strings.TrimSpace(status.Status), "missing") {
return unified.Resource{}, fmt.Errorf("docker inventory for resource %q is %s", resource.ID, status.Status)
}
if resource.Docker.Security != nil && resource.Docker.Security.MutatingCommandsBlocked {
reason := strings.TrimSpace(resource.Docker.Security.MutatingCommandsBlockedReason)
if reason == "" {
reason = "Docker daemon-mutating commands are disabled for this host"
}
return unified.Resource{}, fmt.Errorf("docker container lifecycle action is blocked by host policy: %s", reason)
}
if capability, ok := findDockerLifecycleCapability(resource.Capabilities, operation); !ok {
return unified.Resource{}, fmt.Errorf("resource %q does not currently advertise %s capability", resource.ID, operation)
} else if capability.InternalHandler != dockerContainerOperationHandler(operation) {
return unified.Resource{}, fmt.Errorf("resource %q advertises %s through unsupported handler %q", resource.ID, operation, capability.InternalHandler)
}
return resource, nil
}
func (e dockerContainerActionExecutor) connectedDockerCommandAgentID(ctx context.Context, resource unified.Resource) (string, error) {
if e.agents == nil {
return "", fmt.Errorf("docker container command agent is not connected")
}
if resource.Docker == nil {
return "", fmt.Errorf("docker resource metadata missing")
}
if agentID, ok := commandAgentForToken(ctx, e.agents, resource.Docker.TokenID); ok {
return strings.TrimSpace(agentID), nil
}
// When telemetry names a token, it is the immutable session binding. Do
// not fall back to a different identity or hostname after rotation.
if strings.TrimSpace(resource.Docker.TokenID) != "" {
return "", fmt.Errorf("docker container command agent is not connected")
}
if agentID := strings.TrimSpace(resource.Docker.AgentID); agentID != "" && isAgentCommandConnected(ctx, e.agents, agentID) {
return agentID, nil
}
if agentID, ok := commandAgentForHost(ctx, e.agents, strings.TrimSpace(resource.Docker.Hostname)); ok {
agentID = strings.TrimSpace(agentID)
if agentID != "" && isAgentCommandConnected(ctx, e.agents, agentID) {
return agentID, nil
}
}
return "", fmt.Errorf("docker container command agent is not connected")
}
func unavailableDockerActionReadiness(operation, reasonCode, reason string) unified.ResourceActionReadiness {
return unified.ResourceActionReadiness{
Name: strings.TrimSpace(operation),
Available: false,
ReasonCode: strings.TrimSpace(reasonCode),
Reason: strings.TrimSpace(reason),
}
}
func dockerActionUnavailableReasonCode(err error) string {
if err == nil {
return "unavailable"
}
message := strings.ToLower(err.Error())
switch {
case strings.Contains(message, "not a docker or podman container"):
return "unsupported_resource"
case strings.Contains(message, "inventory") && (strings.Contains(message, "stale") || strings.Contains(message, "offline") || strings.Contains(message, "missing")):
return "stale_inventory"
case strings.Contains(message, "blocked by host policy"):
return "host_policy_blocked"
case strings.Contains(message, "does not currently advertise"):
return "capability_unavailable"
case strings.Contains(message, "unsupported handler"):
return "unsupported_handler"
default:
return "unavailable"
}
}
func dockerActionUnavailableReason(err error) string {
switch dockerActionUnavailableReasonCode(err) {
case "unsupported_resource":
return "Resource is not a Docker or Podman container."
case "stale_inventory":
return "Docker / Podman inventory is not fresh enough to run lifecycle actions."
case "host_policy_blocked":
return "Docker / Podman host policy blocks mutating lifecycle actions."
case "capability_unavailable":
return "Pulse does not currently advertise a fresh command capability for this container."
case "unsupported_handler":
return "This container action is not routed through the supported lifecycle executor."
default:
return "Docker / Podman lifecycle action is not currently available."
}
}
func findDockerLifecycleCapability(capabilities []unified.ResourceCapability, operation string) (unified.ResourceCapability, bool) {
operation = strings.TrimSpace(operation)
for _, capability := range capabilities {
if strings.TrimSpace(capability.Name) == operation {
return capability, true
}
}
return unified.ResourceCapability{}, false
}
func dockerContainerRuntime(resource unified.Resource) (string, error) {
if resource.Docker == nil {
return "", fmt.Errorf("docker resource metadata missing")
}
switch strings.ToLower(strings.TrimSpace(firstNonEmpty(resource.Docker.Runtime, resource.Technology))) {
case "docker":
return "docker", nil
case "podman":
return "podman", nil
default:
return "", fmt.Errorf("unsupported container runtime %q", firstNonEmpty(resource.Docker.Runtime, resource.Technology))
}
}
func dockerContainerRef(resource unified.Resource) string {
if resource.Docker == nil {
return ""
}
if ref := strings.TrimSpace(resource.Docker.ContainerID); ref != "" {
return ref
}
return ""
}
func dockerAgentLifecycleOperation(operation string) (string, error) {
switch strings.TrimSpace(operation) {
case "start", agentexec.DockerContainerOperationStart:
return agentexec.DockerContainerOperationStart, nil
case "stop", agentexec.DockerContainerOperationStop:
return agentexec.DockerContainerOperationStop, nil
case "restart", agentexec.DockerContainerOperationRestart:
return agentexec.DockerContainerOperationRestart, nil
default:
return "", fmt.Errorf("unsupported docker container lifecycle capability %q", operation)
}
}
func dockerContainerLifecycleRequest(attemptID, actionID, operation, runtime string, resource unified.Resource) agentexec.DockerContainerLifecyclePayload {
startedAt := time.Time{}
if resource.Docker != nil && resource.Docker.StartedAt != nil {
startedAt = resource.Docker.StartedAt.UTC()
}
return agentexec.DockerContainerLifecyclePayload{
RequestID: attemptID, ActionID: actionID, Operation: operation, Runtime: runtime,
ContainerID: dockerContainerRef(resource), ExpectedState: strings.ToLower(strings.TrimSpace(resource.Docker.ContainerState)), ExpectedStartedAt: startedAt, Timeout: 120,
}
}
func isDockerContainerLifecycleOperation(operation string) bool {
switch strings.TrimSpace(operation) {
case "start", "stop", "restart":
return true
default:
return false
}
}
func commandOutput(result *agentexec.CommandResultPayload) string {
if result == nil {
return ""
}
parts := []string{}
if stdout := strings.TrimSpace(result.Stdout); stdout != "" {
parts = append(parts, stdout)
}
if stderr := strings.TrimSpace(result.Stderr); stderr != "" {
parts = append(parts, stderr)
}
if errText := strings.TrimSpace(result.Error); errText != "" {
parts = append(parts, errText)
}
return strings.Join(parts, "\n")
}
func redactActionOutput(output string) string {
output = strings.TrimSpace(output)
if output == "" {
return ""
}
redacted, n := safety.RedactSensitiveText(output)
if n > 0 {
return strings.TrimSpace(redacted) + fmt.Sprintf("\n\n[redacted %d sensitive value(s)]", n)
}
return output
}
func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}