main bac21c6

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close