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 networkTestService(
17 t *testing.T,
18 state *outmocks.MockAppState,
19 runtime *outmocks.MockContainerRuntime,
20 networks NetworkConfig,
21 limits ResourceLimits,
22) *Service {
23 t.Helper()
24 return NewService(Deps{State: state, Runtime: runtime, Networks: networks, Limits: limits}, zerowrap.Default())
25}
26
27// TestCreateAndStart_UsesIncarnationPrivateNetworkAndLimits proves the
28// created container joins the app's incarnation-owned network (never the
29// runtime default) and carries the configured resource limits.
30func TestCreateAndStart_UsesIncarnationPrivateNetworkAndLimits(t *testing.T) {
31 ctx := context.Background()
32 state := outmocks.NewMockAppState(t)
33 runtime := outmocks.NewMockContainerRuntime(t)
34
35 private := domain.AppPrivateNetworkName("gordon", "app-1")
36 state.EXPECT().LoadOwnership(mock.Anything, "blog").
37 Return(domain.AppOwnership{App: "blog", ID: "app-1"}, nil).Once()
38 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{}, nil).Once()
39 runtime.EXPECT().CreateNetwork(mock.Anything, private, mock.MatchedBy(func(cfg domain.NetworkConfig) bool {
40 return cfg.Internal && domain.NetworkOwnedBy(cfg.Labels, domain.AppPrivateNetworkLabels("blog", "app-1"))
41 })).Return(nil).Once()
42 runtime.EXPECT().CreateContainer(mock.Anything, mock.MatchedBy(func(cfg *domain.ContainerConfig) bool {
43 return cfg.NetworkMode == private &&
44 cfg.MemoryLimit == int64(1)<<30 &&
45 cfg.NanoCPUs == 2_000_000_000 &&
46 cfg.PidsLimit == 256 &&
47 assert.Equal(t, []string{"web"}, cfg.Aliases) &&
48 assert.Equal(t, "web", cfg.Hostname)
49 })).Return(&domain.Container{ID: "c-1", Name: "web"}, nil).Once()
50 runtime.EXPECT().StartContainer(mock.Anything, "c-1").Return(nil).Once()
51
52 svc := networkTestService(t, state, runtime,
53 NetworkConfig{Prefix: "gordon", Internal: true},
54 ResourceLimits{MemoryBytes: 1 << 30, NanoCPUs: 2_000_000_000, PidsLimit: 256},
55 )
56 created, binds, udp, err := svc.createAndStart(ctx, "blog", "rev-1",
57 pinnedService{name: "web", spec: domain.AppService{Name: "web"}}, "op-1", nil)
58 require.NoError(t, err)
59 require.NotNil(t, created)
60 assert.Empty(t, binds)
61 assert.Empty(t, udp)
62}
63
64// TestCreateAndStart_ReusesOwnedNetwork proves recovery reattaches the
65// already-owned incarnation network instead of creating or replacing it.
66func TestCreateAndStart_ReusesOwnedNetwork(t *testing.T) {
67 ctx := context.Background()
68 state := outmocks.NewMockAppState(t)
69 runtime := outmocks.NewMockContainerRuntime(t)
70
71 private := domain.AppPrivateNetworkName("gordon", "app-1")
72 state.EXPECT().LoadOwnership(mock.Anything, "blog").
73 Return(domain.AppOwnership{App: "blog", ID: "app-1"}, nil).Once()
74 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{
75 {Name: private, Labels: domain.AppPrivateNetworkLabels("blog", "app-1")},
76 }, nil).Once()
77 runtime.EXPECT().CreateContainer(mock.Anything, mock.Anything).
78 Return(&domain.Container{ID: "c-1", Name: "web"}, nil).Once()
79 runtime.EXPECT().StartContainer(mock.Anything, "c-1").Return(nil).Once()
80
81 svc := networkTestService(t, state, runtime, NetworkConfig{Prefix: "gordon"}, ResourceLimits{})
82 _, _, _, err := svc.createAndStart(ctx, "blog", "rev-1",
83 pinnedService{name: "web", spec: domain.AppService{Name: "web"}}, "op-1", nil)
84 require.NoError(t, err)
85}
86
87// TestCreateAndStart_RefusesForeignNetwork proves an unowned network with
88// the derived name fails closed before any container is created.
89func TestCreateAndStart_RefusesForeignNetwork(t *testing.T) {
90 ctx := context.Background()
91 state := outmocks.NewMockAppState(t)
92 runtime := outmocks.NewMockContainerRuntime(t)
93
94 private := domain.AppPrivateNetworkName("gordon", "app-1")
95 state.EXPECT().LoadOwnership(mock.Anything, "blog").
96 Return(domain.AppOwnership{App: "blog", ID: "app-1"}, nil).Once()
97 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{
98 {Name: private, Labels: map[string]string{domain.LabelManaged: "true"}},
99 }, nil).Once()
100
101 svc := networkTestService(t, state, runtime, NetworkConfig{Prefix: "gordon"}, ResourceLimits{})
102 _, _, _, err := svc.createAndStart(ctx, "blog", "rev-1",
103 pinnedService{name: "web", spec: domain.AppService{Name: "web"}}, "op-1", nil)
104 require.ErrorIs(t, err, domain.ErrAppStateConflict)
105}
106
107// TestCreateAndStart_JoinsDeclaredSharedNetworks proves a service is
108// connected only to the shared networks its manifest declares.
109func TestCreateAndStart_JoinsDeclaredSharedNetworks(t *testing.T) {
110 ctx := context.Background()
111 state := outmocks.NewMockAppState(t)
112 runtime := outmocks.NewMockContainerRuntime(t)
113
114 private := domain.AppPrivateNetworkName("gordon", "app-1")
115 shared := domain.AppSharedNetworkName("gordon", "database")
116 state.EXPECT().LoadOwnership(mock.Anything, "blog").
117 Return(domain.AppOwnership{App: "blog", ID: "app-1"}, nil).Once()
118 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{}, nil).Once()
119 runtime.EXPECT().CreateNetwork(mock.Anything, private, mock.Anything).Return(nil).Once()
120 runtime.EXPECT().CreateNetwork(mock.Anything, shared, mock.MatchedBy(func(cfg domain.NetworkConfig) bool {
121 return domain.NetworkOwnedBy(cfg.Labels, domain.AppSharedNetworkLabels("database"))
122 })).Return(nil).Once()
123 runtime.EXPECT().CreateContainer(mock.Anything, mock.Anything).
124 Return(&domain.Container{ID: "c-1", Name: "web"}, nil).Once()
125 runtime.EXPECT().ConnectContainerToNetwork(mock.Anything, "c-1", shared).Return(nil).Once()
126 runtime.EXPECT().StartContainer(mock.Anything, "c-1").Return(nil).Once()
127
128 svc := networkTestService(t, state, runtime, NetworkConfig{Prefix: "gordon"}, ResourceLimits{})
129 _, _, _, err := svc.createAndStart(ctx, "blog", "rev-1", pinnedService{
130 name: "web",
131 spec: domain.AppService{Name: "web"},
132 sharedNetworks: []domain.AppSharedNetwork{{Network: "database", Services: []string{"web"}}},
133 }, "op-1", nil)
134 require.NoError(t, err)
135}
136
137// TestCreateAndStart_RoutesDeclaredReadOnlyVolumeToReadOnlyMount proves a
138// manifest volume declared read-only reaches the runtime config as a
139// read-only mount instead of a writable one.
140func TestCreateAndStart_RoutesDeclaredReadOnlyVolumeToReadOnlyMount(t *testing.T) {
141 ctx := context.Background()
142 state := outmocks.NewMockAppState(t)
143 runtime := outmocks.NewMockContainerRuntime(t)
144
145 private := domain.AppPrivateNetworkName("gordon", "app-1")
146 runtimeName := domain.RuntimeVolumeName("blog", "web", "config")
147 state.EXPECT().LoadOwnership(mock.Anything, "blog").Return(domain.AppOwnership{App: "blog", ID: "app-1"}, nil)
148 state.EXPECT().SaveOwnership(mock.Anything, mock.Anything).Return(nil)
149 runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{}, nil).Once()
150 runtime.EXPECT().CreateNetwork(mock.Anything, private, mock.Anything).Return(nil).Once()
151 runtime.EXPECT().CreateVolume(mock.Anything, runtimeName, mock.Anything).Return(nil).Once()
152 runtime.EXPECT().CreateContainer(mock.Anything, mock.MatchedBy(func(cfg *domain.ContainerConfig) bool {
153 _, readOnly := cfg.ReadOnlyVolumes["/config"]
154 _, writable := cfg.Volumes["/config"]
155 return readOnly && !writable && cfg.ReadOnlyVolumes["/config"] == runtimeName
156 })).Return(&domain.Container{ID: "c-1", Name: "web"}, nil).Once()
157 runtime.EXPECT().StartContainer(mock.Anything, "c-1").Return(nil).Once()
158
159 svc := networkTestService(t, state, runtime, NetworkConfig{Prefix: "gordon"}, ResourceLimits{})
160 _, _, _, err := svc.createAndStart(ctx, "blog", "rev-1", pinnedService{
161 name: "web",
162 spec: domain.AppService{
163 Name: "web",
164 Volumes: []domain.AppVolume{{Name: "config", Path: "/config", ReadOnly: true}},
165 },
166 }, "op-1", nil)
167 require.NoError(t, err)
168}
169
170// TestCreateAndStart_DistinctAppsNeverShareNetwork proves two apps receive
171// different incarnation networks, neither of which is a runtime default.
172func TestCreateAndStart_DistinctAppsNeverShareNetwork(t *testing.T) {
173 networks := map[string]string{}
174 for _, tc := range []struct{ app, id string }{{"blog", "app-1"}, {"shop", "app-2"}} {
175 private := domain.AppPrivateNetworkName("gordon", tc.id)
176 require.NotEqual(t, "default", private)
177 require.NotEqual(t, "bridge", private)
178 networks[tc.app] = private
179 }
180 assert.NotEqual(t, networks["blog"], networks["shop"])
181}