mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
204 lines
9.5 KiB
Go
204 lines
9.5 KiB
Go
package agentexec
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/operationreceipt"
|
|
)
|
|
|
|
const (
|
|
DockerContainerUpdateOperationVersion = 1
|
|
DockerContainerUpdateReceiptKind = "pulse.docker_container_update_result"
|
|
DockerContainerUpdateReceiptVersion = 1
|
|
|
|
maxDockerContainerNameLength = 256
|
|
maxDockerImageDigestLength = 256
|
|
// Image pulls dominate update time; the module's own overall budget is 15
|
|
// minutes, so the transport bound leaves headroom without being unbounded.
|
|
maxDockerContainerUpdateTimeoutSeconds = 1800
|
|
defaultDockerContainerUpdateTimeoutSeconds = 900
|
|
)
|
|
|
|
func DecodeDockerContainerUpdatePayload(data []byte) (DockerContainerUpdatePayload, error) {
|
|
var payload DockerContainerUpdatePayload
|
|
if err := decodeStrictDockerLifecycle(data, &payload); err != nil {
|
|
return DockerContainerUpdatePayload{}, err
|
|
}
|
|
if err := ValidateDockerContainerUpdatePayload(&payload); err != nil {
|
|
return DockerContainerUpdatePayload{}, err
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func DecodeDockerContainerUpdateResultPayload(data []byte) (DockerContainerUpdateResultPayload, error) {
|
|
var payload DockerContainerUpdateResultPayload
|
|
if err := decodeStrictDockerLifecycle(data, &payload); err != nil {
|
|
return DockerContainerUpdateResultPayload{}, err
|
|
}
|
|
if err := ValidateDockerContainerUpdateResultPayload(&payload); err != nil {
|
|
return DockerContainerUpdateResultPayload{}, err
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func BindDockerContainerUpdatePayload(payload *DockerContainerUpdatePayload) error {
|
|
if payload == nil {
|
|
return fmt.Errorf("docker container update payload is required")
|
|
}
|
|
payload.Operation = DockerContainerOperationUpdate
|
|
payload.OperationVersion = DockerContainerUpdateOperationVersion
|
|
digest, err := dockerContainerUpdateRequestDigest(*payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload.RequestDigest = digest
|
|
return nil
|
|
}
|
|
|
|
func dockerContainerUpdateRequestDigest(payload DockerContainerUpdatePayload) (string, error) {
|
|
return operationreceipt.DigestCanonicalJSON(struct {
|
|
ActionID string `json:"action_id"`
|
|
Operation string `json:"operation"`
|
|
OperationVersion int `json:"operation_version"`
|
|
Runtime string `json:"runtime"`
|
|
ContainerID string `json:"container_id"`
|
|
ExpectedImageDigest string `json:"expected_image_digest"`
|
|
}{
|
|
strings.TrimSpace(payload.ActionID), strings.TrimSpace(payload.Operation), payload.OperationVersion,
|
|
strings.ToLower(strings.TrimSpace(payload.Runtime)), strings.ToLower(strings.TrimSpace(payload.ContainerID)),
|
|
strings.ToLower(strings.TrimSpace(payload.ExpectedImageDigest)),
|
|
})
|
|
}
|
|
|
|
func ValidateDockerContainerUpdatePayload(payload *DockerContainerUpdatePayload) error {
|
|
if payload == nil {
|
|
return fmt.Errorf("docker container update payload is required")
|
|
}
|
|
payload.RequestID = strings.TrimSpace(payload.RequestID)
|
|
payload.ActionID = strings.TrimSpace(payload.ActionID)
|
|
payload.Operation = strings.TrimSpace(payload.Operation)
|
|
payload.Runtime = strings.ToLower(strings.TrimSpace(payload.Runtime))
|
|
payload.ContainerID = strings.ToLower(strings.TrimSpace(payload.ContainerID))
|
|
payload.ExpectedImageDigest = strings.ToLower(strings.TrimSpace(payload.ExpectedImageDigest))
|
|
if payload.RequestID == "" || len(payload.RequestID) > maxRequestIDLength || payload.ActionID == "" || len(payload.ActionID) > maxRequestIDLength {
|
|
return fmt.Errorf("invalid docker update request or action id")
|
|
}
|
|
if payload.Operation != DockerContainerOperationUpdate {
|
|
return fmt.Errorf("unsupported docker container update operation %q", payload.Operation)
|
|
}
|
|
if payload.OperationVersion != DockerContainerUpdateOperationVersion {
|
|
return fmt.Errorf("unsupported docker container update operation version %d", payload.OperationVersion)
|
|
}
|
|
if payload.Runtime != "docker" && payload.Runtime != "podman" {
|
|
return fmt.Errorf("unsupported container runtime %q", payload.Runtime)
|
|
}
|
|
if !dockerContainerIDPattern.MatchString(payload.ContainerID) {
|
|
return fmt.Errorf("container id must be an immutable hexadecimal id")
|
|
}
|
|
if !hostUpdateInventoryHashPattern.MatchString(payload.ExpectedImageDigest) {
|
|
return fmt.Errorf("invalid docker update expected image digest")
|
|
}
|
|
expectedDigest, err := dockerContainerUpdateRequestDigest(*payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if payload.RequestDigest != expectedDigest {
|
|
return fmt.Errorf("docker container update request digest mismatch")
|
|
}
|
|
if payload.Timeout < 0 || payload.Timeout > maxDockerContainerUpdateTimeoutSeconds {
|
|
return fmt.Errorf("docker container update timeout must be between 0 and %d seconds", maxDockerContainerUpdateTimeoutSeconds)
|
|
}
|
|
if payload.Timeout == 0 {
|
|
payload.Timeout = defaultDockerContainerUpdateTimeoutSeconds
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateDockerContainerUpdateResultPayload(result *DockerContainerUpdateResultPayload) error {
|
|
if result == nil {
|
|
return fmt.Errorf("docker container update result is required")
|
|
}
|
|
result.RequestID = strings.TrimSpace(result.RequestID)
|
|
result.ActionID = strings.TrimSpace(result.ActionID)
|
|
result.Operation = strings.TrimSpace(result.Operation)
|
|
result.RequestDigest = strings.TrimSpace(result.RequestDigest)
|
|
result.ContainerID = strings.ToLower(strings.TrimSpace(result.ContainerID))
|
|
result.NewContainerID = strings.ToLower(strings.TrimSpace(result.NewContainerID))
|
|
result.ContainerName = strings.TrimSpace(result.ContainerName)
|
|
result.OldImageDigest = strings.TrimSpace(result.OldImageDigest)
|
|
result.NewImageDigest = strings.TrimSpace(result.NewImageDigest)
|
|
result.BackupContainer = strings.TrimSpace(result.BackupContainer)
|
|
result.ExecutionPhase = strings.TrimSpace(result.ExecutionPhase)
|
|
result.ReasonCode = strings.TrimSpace(result.ReasonCode)
|
|
result.Error = strings.TrimSpace(result.Error)
|
|
if result.RequestID == "" || len(result.RequestID) > maxRequestIDLength || result.ActionID == "" || len(result.ActionID) > maxRequestIDLength {
|
|
return fmt.Errorf("invalid docker update result identity")
|
|
}
|
|
if result.Operation != DockerContainerOperationUpdate {
|
|
return fmt.Errorf("unsupported docker update result operation %q", result.Operation)
|
|
}
|
|
if result.OperationVersion != DockerContainerUpdateOperationVersion || !dockerContainerIDPattern.MatchString(result.ContainerID) || !hostUpdateInventoryHashPattern.MatchString(result.RequestDigest) {
|
|
return fmt.Errorf("invalid docker update result binding")
|
|
}
|
|
if result.ExecutionPhase != DockerContainerPhasePreflight && result.ExecutionPhase != DockerContainerPhaseMutate && result.ExecutionPhase != DockerContainerPhaseVerify && result.ExecutionPhase != DockerContainerPhaseComplete {
|
|
return fmt.Errorf("unsupported docker update execution phase %q", result.ExecutionPhase)
|
|
}
|
|
if result.NewContainerID != "" && !dockerContainerIDPattern.MatchString(result.NewContainerID) {
|
|
return fmt.Errorf("docker update result has invalid replacement container id")
|
|
}
|
|
if len(result.Error) > 1024 || len(result.ContainerName) > maxDockerContainerNameLength || len(result.BackupContainer) > maxDockerContainerNameLength {
|
|
return fmt.Errorf("docker update result exceeds bounded contract")
|
|
}
|
|
if result.ReasonCode != "" && !IsActionRefusalReasonCode(result.ReasonCode) {
|
|
return fmt.Errorf("invalid docker update refusal reason code")
|
|
}
|
|
if result.ReasonCode != "" && result.MutationStarted {
|
|
return fmt.Errorf("docker update refusal reason conflicts with mutation state")
|
|
}
|
|
if len(result.OldImageDigest) > maxDockerImageDigestLength || len(result.NewImageDigest) > maxDockerImageDigestLength {
|
|
return fmt.Errorf("docker update result digest exceeds bounded contract")
|
|
}
|
|
if result.MutationCompleted && !result.MutationStarted {
|
|
return fmt.Errorf("completed docker update mutation requires mutation start")
|
|
}
|
|
if result.RolledBack && !result.RollbackAttempted {
|
|
return fmt.Errorf("docker update rollback success requires a rollback attempt")
|
|
}
|
|
if result.RollbackAttempted && !result.MutationStarted {
|
|
return fmt.Errorf("docker update rollback requires mutation start")
|
|
}
|
|
if result.ReadbackRan && result.After.ObservedAt.IsZero() {
|
|
return fmt.Errorf("docker update readback requires an observation")
|
|
}
|
|
if result.ExecutionPhase == DockerContainerPhaseComplete {
|
|
if result.Error != "" || !result.MutationCompleted || result.NewContainerID == "" {
|
|
return fmt.Errorf("complete docker update requires a replacement container and no error")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DockerContainerUpdateOperationIdentity(agentID string, payload DockerContainerUpdatePayload) operationreceipt.Identity {
|
|
return operationreceipt.Identity{AttemptID: payload.RequestID, ActionID: payload.ActionID, OperationKind: payload.Operation, OperationVersion: payload.OperationVersion, RequestDigest: payload.RequestDigest, AgentID: strings.TrimSpace(agentID)}
|
|
}
|
|
|
|
func ValidateDockerContainerUpdateResultForRequest(req DockerContainerUpdatePayload, result DockerContainerUpdateResultPayload) error {
|
|
if err := ValidateDockerContainerUpdatePayload(&req); err != nil {
|
|
return err
|
|
}
|
|
if err := ValidateDockerContainerUpdateResultPayload(&result); err != nil {
|
|
return err
|
|
}
|
|
if result.RequestID != req.RequestID || result.ActionID != req.ActionID || result.Operation != req.Operation || result.OperationVersion != req.OperationVersion || result.RequestDigest != req.RequestDigest {
|
|
return fmt.Errorf("docker update result identity mismatch")
|
|
}
|
|
if result.ContainerID != req.ContainerID {
|
|
return fmt.Errorf("docker update result container mismatch")
|
|
}
|
|
if result.After.ContainerID != "" && result.NewContainerID != "" && !strings.EqualFold(result.After.ContainerID, result.NewContainerID) {
|
|
return fmt.Errorf("docker update after-state container mismatch")
|
|
}
|
|
return nil
|
|
}
|