v3.1.0 d676137

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
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	candidate, 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, candidate.Container)
60	assert.Empty(t, candidate.TCPBinds)
61	assert.Empty(t, candidate.UDPBinds)
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}

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