Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions internal/deploy/controlled_session_cleanup_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import (
// selection a session watchdog may receive. It is derived from durable
// ownership rather than accepting later resource choices.
//
// Networks and volumes are explicit arrays even while controlled sessions do
// not create either resource. Future slices may populate them only after their
// exact identities become part of durable ownership.
// Networks and volumes are explicit arrays. Network entries carry the exact
// immutable identity selected by durable ownership; volumes remain reserved
// for a later resource slice.
type ControlledSessionCleanupManifest struct {
LiveRunID string `json:"live_run_id"`
BootSession string `json:"boot_session"`
Expand All @@ -25,7 +25,7 @@ type ControlledSessionCleanupManifest struct {
IncidentReceipt string `json:"incident_receipt"`
Controller ControlledSessionContainerOwnershipV1 `json:"controller"`
Workload ControlledSessionContainerOwnershipV1 `json:"workload"`
Networks []string `json:"networks"`
Networks []ControlledSessionNetworkOwnershipV1 `json:"networks"`
Volumes []string `json:"volumes"`
}

Expand All @@ -40,12 +40,18 @@ func ControlledSessionCleanupManifestFromOwnership(ownership ControlledSessionOw
if err != nil {
return ControlledSessionCleanupManifest{}, err
}
networks := []ControlledSessionNetworkOwnershipV1{}
if ownership.NetworkName != "" {
networks = append(networks, ControlledSessionNetworkOwnershipV1{
Role: ControlledSessionNetworkRoleV1, ID: ownership.NetworkID, Name: ownership.NetworkName,
})
}
manifest := ControlledSessionCleanupManifest{
LiveRunID: ownership.LiveRunID, BootSession: ownership.BootSession,
DockerEndpoint: ownership.DockerEndpoint,
ChannelDirectory: ownership.ChannelDirectory, IncidentReceipt: receiptPath,
Controller: ownership.Controller, Workload: ownership.Workload,
Networks: []string{}, Volumes: []string{},
Networks: networks, Volumes: []string{},
}
if err := ValidateControlledSessionCleanupManifest(manifest); err != nil {
return ControlledSessionCleanupManifest{}, err
Expand Down Expand Up @@ -87,8 +93,13 @@ func ValidateControlledSessionCleanupManifest(manifest ControlledSessionCleanupM
if manifest.Networks == nil || manifest.Volumes == nil {
return fmt.Errorf("controlled-session cleanup manifest networks and volumes must use arrays")
}
if err := validateControlledSessionCleanupResourceIDs(manifest.Networks, "network"); err != nil {
return err
if len(manifest.Networks) > 1 {
return fmt.Errorf("controlled-session cleanup manifest may name only one network")
}
for index, network := range manifest.Networks {
if err := validateControlledSessionNetworkOwnershipV1(network); err != nil {
return fmt.Errorf("controlled-session cleanup manifest network %d: %w", index, err)
}
}
if err := validateControlledSessionCleanupResourceIDs(manifest.Volumes, "volume"); err != nil {
return err
Expand Down
28 changes: 24 additions & 4 deletions internal/deploy/controlled_session_cleanup_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@ import (
func TestControlledSessionCleanupManifestDerivesExactDurableOwnership(t *testing.T) {
ownership := controlledSessionOwnershipFixtureV1(t.TempDir(), "run-0000000000000001", "reploy/env/workload:g-current")
ownership.BootSession = "boot-session"
ownership.NetworkID = strings.Repeat("d", 64)
ownership.NetworkName = "reploy-session-network"
manifest, err := ControlledSessionCleanupManifestFromOwnership(ownership)
if err != nil {
t.Fatal(err)
}
if manifest.LiveRunID != ownership.LiveRunID || manifest.BootSession != ownership.BootSession ||
manifest.DockerEndpoint != ownership.DockerEndpoint || manifest.ChannelDirectory != ownership.ChannelDirectory || manifest.Controller != ownership.Controller ||
manifest.Workload != ownership.Workload || len(manifest.Networks) != 0 || len(manifest.Volumes) != 0 {
manifest.Workload != ownership.Workload || len(manifest.Networks) != 1 || len(manifest.Volumes) != 0 {
t.Fatalf("cleanup manifest = %#v", manifest)
}
wantNetwork := ControlledSessionNetworkOwnershipV1{
Role: ControlledSessionNetworkRoleV1, ID: ownership.NetworkID, Name: ownership.NetworkName,
}
if manifest.Networks[0] != wantNetwork {
t.Fatalf("cleanup manifest network = %#v, want %#v", manifest.Networks[0], wantNetwork)
}
wantReceipt := filepath.Join(filepath.Dir(filepath.Dir(ownership.ChannelDirectory)), "incidents", ownership.LiveRunID+".json")
if manifest.IncidentReceipt != wantReceipt {
t.Fatalf("incident receipt = %q, want %q", manifest.IncidentReceipt, wantReceipt)
Expand All @@ -37,6 +45,15 @@ func TestControlledSessionCleanupManifestDerivesExactDurableOwnership(t *testing
}
}

func TestControlledSessionCleanupManifestRejectsPlannedNetworkWithoutExactID(t *testing.T) {
ownership := controlledSessionOwnershipFixtureV1(t.TempDir(), "run-0000000000000001", "reploy/env/workload:g-current")
ownership.BootSession = "boot-session"
ownership.NetworkName = "reploy-session-network"
if _, err := ControlledSessionCleanupManifestFromOwnership(ownership); err == nil || !strings.Contains(err.Error(), "network ID") {
t.Fatalf("incomplete network ownership error = %v", err)
}
}

func TestControlledSessionCleanupManifestRejectsRemoteDockerEndpoint(t *testing.T) {
ownership := controlledSessionOwnershipFixtureV1(t.TempDir(), "run-0000000000000001", "reploy/env/workload:g-current")
ownership.BootSession = "boot-session"
Expand Down Expand Up @@ -71,9 +88,12 @@ func TestControlledSessionCleanupManifestRequiresExactChannelAndCanonicalArrays(
if err := ValidateControlledSessionCleanupManifest(manifest); err == nil || !strings.Contains(err.Error(), "must use arrays") {
t.Fatalf("nil resources error = %v", err)
}
manifest.Networks = []string{"network-b", "network-a"}
if err := ValidateControlledSessionCleanupManifest(manifest); err == nil || !strings.Contains(err.Error(), "sorted and unique") {
t.Fatalf("unordered resources error = %v", err)
manifest.Networks = []ControlledSessionNetworkOwnershipV1{
{Role: ControlledSessionNetworkRoleV1, ID: strings.Repeat("a", 64), Name: "network-a"},
{Role: ControlledSessionNetworkRoleV1, ID: strings.Repeat("b", 64), Name: "network-b"},
}
if err := ValidateControlledSessionCleanupManifest(manifest); err == nil || !strings.Contains(err.Error(), "only one network") {
t.Fatalf("multiple network resources error = %v", err)
}
}

Expand Down
31 changes: 31 additions & 0 deletions internal/deploy/controlled_session_incident_receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ type ControlledSessionIncidentContainerV1 struct {
CleanupStatus ControlledSessionIncidentResourceStatusV1 `json:"cleanup_status"`
}

// ControlledSessionIncidentNetworkV1 carries only the immutable network
// identity and its allowlisted cleanup outcome. The ownership labels are the
// receipt live-run ID and the fixed network role.
type ControlledSessionIncidentNetworkV1 struct {
Role string `json:"role"`
ID string `json:"id"`
Name string `json:"name"`
CleanupStatus ControlledSessionIncidentResourceStatusV1 `json:"cleanup_status"`
}

// ControlledSessionIncidentReceiptV1 is the bounded durable evidence written
// by the session watchdog after loss of its parent. The fixed fields cannot
// carry PTY bytes, environment values, secrets, arbitrary logs, or raw Docker
Expand All @@ -72,6 +82,7 @@ type ControlledSessionIncidentReceiptV1 struct {
Trigger ControlledSessionIncidentTriggerV1 `json:"trigger"`
Controller ControlledSessionIncidentContainerV1 `json:"controller"`
Workload ControlledSessionIncidentContainerV1 `json:"workload"`
Networks []ControlledSessionIncidentNetworkV1 `json:"networks,omitempty"`
ChannelCleanupStatus ControlledSessionIncidentResourceStatusV1 `json:"channel_cleanup_status"`
CleanupStatus ControlledSessionIncidentCleanupStatusV1 `json:"cleanup_status"`
RecoveryAction ControlledSessionIncidentRecoveryActionV1 `json:"recovery_action"`
Expand Down Expand Up @@ -346,12 +357,32 @@ func ValidateControlledSessionIncidentReceiptV1(receipt ControlledSessionInciden
if receipt.Controller.ID == receipt.Workload.ID {
return fmt.Errorf("controlled-session incident receipt containers must be different")
}
if len(receipt.Networks) > 1 {
return fmt.Errorf("controlled-session incident receipt may name only one network")
}
for index, network := range receipt.Networks {
if network.Role != ControlledSessionNetworkRoleV1 {
return fmt.Errorf("controlled-session incident receipt network %d role must be %q", index, ControlledSessionNetworkRoleV1)
}
if !controlledSessionContainerIDPatternV1.MatchString(network.ID) {
return fmt.Errorf("controlled-session incident receipt network %d ID must use 64 lowercase hexadecimal characters", index)
}
if !safeRecoveryIdentity(network.Name) {
return fmt.Errorf("controlled-session incident receipt network %d name must be nonempty safe text", index)
}
if err := validateControlledSessionIncidentResourceStatusV1(network.CleanupStatus); err != nil {
return fmt.Errorf("controlled-session incident receipt network %d: %w", index, err)
}
}
if err := validateControlledSessionIncidentResourceStatusV1(receipt.ChannelCleanupStatus); err != nil {
return fmt.Errorf("controlled-session incident receipt channel: %w", err)
}
allSucceeded := receipt.Controller.CleanupStatus == ControlledSessionIncidentResourceVerifiedAbsentV1 &&
receipt.Workload.CleanupStatus == ControlledSessionIncidentResourceVerifiedAbsentV1 &&
receipt.ChannelCleanupStatus == ControlledSessionIncidentResourceVerifiedAbsentV1
for _, network := range receipt.Networks {
allSucceeded = allSucceeded && network.CleanupStatus == ControlledSessionIncidentResourceVerifiedAbsentV1
}
switch receipt.CleanupStatus {
case ControlledSessionIncidentCleanupSucceededV1:
if !allSucceeded || receipt.RecoveryAction != ControlledSessionIncidentRecoveryNoneV1 {
Expand Down
28 changes: 28 additions & 0 deletions internal/deploy/controlled_session_incident_receipt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,30 @@ func TestControlledSessionIncidentReceiptRoundTripHasOnlyAllowlistedFacts(t *tes
}
}

func TestControlledSessionIncidentReceiptValidatesExactNetworkIdentityAndOutcome(t *testing.T) {
for _, test := range []struct {
name string
mutate func(*ControlledSessionIncidentReceiptV1)
want string
}{
{name: "role", mutate: func(value *ControlledSessionIncidentReceiptV1) { value.Networks[0].Role = "other" }, want: "role"},
{name: "ID", mutate: func(value *ControlledSessionIncidentReceiptV1) { value.Networks[0].ID = "short" }, want: "64 lowercase"},
{name: "name", mutate: func(value *ControlledSessionIncidentReceiptV1) { value.Networks[0].Name = "" }, want: "name"},
{name: "outcome", mutate: func(value *ControlledSessionIncidentReceiptV1) { value.Networks[0].CleanupStatus = "unknown" }, want: "cleanup status"},
{name: "overall success", mutate: func(value *ControlledSessionIncidentReceiptV1) {
value.Networks[0].CleanupStatus = ControlledSessionIncidentResourceCleanupFailedV1
}, want: "every resource verified absent"},
} {
t.Run(test.name, func(t *testing.T) {
receipt := controlledSessionIncidentReceiptFixtureV1("run-0000000000000001")
test.mutate(&receipt)
if err := ValidateControlledSessionIncidentReceiptV1(receipt); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("network receipt validation error = %v", err)
}
})
}
}

func TestOperationLockPreparesRetrievesAndAcknowledgesExactIncidentReceipt(t *testing.T) {
dir := t.TempDir()
lock, err := AcquireOperationLock(t.Context(), dir)
Expand Down Expand Up @@ -226,6 +250,10 @@ func controlledSessionIncidentReceiptFixtureV1(runID string) ControlledSessionIn
RecordedAt: time.Date(2026, 8, 10, 3, 0, 0, 0, time.UTC).Format(time.RFC3339Nano),
Trigger: ControlledSessionIncidentParentLostV1,
Controller: container("controller", "c"), Workload: container("workload", "d"),
Networks: []ControlledSessionIncidentNetworkV1{{
Role: ControlledSessionNetworkRoleV1, ID: strings.Repeat("e", 64), Name: "reploy-session-network",
CleanupStatus: ControlledSessionIncidentResourceVerifiedAbsentV1,
}},
ChannelCleanupStatus: ControlledSessionIncidentResourceVerifiedAbsentV1,
CleanupStatus: ControlledSessionIncidentCleanupSucceededV1,
RecoveryAction: ControlledSessionIncidentRecoveryNoneV1,
Expand Down
38 changes: 38 additions & 0 deletions internal/deploy/live_run_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,20 @@ type ControlledSessionOwnershipV1 struct {
SessionHandle string `json:"session_handle"`
DockerEndpoint string `json:"docker_endpoint,omitempty"`
ChannelDirectory string `json:"channel_directory"`
NetworkID string `json:"network_id,omitempty"`
NetworkName string `json:"network_name,omitempty"`
Controller ControlledSessionContainerOwnershipV1 `json:"controller"`
Workload ControlledSessionContainerOwnershipV1 `json:"workload"`
}

const ControlledSessionNetworkRoleV1 = "network"

type ControlledSessionNetworkOwnershipV1 struct {
Role string `json:"role"`
ID string `json:"id"`
Name string `json:"name"`
}

type ControlledSessionContainerOwnershipV1 struct {
Role string `json:"role"`
ID string `json:"id"`
Expand Down Expand Up @@ -299,6 +309,18 @@ func validateControlledSessionOwnershipV1(ownership ControlledSessionOwnershipV1
if !filepath.IsAbs(ownership.ChannelDirectory) || filepath.Clean(ownership.ChannelDirectory) != ownership.ChannelDirectory || !safeRecoveryIdentity(ownership.ChannelDirectory) {
return fmt.Errorf("channel directory must be a clean absolute path")
}
if ownership.NetworkName == "" {
if ownership.NetworkID != "" {
return fmt.Errorf("network ID cannot be recorded without a network name")
}
} else {
if !safeRecoveryIdentity(ownership.NetworkName) {
return fmt.Errorf("network name must be nonempty safe text")
}
if ownership.NetworkID != "" && !controlledSessionContainerIDPatternV1.MatchString(ownership.NetworkID) {
return fmt.Errorf("network ID must use 64 lowercase hexadecimal characters")
}
}
if err := validateControlledSessionContainerOwnershipStateV1(ownership.Controller, "controller"); err != nil {
return fmt.Errorf("controller: %w", err)
}
Expand All @@ -308,12 +330,28 @@ func validateControlledSessionOwnershipV1(ownership ControlledSessionOwnershipV1
if ownership.Controller.ID == "" && ownership.Workload.ID != "" {
return fmt.Errorf("workload container ID cannot be recorded before the controller container ID")
}
if ownership.NetworkName != "" && ownership.NetworkID == "" && (ownership.Controller.ID != "" || ownership.Workload.ID != "") {
return fmt.Errorf("container IDs cannot be recorded before the network ID")
}
if ownership.Controller.ID != "" && ownership.Workload.ID != "" && ownership.Controller.ID == ownership.Workload.ID {
return fmt.Errorf("controller and workload must name different containers")
}
return nil
}

func validateControlledSessionNetworkOwnershipV1(ownership ControlledSessionNetworkOwnershipV1) error {
if ownership.Role != ControlledSessionNetworkRoleV1 {
return fmt.Errorf("role must be %q", ControlledSessionNetworkRoleV1)
}
if !controlledSessionContainerIDPatternV1.MatchString(ownership.ID) {
return fmt.Errorf("network ID must use 64 lowercase hexadecimal characters")
}
if !safeRecoveryIdentity(ownership.Name) {
return fmt.Errorf("network name must be nonempty safe text")
}
return nil
}

func validateCurrentControlledSessionOwnershipV1(ownership ControlledSessionOwnershipV1) error {
if ownership.DockerEndpoint == "" {
return fmt.Errorf("Docker endpoint must be recorded for a new controlled session")
Expand Down
20 changes: 13 additions & 7 deletions internal/deploy/live_run_queue_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ func (lock *OperationLock) RecordLiveRunContainerV1(id string, container string)
}

// RecordControlledSessionOwnershipV1 durably binds the planned resources to an
// active admitted shell and monotonically fills each exact container ID after
// Docker returns it. The boot identity comes from the admitted run already
// protected by this lock.
// active admitted shell and monotonically fills each exact network and
// container ID after Docker returns it. The boot identity comes from the
// admitted run already protected by this lock.
func (lock *OperationLock) RecordControlledSessionOwnershipV1(ownership ControlledSessionOwnershipV1) (ControlledSessionOwnershipV1, error) {
if lock == nil {
return ControlledSessionOwnershipV1{}, fmt.Errorf("record controlled session ownership requires an operation lock")
Expand Down Expand Up @@ -204,6 +204,8 @@ func mergeControlledSessionOwnershipV1(
) (ControlledSessionOwnershipV1, error) {
existingPlan := existing
requestedPlan := requested
existingPlan.NetworkID = ""
requestedPlan.NetworkID = ""
existingPlan.Controller.ID = ""
existingPlan.Workload.ID = ""
requestedPlan.Controller.ID = ""
Expand All @@ -212,21 +214,25 @@ func mergeControlledSessionOwnershipV1(
return ControlledSessionOwnershipV1{}, fmt.Errorf("immutable resource plan changed")
}
merged := existing
mergeID := func(current string, next string, role string) (string, error) {
mergeID := func(current string, next string, resource string) (string, error) {
if next == "" {
return current, nil
}
if current != "" && current != next {
return "", fmt.Errorf("%s container ID changed", role)
return "", fmt.Errorf("%s ID changed", resource)
}
return next, nil
}
var err error
merged.Controller.ID, err = mergeID(existing.Controller.ID, requested.Controller.ID, "controller")
merged.NetworkID, err = mergeID(existing.NetworkID, requested.NetworkID, "network")
if err != nil {
return ControlledSessionOwnershipV1{}, err
}
merged.Workload.ID, err = mergeID(existing.Workload.ID, requested.Workload.ID, "workload")
merged.Controller.ID, err = mergeID(existing.Controller.ID, requested.Controller.ID, "controller container")
if err != nil {
return ControlledSessionOwnershipV1{}, err
}
merged.Workload.ID, err = mergeID(existing.Workload.ID, requested.Workload.ID, "workload container")
if err != nil {
return ControlledSessionOwnershipV1{}, err
}
Expand Down
Loading
Loading