network_probe_test.go RawHistoryFolder

1package docker
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"strconv"
8	"strings"
9	"testing"
10	"time"
11
12	"github.com/bnema/zerowrap"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/require"
15
16	"github.com/moby/moby/api/types/container"
17
18	"github.com/bnema/gordon/internal/domain"
19)
20
21// TestBuildNetworkProbeCommand proves the helper argv carries the exact
22// target and clamps the timeout to whole seconds within its bound.
23func TestBuildNetworkProbeCommand(t *testing.T) {
24	t.Run("http carries path, whole-second timeout, and exact endpoint", func(t *testing.T) {
25		command := buildNetworkProbeCommand(domain.ContainerNetworkProbeRequest{
26			Protocol: domain.ProbeProtocolHTTP,
27			Path:     "/healthz",
28			Port:     8080,
29			Timeout:  1500 * time.Millisecond,
30		}, "172.18.0.5")
31		assert.Equal(t, httpProbeScript, command.script)
32		assert.Equal(t, []string{"/healthz", "1", "172.18.0.5", "8080"}, command.args)
33	})
34
35	t.Run("tcp carries no path", func(t *testing.T) {
36		command := buildNetworkProbeCommand(domain.ContainerNetworkProbeRequest{
37			Protocol: domain.ProbeProtocolTCP,
38			Port:     5432,
39			Timeout:  time.Second,
40		}, "172.18.0.6")
41		assert.Equal(t, tcpProbeScript, command.script)
42		assert.Equal(t, []string{"1", "172.18.0.6", "5432"}, command.args)
43	})
44
45	t.Run("nc bound stays strictly under the attempt bound", func(t *testing.T) {
46		for _, timeout := range []time.Duration{1500 * time.Millisecond, 5 * time.Second, 30 * time.Second} {
47			command := buildNetworkProbeCommand(domain.ContainerNetworkProbeRequest{
48				Protocol: domain.ProbeProtocolTCP, Port: 1, Timeout: timeout,
49			}, "10.0.0.1")
50			seconds, err := strconv.Atoi(command.args[0])
51			require.NoError(t, err)
52			assert.Less(t, float64(seconds), timeout.Seconds(),
53				"the helper must exit before the caller context expires for %s", timeout)
54		}
55	})
56
57	t.Run("timeout is clamped into the bounded window", func(t *testing.T) {
58		tooSmall := buildNetworkProbeCommand(domain.ContainerNetworkProbeRequest{
59			Protocol: domain.ProbeProtocolTCP, Port: 1, Timeout: time.Millisecond,
60		}, "10.0.0.1")
61		assert.Equal(t, "1", tooSmall.args[0])
62
63		tooLarge := buildNetworkProbeCommand(domain.ContainerNetworkProbeRequest{
64			Protocol: domain.ProbeProtocolTCP, Port: 1, Timeout: time.Hour,
65		}, "10.0.0.1")
66		assert.Equal(t, "30", tooLarge.args[0])
67	})
68}
69
70// TestProbeHelperHostConfig_IsHardened proves the helper can reach its
71// network and nothing else: no ports, mounts, volumes, or extra capability.
72func TestProbeHelperHostConfig_IsHardened(t *testing.T) {
73	config := probeHelperHostConfig("gordon--blog--net")
74	require.NotNil(t, config)
75	assert.Equal(t, "gordon--blog--net", string(config.NetworkMode))
76	assert.True(t, config.ReadonlyRootfs)
77	assert.Equal(t, []string{"ALL"}, config.CapDrop)
78	assert.Empty(t, config.CapAdd)
79	assert.Equal(t, []string{"no-new-privileges:true"}, config.SecurityOpt)
80	assert.False(t, config.Privileged)
81
82	assert.NotZero(t, config.Memory)
83	require.NotNil(t, config.PidsLimit)
84	assert.Positive(t, *config.PidsLimit)
85	assert.NotZero(t, config.NanoCPUs)
86
87	assert.Empty(t, config.PortBindings)
88	assert.Empty(t, config.Binds)
89	assert.Empty(t, config.Mounts)
90	assert.Empty(t, config.VolumesFrom)
91	assert.Empty(t, config.Devices)
92	assert.NotEqual(t, container.NetworkMode("none"), config.NetworkMode, "the helper must reach its target network")
93}
94
95// TestProbeHelperConfig_NoSharedNetworks proves the helper is created with
96// exactly one network and no aliases.
97func TestProbeHelperConfig_NoSharedNetworks(t *testing.T) {
98	config := probeHelperHostConfig("gordon--blog--net")
99	assert.Empty(t, config.ExtraHosts)
100	assert.Empty(t, config.DNS)
101	assert.Empty(t, config.Links)
102}
103
104func TestParseProbeStatus(t *testing.T) {
105	assert.Equal(t, 200, parseProbeStatus("200\n"))
106	assert.Equal(t, 404, parseProbeStatus("404"))
107	assert.Equal(t, 0, parseProbeStatus(""))
108	assert.Equal(t, 0, parseProbeStatus("not-a-status"))
109	assert.Equal(t, 0, parseProbeStatus("1234"))
110}
111
112func TestCappedProbeBuffer(t *testing.T) {
113	var buffer cappedProbeBuffer
114	input := strings.Repeat("x", 1024)
115	written, err := buffer.Write([]byte(input))
116	require.NoError(t, err)
117	assert.Equal(t, len(input), written)
118	assert.Len(t, buffer.String(), 64)
119}
120
121func TestHTTPProbeScript_RequiresThreeDigitStatus(t *testing.T) {
122	assert.Contains(t, httpProbeScript, "$2 ~ /^[0-9][0-9][0-9]$/")
123	assert.NotContains(t, httpProbeScript, "2*|3*")
124	assert.Contains(t, httpProbeScript, "head -c 128")
125}
126
127func TestProbeCleanupResult_PriorErrorNeverHidesCleanupFailure(t *testing.T) {
128	cleanupErr := errors.New("remove failed")
129	priorErr := fmt.Errorf("stale: %w", domain.ErrAppStateConflict)
130	result, err := probeCleanupResult(zerowrap.Default(), func() error { return cleanupErr }, domain.ContainerNetworkProbeResult{Ready: true}, priorErr)
131	assert.False(t, result.Ready)
132	require.ErrorIs(t, err, domain.ErrNetworkProbeCleanup)
133	require.ErrorIs(t, err, priorErr)
134}
135
136func TestProbeBoundExpired_OnlySessionDeadline(t *testing.T) {
137	parent, cancel := context.WithCancel(context.Background())
138	session, sessionCancel := context.WithDeadline(parent, time.Now().Add(-time.Second))
139	defer sessionCancel()
140	<-session.Done()
141
142	assert.True(t, probeBoundExpired(session, parent),
143		"the session's own bound expiring is a readiness timeout")
144
145	// Caller cancellation must never be reclassified as a readiness
146	// timeout: it has to stay an error so the deployment stops.
147	cancel()
148	<-session.Done()
149	assert.False(t, probeBoundExpired(session, parent),
150		"caller cancellation must stay an error")
151
152	assert.False(t, probeBoundExpired(context.Background(), context.Background()),
153		"a live session has not expired")
154}
155
156func TestProbeTimeoutFailure_IsUnhealthyNotError(t *testing.T) {
157	result := probeTimeoutFailure()
158	assert.False(t, result.Ready, "a bound expiry must not report readiness")
159	assert.NotEmpty(t, result.Diagnostic, "a bound expiry must carry a bounded diagnostic")
160}
161
162func TestProbeDiagnostic(t *testing.T) {
163	http := domain.ContainerNetworkProbeRequest{Protocol: domain.ProbeProtocolHTTP}
164	assert.Equal(t, "http status 503", probeDiagnostic(http, 503))
165	assert.Equal(t, "no http response", probeDiagnostic(http, 0))
166
167	tcp := domain.ContainerNetworkProbeRequest{Protocol: domain.ProbeProtocolTCP}
168	assert.Equal(t, "tcp connection refused or timed out", probeDiagnostic(tcp, 0))
169}
170
171func TestDefaultNetworkProbeImage_IsDigestPinned(t *testing.T) {
172	assert.Contains(t, DefaultNetworkProbeImage, "@sha256:", "the helper image must be pinned by digest")
173	assert.True(t, strings.HasPrefix(DefaultNetworkProbeImage, "alpine@"), "the helper needs only busybox shell tools")
174}

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