v3.0.0 f19f453

live_network_probe_test.go RawHistoryFolder

1//go:build live
2
3// Live bounded-network-readiness verification against a real Docker
4// daemon. These tests are opt-in: build with `-tags live` and run with the
5// daemon reachable. They create and remove their own networks and
6// containers, and assert no helper is ever left behind.
7//
8//	go test -tags live ./internal/adapters/out/docker/ -run TestLiveNetworkProbe -count=1 -v
9
10package docker
11
12import (
13	"context"
14	"testing"
15	"time"
16
17	"github.com/stretchr/testify/assert"
18	"github.com/stretchr/testify/require"
19
20	"github.com/bnema/gordon/internal/domain"
21)
22
23// liveProbeNetwork creates an owned private network for one probe scenario.
24func liveProbeNetwork(t *testing.T, runtime *Runtime, ctx context.Context) string {
25	t.Helper()
26	name := liveName("gordon-live-probe")
27	require.NoError(t, runtime.CreateNetwork(ctx, name, domain.NetworkConfig{
28		Driver: "bridge", Labels: domain.AppPrivateNetworkLabels("live", name),
29	}))
30	t.Cleanup(func() {
31		cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second)
32		defer cancel()
33		_ = runtime.RemoveNetwork(cleanup, name)
34	})
35	return name
36}
37
38// liveProbeHelpers counts leftover bounded-readiness helpers.
39func liveProbeHelpers(t *testing.T, runtime *Runtime, ctx context.Context) int {
40	t.Helper()
41	containers, err := runtime.ListContainers(ctx, true)
42	require.NoError(t, err)
43	count := 0
44	for _, c := range containers {
45		if c != nil && c.Labels[domain.LabelPurpose] == domain.PurposeNetworkProbe {
46			count++
47		}
48	}
49	return count
50}
51
52// liveProbeTarget starts one listener container on the network.
53func liveProbeTarget(t *testing.T, runtime *Runtime, ctx context.Context, network, listenScript string) *domain.Container {
54	t.Helper()
55	return liveCreate(t, runtime, ctx, &domain.ContainerConfig{
56		Image:       liveImage,
57		Name:        liveName("live-probe-target"),
58		NetworkMode: network,
59		Entrypoint:  []string{"sh", "-c", listenScript},
60		AutoRemove:  false,
61	})
62}
63
64// TestLiveNetworkProbe_PeerFacingReadyAndCleanedUp proves a published-free
65// internal listener answers a bounded probe, the helper is removed, and the
66// target exposes no host port.
67func TestLiveNetworkProbe_PeerFacingReadyAndCleanedUp(t *testing.T) {
68	runtime, ctx := liveRuntime(t)
69	network := liveProbeNetwork(t, runtime, ctx)
70	target := liveProbeTarget(t, runtime, ctx, network,
71		`while true; do printf "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok" | nc -l -p 8080; done`)
72
73	before := liveProbeHelpers(t, runtime, ctx)
74	inspect, err := runtime.InspectContainer(ctx, target.ID)
75	require.NoError(t, err)
76	require.False(t, inspect.StartedAt.IsZero(), "the target must report an execution start")
77	assert.Empty(t, inspect.Ports, "an internal listener must not publish a host port")
78
79	result, err := runtime.ProbeContainerNetwork(ctx, domain.ContainerNetworkProbeRequest{
80		TargetContainerID: target.ID,
81		ExpectedStartedAt: inspect.StartedAt,
82		Network:           network,
83		Protocol:          domain.ProbeProtocolHTTP,
84		Port:              8080,
85		Path:              "/healthz",
86		Timeout:           10 * time.Second,
87	})
88	require.NoError(t, err)
89	assert.True(t, result.Ready)
90	assert.Equal(t, 200, result.Status)
91
92	assert.Equal(t, before, liveProbeHelpers(t, runtime, ctx), "the helper must always be removed")
93}
94
95// TestLiveNetworkProbe_LoopbackOnlyListenerFails proves a target listening
96// only on its own loopback never reports ready from the network.
97func TestLiveNetworkProbe_LoopbackOnlyListenerFails(t *testing.T) {
98	runtime, ctx := liveRuntime(t)
99	network := liveProbeNetwork(t, runtime, ctx)
100	target := liveProbeTarget(t, runtime, ctx, network,
101		`while true; do printf "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok" | nc -l -p 8081 -s 127.0.0.1; done`)
102
103	inspect, err := runtime.InspectContainer(ctx, target.ID)
104	require.NoError(t, err)
105
106	result, err := runtime.ProbeContainerNetwork(ctx, domain.ContainerNetworkProbeRequest{
107		TargetContainerID: target.ID,
108		ExpectedStartedAt: inspect.StartedAt,
109		Network:           network,
110		Protocol:          domain.ProbeProtocolHTTP,
111		Port:              8081,
112		Path:              "/",
113		Timeout:           5 * time.Second,
114	})
115	require.NoError(t, err)
116	assert.False(t, result.Ready, "a loopback-only listener must never be reachable over the private network")
117	assert.NotEmpty(t, result.Diagnostic)
118}
119
120// TestLiveNetworkProbe_TCPReady proves a TCP readiness session accepts an
121// open port and rejects a closed one.
122func TestLiveNetworkProbe_TCPReady(t *testing.T) {
123	runtime, ctx := liveRuntime(t)
124	network := liveProbeNetwork(t, runtime, ctx)
125	target := liveProbeTarget(t, runtime, ctx, network,
126		`while true; do nc -l -p 5432 >/dev/null 2>&1; done`)
127
128	inspect, err := runtime.InspectContainer(ctx, target.ID)
129	require.NoError(t, err)
130
131	ready, err := runtime.ProbeContainerNetwork(ctx, domain.ContainerNetworkProbeRequest{
132		TargetContainerID: target.ID,
133		ExpectedStartedAt: inspect.StartedAt,
134		Network:           network,
135		Protocol:          domain.ProbeProtocolTCP,
136		Port:              5432,
137		Timeout:           5 * time.Second,
138	})
139	require.NoError(t, err)
140	assert.True(t, ready.Ready)
141
142	closed, err := runtime.ProbeContainerNetwork(ctx, domain.ContainerNetworkProbeRequest{
143		TargetContainerID: target.ID,
144		ExpectedStartedAt: inspect.StartedAt,
145		Network:           network,
146		Protocol:          domain.ProbeProtocolTCP,
147		Port:              5499,
148		Timeout:           2 * time.Second,
149	})
150	require.NoError(t, err)
151	assert.False(t, closed.Ready)
152}
153
154// TestLiveNetworkProbe_RejectsStaleGeneration proves a probe whose expected
155// execution start no longer matches the target fails instead of reporting
156// readiness for a generation that restarted.
157func TestLiveNetworkProbe_RejectsStaleGeneration(t *testing.T) {
158	runtime, ctx := liveRuntime(t)
159	network := liveProbeNetwork(t, runtime, ctx)
160	target := liveProbeTarget(t, runtime, ctx, network,
161		`while true; do printf "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok" | nc -l -p 8080; done`)
162
163	_, err := runtime.ProbeContainerNetwork(ctx, domain.ContainerNetworkProbeRequest{
164		TargetContainerID: target.ID,
165		ExpectedStartedAt: time.Now().Add(-time.Hour).UTC(),
166		Network:           network,
167		Protocol:          domain.ProbeProtocolHTTP,
168		Port:              8080,
169		Path:              "/",
170		Timeout:           5 * time.Second,
171	})
172	require.Error(t, err)
173	assert.ErrorIs(t, err, domain.ErrAppStateConflict)
174}
175
176// TestLiveNetworkProbe_NonHTTPGreetingIsNotReady proves a listener whose
177// first line merely starts with 2 or 3 (a non-HTTP service on a
178// misdirected port) is never reported ready.
179func TestLiveNetworkProbe_NonHTTPGreetingIsNotReady(t *testing.T) {
180	runtime, ctx := liveRuntime(t)
181	network := liveProbeNetwork(t, runtime, ctx)
182	target := liveProbeTarget(t, runtime, ctx, network,
183		`while true; do printf '300 ready\r\n' | nc -l -p 8083; done`)
184
185	inspect, err := runtime.InspectContainer(ctx, target.ID)
186	require.NoError(t, err)
187
188	result, err := runtime.ProbeContainerNetwork(ctx, domain.ContainerNetworkProbeRequest{
189		TargetContainerID: target.ID,
190		ExpectedStartedAt: inspect.StartedAt,
191		Network:           network,
192		Protocol:          domain.ProbeProtocolHTTP,
193		Port:              8083,
194		Path:              "/",
195		Timeout:           5 * time.Second,
196	})
197	require.NoError(t, err)
198	assert.False(t, result.Ready, "a non-HTTP greeting must never satisfy HTTP readiness")
199}
200
201// TestLiveNetworkProbe_HungTargetIsUnhealthyNotInfrastructure proves a
202// target that accepts the connection but never answers yields an unhealthy
203// result rather than an infrastructure error: the retry loop must keep
204// polling instead of aborting the wait.
205func TestLiveNetworkProbe_HungTargetIsUnhealthyNotInfrastructure(t *testing.T) {
206	runtime, ctx := liveRuntime(t)
207	network := liveProbeNetwork(t, runtime, ctx)
208	target := liveProbeTarget(t, runtime, ctx, network,
209		`while true; do sleep 3600 | nc -l -p 8084; done`)
210
211	inspect, err := runtime.InspectContainer(ctx, target.ID)
212	require.NoError(t, err)
213
214	result, err := runtime.ProbeContainerNetwork(ctx, domain.ContainerNetworkProbeRequest{
215		TargetContainerID: target.ID,
216		ExpectedStartedAt: inspect.StartedAt,
217		Network:           network,
218		Protocol:          domain.ProbeProtocolHTTP,
219		Port:              8084,
220		Path:              "/",
221		Timeout:           4 * time.Second,
222	})
223	require.NoError(t, err, "a hanging target is a readiness failure, not an infrastructure error")
224	assert.False(t, result.Ready)
225	assert.NotEmpty(t, result.Diagnostic)
226}
227
228// TestLiveNetworkProbe_RejectsForeignNetwork proves the helper is bound to
229// exactly one network: a target attached elsewhere is never probed across
230// networks.
231func TestLiveNetworkProbe_RejectsForeignNetwork(t *testing.T) {
232	runtime, ctx := liveRuntime(t)
233	ownerNetwork := liveProbeNetwork(t, runtime, ctx)
234	foreignNetwork := liveProbeNetwork(t, runtime, ctx)
235	target := liveProbeTarget(t, runtime, ctx, ownerNetwork,
236		`while true; do printf "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok" | nc -l -p 8080; done`)
237
238	before := liveProbeHelpers(t, runtime, ctx)
239	inspect, err := runtime.InspectContainer(ctx, target.ID)
240	require.NoError(t, err)
241
242	_, err = runtime.ProbeContainerNetwork(ctx, domain.ContainerNetworkProbeRequest{
243		TargetContainerID: target.ID,
244		ExpectedStartedAt: inspect.StartedAt,
245		Network:           foreignNetwork,
246		Protocol:          domain.ProbeProtocolHTTP,
247		Port:              8080,
248		Path:              "/",
249		Timeout:           5 * time.Second,
250	})
251	require.Error(t, err)
252	assert.ErrorIs(t, err, domain.ErrAppStateConflict)
253
254	assert.Equal(t, before, liveProbeHelpers(t, runtime, ctx), "no helper may survive a rejected probe")
255}

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 8

Tags 106

↑↓ moveenter openesc close