mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
653 lines
29 KiB
Go
653 lines
29 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.DockerContainerObservationSnapshot
|
|
ReceivedAt time.Time
|
|
}
|
|
|
|
type dockerContainerPostconditionObserver interface {
|
|
ObserveDockerContainer(context.Context, string, string, string, string) (dockerContainerPostconditionObservation, error)
|
|
}
|
|
|
|
type dockerContainerObservationCommander interface {
|
|
ObserveDockerContainer(context.Context, string, agentexec.DockerContainerObservationPayload) (*agentexec.DockerContainerObservationResultPayload, error)
|
|
}
|
|
|
|
type agentDockerContainerPostconditionObserver struct {
|
|
commander dockerContainerObservationCommander
|
|
}
|
|
|
|
func (o agentDockerContainerPostconditionObserver) ObserveDockerContainer(ctx context.Context, actionID, agentID, runtime, containerID string) (dockerContainerPostconditionObservation, error) {
|
|
result, err := o.commander.ObserveDockerContainer(ctx, agentID, agentexec.DockerContainerObservationPayload{
|
|
ActionID: actionID, Runtime: runtime, ContainerID: containerID,
|
|
})
|
|
if err != nil {
|
|
return dockerContainerPostconditionObservation{}, err
|
|
}
|
|
if result == nil {
|
|
return dockerContainerPostconditionObservation{}, fmt.Errorf("docker daemon observation unavailable")
|
|
}
|
|
if !result.Observed {
|
|
return dockerContainerPostconditionObservation{}, fmt.Errorf("docker daemon observation unavailable: %s", strings.TrimSpace(result.ReasonCode))
|
|
}
|
|
return dockerContainerPostconditionObservation{
|
|
ObserverID: "docker-daemon:" + agentID, TrustDomain: "docker-daemon:" + agentID,
|
|
Method: "typed_docker_daemon_observation", Snapshot: result.Snapshot, ReceivedAt: time.Now().UTC(),
|
|
}, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
executor := dockerContainerActionExecutor{resources: resources, agents: agents}
|
|
if commander, ok := agents.(dockerContainerObservationCommander); ok {
|
|
executor.observer = agentDockerContainerPostconditionObserver{commander: commander}
|
|
}
|
|
return executor
|
|
}
|
|
|
|
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, agentID, runtime, 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, agentID, runtime, 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, attempt.AgentID, e.dockerObservationRuntime(ctx, record), 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, attempt.AgentID, e.dockerObservationRuntime(ctx, record), 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) dockerObservationRuntime(ctx context.Context, record unified.ActionAuditRecord) string {
|
|
if e.resources != nil {
|
|
if registry, err := e.resources.buildRegistry(GetOrgID(ctx)); err == nil {
|
|
if resource, ok := registry.Get(record.Request.ResourceID); ok && resource != nil {
|
|
if runtime, runtimeErr := dockerContainerRuntime(*resource); runtimeErr == nil {
|
|
return runtime
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Older durable attempts did not retain runtime separately. Docker is the
|
|
// compatibility default; a Podman target with no current resource fails
|
|
// the observation safely and retains agent-attested truth.
|
|
return "docker"
|
|
}
|
|
|
|
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))
|
|
}
|
|
agentID, connectErr := e.connectedDockerCommandAgentID(ctx, resource)
|
|
if connectErr != nil {
|
|
readiness := unavailableDockerActionReadiness(operation, "command_agent_disconnected", "Docker / Podman command agent is not connected.")
|
|
readiness.Detail = connectErr.Error()
|
|
return readiness
|
|
}
|
|
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: no agent command server is configured")
|
|
}
|
|
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
|
|
}
|
|
// Telemetry retains its last-seen token ID across enrollment-token rotation.
|
|
// A token miss may therefore use the live admission only when its tenant,
|
|
// agent ID, and hostname all match this resource. Never degrade this to an
|
|
// ID-only or hostname-only lookup: either could select a different identity.
|
|
if tokenID := strings.TrimSpace(resource.Docker.TokenID); tokenID != "" {
|
|
if agentID, ok := commandAgentForIdentity(ctx, e.agents, resource.Docker.AgentID, resource.Docker.Hostname); ok {
|
|
return strings.TrimSpace(agentID), nil
|
|
}
|
|
return "", fmt.Errorf("docker container command agent is not connected: no live command session matches enrollment token %s or the exact agent %q and hostname %q identity", tokenID, strings.TrimSpace(resource.Docker.AgentID), strings.TrimSpace(resource.Docker.Hostname))
|
|
}
|
|
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: no live command session for agent %q or hostname %q", strings.TrimSpace(resource.Docker.AgentID), strings.TrimSpace(resource.Docker.Hostname))
|
|
}
|
|
|
|
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, "'", "'\"'\"'") + "'"
|
|
}
|