network_reclaim_internal_test.go RawHistoryFolder
1package deployment
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/zerowrap"
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/mock"
10 "github.com/stretchr/testify/require"
11
12 outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
13 "github.com/bnema/gordon/internal/domain"
14)
15
16func reclaimService(t *testing.T, state *outmocks.MockAppState, runtime *outmocks.MockContainerRuntime) *Service {
17 t.Helper()
18 return NewService(Deps{
19 State: state, Runtime: runtime,
20 Networks: NetworkConfig{Prefix: "gordon"},
21 }, zerowrap.Default())
22}
23
24// TestReclaimPrivateNetworks_RemovesOwnedEmptyIncarnationNetwork proves the
25// app's own empty private network is removed immediately, by exact derived
26// name and verified ownership.
27func TestReclaimPrivateNetworks_RemovesOwnedEmptyIncarnationNetwork(t *testing.T) {
28 ctx := context.Background()
29 state := outmocks.NewMockAppState(t)
30 runtime := outmocks.NewMockContainerRuntime(t)
31
32 private := domain.AppPrivateNetworkName("gordon", "app-1")
33 ownership := domain.AppOwnership{
34 App: "blog", ID: "app-1",
35 Networks: []domain.AppOwnedNetwork{{Name: private, Role: domain.AppNetworkRolePrivate}},
36 }
37 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{
38 {Name: private, Labels: domain.AppPrivateNetworkLabels("blog", "app-1")},
39 }, nil).Once()
40 runtime.EXPECT().RemoveNetwork(mock.Anything, private).Return(nil).Once()
41
42 warnings, err := reclaimService(t, state, runtime).reclaimPrivateNetworks(ctx, "blog", ownership)
43 require.NoError(t, err)
44 assert.Empty(t, warnings)
45}
46
47// TestReclaimPrivateNetworks_KeepsSharedNetworks proves a declared shared
48// network is never removed by an app removal.
49func TestReclaimPrivateNetworks_KeepsSharedNetworks(t *testing.T) {
50 ctx := context.Background()
51 state := outmocks.NewMockAppState(t)
52 runtime := outmocks.NewMockContainerRuntime(t)
53
54 private := domain.AppPrivateNetworkName("gordon", "app-1")
55 shared := domain.AppSharedNetworkName("gordon", "shared-backend")
56 ownership := domain.AppOwnership{
57 App: "blog", ID: "app-1",
58 Networks: []domain.AppOwnedNetwork{
59 {Name: private, Role: domain.AppNetworkRolePrivate},
60 {Name: shared, Role: domain.AppNetworkRoleShared},
61 },
62 }
63 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{
64 {Name: private, Labels: domain.AppPrivateNetworkLabels("blog", "app-1")},
65 {Name: shared, Labels: domain.AppSharedNetworkLabels("shared-backend")},
66 }, nil).Once()
67 runtime.EXPECT().RemoveNetwork(mock.Anything, private).Return(nil).Once()
68
69 warnings, err := reclaimService(t, state, runtime).reclaimPrivateNetworks(ctx, "blog", ownership)
70 require.NoError(t, err)
71 assert.Empty(t, warnings)
72 runtime.AssertNotCalled(t, "RemoveNetwork", mock.Anything, shared)
73}
74
75// TestReclaimPrivateNetworks_RefusesForeignOwnership proves a network whose
76// labels do not prove this exact incarnation is left in place and reported,
77// never deleted.
78func TestReclaimPrivateNetworks_RefusesForeignOwnership(t *testing.T) {
79 ctx := context.Background()
80 state := outmocks.NewMockAppState(t)
81 runtime := outmocks.NewMockContainerRuntime(t)
82
83 private := domain.AppPrivateNetworkName("gordon", "app-1")
84 ownership := domain.AppOwnership{
85 App: "blog", ID: "app-1",
86 Networks: []domain.AppOwnedNetwork{{Name: private, Role: domain.AppNetworkRolePrivate}},
87 }
88 // A different incarnation's labels (or a hand-made network) must never
89 // be adopted, let alone deleted.
90 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{
91 {Name: private, Labels: domain.AppPrivateNetworkLabels("blog", "app-99")},
92 }, nil).Once()
93
94 warnings, err := reclaimService(t, state, runtime).reclaimPrivateNetworks(ctx, "blog", ownership)
95 require.NoError(t, err)
96 require.Len(t, warnings, 1)
97 assert.Equal(t, private, warnings[0].Leftover)
98 assert.Contains(t, warnings[0].Detail, "labels")
99 runtime.AssertNotCalled(t, "RemoveNetwork", mock.Anything, mock.Anything)
100}
101
102// TestReclaimPrivateNetworks_RefusesAttachedNetwork proves a network that
103// still has attachments is left in place and reported.
104func TestReclaimPrivateNetworks_RefusesAttachedNetwork(t *testing.T) {
105 ctx := context.Background()
106 state := outmocks.NewMockAppState(t)
107 runtime := outmocks.NewMockContainerRuntime(t)
108
109 private := domain.AppPrivateNetworkName("gordon", "app-1")
110 ownership := domain.AppOwnership{
111 App: "blog", ID: "app-1",
112 Networks: []domain.AppOwnedNetwork{{Name: private, Role: domain.AppNetworkRolePrivate}},
113 }
114 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{
115 {
116 Name: private,
117 Labels: domain.AppPrivateNetworkLabels("blog", "app-1"),
118 Containers: []string{"c-still-there"},
119 },
120 }, nil).Once()
121
122 warnings, err := reclaimService(t, state, runtime).reclaimPrivateNetworks(ctx, "blog", ownership)
123 require.NoError(t, err)
124 require.Len(t, warnings, 1)
125 assert.Contains(t, warnings[0].Detail, "attached containers")
126 runtime.AssertNotCalled(t, "RemoveNetwork", mock.Anything, mock.Anything)
127}
128
129// TestReclaimPrivateNetworks_NeverTouchesAnotherIncarnationName proves a
130// stale ownership entry naming a network this incarnation does not own is
131// ignored: a re-used name must never delete the previous incarnation's
132// network.
133func TestReclaimPrivateNetworks_NeverTouchesAnotherIncarnationName(t *testing.T) {
134 ctx := context.Background()
135 state := outmocks.NewMockAppState(t)
136 runtime := outmocks.NewMockContainerRuntime(t)
137
138 other := domain.AppPrivateNetworkName("gordon", "app-old")
139 ownership := domain.AppOwnership{
140 App: "blog", ID: "app-1",
141 Networks: []domain.AppOwnedNetwork{{Name: other, Role: domain.AppNetworkRolePrivate}},
142 }
143
144 warnings, err := reclaimService(t, state, runtime).reclaimPrivateNetworks(ctx, "blog", ownership)
145 require.NoError(t, err)
146 assert.Empty(t, warnings)
147 runtime.AssertNotCalled(t, "ListNetworks", mock.Anything)
148 runtime.AssertNotCalled(t, "RemoveNetwork", mock.Anything, mock.Anything)
149}
150
151// TestReclaimPrivateNetworks_RuntimeFailureIsRetryable proves a failed
152// removal is reported so the caller keeps app state and the stopped intent
153// for a retry.
154func TestReclaimPrivateNetworks_RuntimeFailureIsRetryable(t *testing.T) {
155 ctx := context.Background()
156 state := outmocks.NewMockAppState(t)
157 runtime := outmocks.NewMockContainerRuntime(t)
158
159 private := domain.AppPrivateNetworkName("gordon", "app-1")
160 ownership := domain.AppOwnership{
161 App: "blog", ID: "app-1",
162 Networks: []domain.AppOwnedNetwork{{Name: private, Role: domain.AppNetworkRolePrivate}},
163 }
164 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{
165 {Name: private, Labels: domain.AppPrivateNetworkLabels("blog", "app-1")},
166 }, nil).Once()
167 runtime.EXPECT().RemoveNetwork(mock.Anything, private).Return(assert.AnError).Once()
168
169 _, err := reclaimService(t, state, runtime).reclaimPrivateNetworks(ctx, "blog", ownership)
170 require.Error(t, err)
171 assert.Contains(t, err.Error(), "remove private network")
172}