v3.0.0-alpha.1 6fdddb8

publication_internal_test.go RawHistoryFolder

1package deployment
2
3import (
4	"context"
5	"errors"
6	"testing"
7	"time"
8
9	"github.com/bnema/zerowrap"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/mock"
12	"github.com/stretchr/testify/require"
13
14	outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
15	"github.com/bnema/gordon/internal/domain"
16)
17
18// readySpec is an HTTP service whose readiness fails fast.
19func readySpec() domain.AppService {
20	return domain.AppService{
21		Name:      "web",
22		Image:     "registry.example.com/blog/web:1.4.2",
23		HTTP:      []domain.AppHTTPInterface{{Host: "blog.example.com", Port: 8080, TLS: "auto"}},
24		Readiness: domain.AppReadiness{Type: domain.AppReadinessHTTP, Path: "/healthz", Timeout: 50 * time.Millisecond},
25	}
26}
27
28// TestVerifyRunningService_NotReadyWithdrawsAndClearsBinds proves a running
29// generation that fails readiness is withdrawn, never published, and its
30// recorded binds are cleared so the proxy cannot dial an unready port.
31func TestVerifyRunningService_NotReadyWithdrawsAndClearsBinds(t *testing.T) {
32	ctx := context.Background()
33	state := outmocks.NewMockAppState(t)
34	runtime := outmocks.NewMockContainerRuntime(t)
35	traffic := &stubTraffic{}
36	eff := domain.AppEffectiveService{
37		Container:         "c-1",
38		EffectiveRevision: "rev-1",
39		Spec:              readySpec(),
40		BackendBinds:      map[int]int{8080: 32771},
41	}
42	runtime.EXPECT().GetContainerBackendBinds(mock.Anything, "c-1", mock.Anything).Return(
43		[]domain.ContainerBackendBind{{ContainerPort: 8080, HostPort: 32771, Protocol: domain.NetworkProtocolTCP}}, nil).Once()
44	state.EXPECT().RegisterBackendBinds(mock.Anything, mock.Anything).Return(nil).Once()
45	state.EXPECT().LoadActive(mock.Anything, "blog").Return(domain.AppActive{App: "blog", Services: map[string]domain.AppEffectiveService{"web": {Container: "c-1"}}}, true, nil).Once()
46	state.EXPECT().SaveActive(mock.Anything, mock.Anything).Return(nil).Once()
47	svc := NewService(Deps{State: state, Runtime: runtime, Traffic: traffic}, zerowrap.Default()).
48		WithProbeDeps(NewTestProbeDeps(runtime,
49			func(context.Context, string, string) (int, error) { return 500, nil },
50			func(context.Context, string) error { return nil },
51		))
52
53	step := domain.AppOperationStep{ID: "service.web.start"}
54	result := &LifecycleResult{Services: map[string]ServiceResult{}}
55	svc.verifyRunningService(ctx, "blog", "web", eff, &step, result)
56
57	assert.Equal(t, domain.AppStepFailed, step.State)
58	assert.Equal(t, "failed", result.Services["web"].Result)
59	// One serialized withdrawal plus one state-only bind clear through
60	// the canonical boundary; the fail-closed ACTIVE write itself is
61	// covered by the publisher tests in internal/app.
62	assert.Equal(t, []string{"blog/web", "blog/web"}, traffic.withdrawn)
63	state.AssertNumberOfCalls(t, "SaveActive", 1)
64}
65
66// TestVerifyRunningService_ReadyPublishesBinds proves a ready generation is
67// published with its freshly inspected binds.
68func TestVerifyRunningService_ReadyPublishesBinds(t *testing.T) {
69	ctx := context.Background()
70	state := outmocks.NewMockAppState(t)
71	runtime := outmocks.NewMockContainerRuntime(t)
72	traffic := &stubTraffic{}
73	eff := domain.AppEffectiveService{Container: "c-1", EffectiveRevision: "rev-1", Spec: readySpec()}
74	runtime.EXPECT().GetContainerBackendBinds(mock.Anything, "c-1", mock.Anything).Return(
75		[]domain.ContainerBackendBind{{ContainerPort: 8080, HostPort: 32771, Protocol: domain.NetworkProtocolTCP}}, nil).Once()
76	state.EXPECT().RegisterBackendBinds(mock.Anything, mock.Anything).Return(nil).Once()
77	state.EXPECT().LoadActive(mock.Anything, "blog").Return(domain.AppActive{
78		App: "blog", Services: map[string]domain.AppEffectiveService{"web": eff},
79	}, true, nil).Once()
80	state.EXPECT().SaveActive(mock.Anything, mock.Anything).Return(nil).Once()
81
82	svc := NewService(Deps{State: state, Runtime: runtime, Traffic: traffic}, zerowrap.Default()).
83		WithProbeDeps(NewTestProbeDeps(runtime,
84			func(context.Context, string, string) (int, error) { return 200, nil },
85			func(context.Context, string) error { return nil },
86		))
87
88	step := domain.AppOperationStep{ID: "service.web.start"}
89	result := &LifecycleResult{Services: map[string]ServiceResult{}}
90	svc.verifyRunningService(ctx, "blog", "web", eff, &step, result)
91
92	assert.Equal(t, domain.AppStepSucceeded, step.State)
93	assert.Equal(t, map[int]int{8080: 32771}, result.Services["web"].BackendBinds)
94	assert.Equal(t, []string{"blog/web"}, traffic.withdrawn, "the service is withdrawn before re-verification")
95}
96
97// TestRedactDiagnostics_RemovesSecretValues proves a known secret value is
98// never persisted in failure diagnostics.
99func TestRedactDiagnostics_RemovesSecretValues(t *testing.T) {
100	state := outmocks.NewMockAppState(t)
101	secrets := outmocks.NewMockSecretProvider(t)
102	state.EXPECT().LoadOwnership(mock.Anything, "blog").Return(domain.AppOwnership{App: "blog", ID: "app-1"}, nil).Once()
103	secrets.EXPECT().GetSecret(mock.Anything, "gordon/apps/app-1/web/database-url").Return("supersecret", nil).Once()
104
105	svc := NewService(Deps{State: state, Secrets: secrets}, zerowrap.Default())
106	p := pinnedService{name: "web", spec: domain.AppService{
107		Name: "web", Secrets: map[string]string{"DATABASE_URL": "database-url"},
108	}}
109
110	got := svc.redactDiagnostics(context.Background(), "blog", p, []string{"connect failed for supersecret", "still running"})
111
112	assert.Equal(t, []string{"connect failed for [redacted]", "still running"}, got)
113}
114
115// TestRedactDiagnostics_DropsWhenSecretUnreadable proves diagnostics are
116// dropped rather than persisted unredacted when a secret cannot be read.
117func TestRedactDiagnostics_DropsWhenSecretUnreadable(t *testing.T) {
118	state := outmocks.NewMockAppState(t)
119	secrets := outmocks.NewMockSecretProvider(t)
120	state.EXPECT().LoadOwnership(mock.Anything, "blog").Return(domain.AppOwnership{App: "blog", ID: "app-1"}, nil).Once()
121	secrets.EXPECT().GetSecret(mock.Anything, "gordon/apps/app-1/web/database-url").Return("", assert.AnError).Once()
122
123	svc := NewService(Deps{State: state, Secrets: secrets}, zerowrap.Default())
124	p := pinnedService{name: "web", spec: domain.AppService{
125		Name: "web", Secrets: map[string]string{"DATABASE_URL": "database-url"},
126	}}
127
128	assert.Nil(t, svc.redactDiagnostics(context.Background(), "blog", p, []string{"supersecret leaked"}))
129}
130
131// TestStopService_StopFailureSurfaces proves a runtime stop failure is
132// reported rather than recorded as a successful stop.
133func TestStopService_StopFailureSurfaces(t *testing.T) {
134	state := outmocks.NewMockAppState(t)
135	runtime := outmocks.NewMockContainerRuntime(t)
136	runtime.EXPECT().StopContainer(mock.Anything, "c-1", mock.Anything).Return(errors.New("cannot stop")).Once()
137
138	svc := NewService(Deps{State: state, Runtime: runtime, Traffic: &stubTraffic{}}, zerowrap.Default())
139	eff := domain.AppEffectiveService{Container: "c-1", Spec: domain.AppService{Name: "web", StopGrace: time.Second}}
140	step, _, err := svc.stopService(context.Background(), "blog", "web", eff)
141
142	require.Error(t, err)
143	assert.Equal(t, domain.AppStepFailed, step.State)
144	assert.Contains(t, step.Error, "cannot stop")
145}
146
147// TestStopService_WithdrawFailureBlocksStop proves withdrawal runs first and
148// its failure stops the operation before the container is touched.
149func TestStopService_WithdrawFailureBlocksStop(t *testing.T) {
150	state := outmocks.NewMockAppState(t)
151	runtime := outmocks.NewMockContainerRuntime(t)
152
153	svc := NewService(Deps{State: state, Runtime: runtime, Traffic: &stubTraffic{fail: true}}, zerowrap.Default())
154	eff := domain.AppEffectiveService{Container: "c-1", Spec: domain.AppService{Name: "web", StopGrace: time.Second}}
155	step, _, err := svc.stopService(context.Background(), "blog", "web", eff)
156
157	require.Error(t, err)
158	assert.Equal(t, domain.AppStepFailed, step.State)
159	runtime.AssertNotCalled(t, "StopContainer", mock.Anything, mock.Anything, mock.Anything)
160}

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