1package deployment
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
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
18func TestCreateContainer_GuardRefuses(t *testing.T) {
19 protected := t.TempDir()
20 child := filepath.Join(protected, "registry")
21 require.NoError(t, os.MkdirAll(child, 0o700))
22 cases := map[string]*domain.ContainerConfig{
23 "host network": {Name: "c", NetworkMode: "host"},
24 "protected bind": {
25 Name: "c",
26 Binds: []domain.ContainerBind{{Name: "conf", Source: child, Destination: "/etc/app", ReadOnly: true}},
27 },
28 }
29 for name, config := range cases {
30 t.Run(name, func(t *testing.T) {
31 runtime := outmocks.NewMockContainerRuntime(t)
32 svc := NewService(Deps{Runtime: runtime, Guard: domain.RuntimeGuard{ProtectedPaths: []string{protected}}}, zerowrap.Default())
33
34 _, err := svc.createContainer(context.Background(), "web", config)
35 require.ErrorIs(t, err, domain.ErrRuntimeGuard)
36 assert.NotContains(t, err.Error(), protected)
37 runtime.AssertNotCalled(t, "CreateContainer", mock.Anything, mock.Anything)
38 })
39 }
40}
41
42func TestCreateWithSecretFiles_GuardAllowsOwnSecretBind(t *testing.T) {
43 dataDir := t.TempDir()
44 base := filepath.Join(dataDir, "secrets-run")
45 store := outmocks.NewMockSecretFileStore(t)
46 runtime := outmocks.NewMockContainerRuntime(t)
47 guard := domain.RuntimeGuard{ProtectedPaths: []string{dataDir}, SecretFilesDir: base}
48 svc := NewService(Deps{Runtime: runtime, SecretFiles: store, Guard: guard}, zerowrap.Default())
49 config := &domain.ContainerConfig{Name: "gordon-blog--web--abc", Labels: appLabels("blog", "web", "rev-1")}
50
51 store.EXPECT().Write(mock.Anything, "blog", "gordon-blog--web--abc", mock.Anything).
52 Return(map[string]string{"B": filepath.Join(base, "blog", "gordon-blog--web--abc", "B")}, nil).Once()
53 runtime.EXPECT().CreateContainer(mock.Anything, mock.Anything).Return(&domain.Container{ID: "c-1"}, nil).Once()
54
55 _, err := svc.createWithSecretFiles(context.Background(), "blog", "web", config, map[string]string{"B": "v"})
56 require.NoError(t, err)
57}
58
59func TestResolveServiceBinds_GuardRefusesProtectedSource(t *testing.T) {
60 protected := t.TempDir()
61 svc := NewService(Deps{Guard: domain.RuntimeGuard{ProtectedPaths: []string{protected}}}, zerowrap.Default()).
62 WithBindPolicies(map[string]domain.AppBindPolicy{
63 "config": bindPolicyFor(protected, protected, true, []string{"blog"}, []string{"web"}),
64 })
65
66 _, err := svc.resolveServiceBinds("blog", domain.AppService{
67 Name: "web",
68 Binds: []domain.AppBind{{Name: "config", Path: "/etc/app", ReadOnly: true}},
69 })
70 require.ErrorIs(t, err, domain.ErrRuntimeGuard)
71 assert.NotContains(t, err.Error(), protected)
72}
73
74// TestCreateAndStart_GuardRefusesBeforeRuntimeMutation proves the guard in
75// resolveServiceBinds stops a protected bind before secrets, volumes, or
76// container create.
77func TestCreateAndStart_GuardRefusesBeforeRuntimeMutation(t *testing.T) {
78 protected := t.TempDir()
79 runtime := outmocks.NewMockContainerRuntime(t)
80 svc := NewService(Deps{Runtime: runtime, Guard: domain.RuntimeGuard{ProtectedPaths: []string{protected}}}, zerowrap.Default()).
81 WithBindPolicies(map[string]domain.AppBindPolicy{
82 "config": bindPolicyFor(protected, protected, true, []string{"blog"}, []string{"web"}),
83 })
84 p := pinnedService{name: "web", spec: domain.AppService{
85 Name: "web",
86 Binds: []domain.AppBind{{Name: "config", Path: "/etc/app", ReadOnly: true}},
87 }}
88
89 _, err := svc.createAndStart(context.Background(), "blog", "rev-1", p, "op-1", nil)
90 require.ErrorIs(t, err, domain.ErrRuntimeGuard)
91 runtime.AssertNotCalled(t, "CreateContainer", mock.Anything, mock.Anything)
92 runtime.AssertNotCalled(t, "CreateVolume", mock.Anything, mock.Anything)
93}