detached b7019c4

feat(secrets): add host secret file store

commit
b7019c4864175add9a2d5e45009c73b60f625625
author
brice · · 2026-10-08 20:22 UTC
parent
e55fb3b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +685 -0
M .mockery.yaml +1 -0
@@ -17,6 +17,7 @@ packages:
1717 EventSubscriber:
1818 EventHandler:
1919 SecretProvider:
20+ SecretFileStore:
2021 ContainerLogWriter:
2122 Metrics:
2223 LogExporter:
@@ -17,6 +17,7 @@ packages:
17 EventSubscriber:17 EventSubscriber:
18 EventHandler:18 EventHandler:
19 SecretProvider:19 SecretProvider:
20+ SecretFileStore:
20 ContainerLogWriter:21 ContainerLogWriter:
21 Metrics:22 Metrics:
22 LogExporter:23 LogExporter:
A internal/adapters/out/secretfiles/store.go +194 -0
@@ -0,0 +1,194 @@
1+// Package secretfiles stores file-delivered app secrets on the host.
2+//
3+// Layout: <data_dir>/secrets-run/<app>/<container-name>/<KEY>, directories
4+// 0700 and files 0444. All access goes through an os.Root so symlinks
5+// cannot escape the base directory. Values and paths are never logged.
6+package secretfiles
7+
8+import (
9+ "context"
10+ "errors"
11+ "fmt"
12+ "io/fs"
13+ "maps"
14+ "os"
15+ "path/filepath"
16+ "slices"
17+ "strings"
18+ "syscall"
19+
20+ "github.com/bnema/gordon/internal/boundaries/out"
21+ "github.com/bnema/gordon/internal/domain"
22+)
23+
24+const (
25+ dirMode os.FileMode = 0o700
26+ fileMode os.FileMode = 0o444
27+ // maxContainerName bounds a container directory name.
28+ maxContainerName = 255
29+)
30+
31+// Store implements out.SecretFileStore on the local filesystem.
32+type Store struct {
33+ dir string
34+ root *os.Root
35+}
36+
37+var _ out.SecretFileStore = (*Store)(nil)
38+
39+// NewStore opens (creating if needed) <dataDir>/secrets-run.
40+func NewStore(dataDir string) (*Store, error) {
41+ dir := filepath.Join(dataDir, "secrets-run")
42+ if err := os.MkdirAll(dir, dirMode); err != nil {
43+ return nil, fmt.Errorf("secret files: create base directory: %w", err)
44+ }
45+ info, err := os.Lstat(dir)
46+ if err != nil {
47+ return nil, fmt.Errorf("secret files: stat base directory: %w", err)
48+ }
49+ if !info.IsDir() {
50+ return nil, errors.New("secret files: base directory must be a directory, not a symlink")
51+ }
52+ if err := os.Chmod(dir, dirMode); err != nil {
53+ return nil, fmt.Errorf("secret files: chmod base directory: %w", err)
54+ }
55+ root, err := os.OpenRoot(dir)
56+ if err != nil {
57+ return nil, fmt.Errorf("secret files: open base directory: %w", err)
58+ }
59+ return &Store{dir: dir, root: root}, nil
60+}
61+
62+// Dir returns the absolute base directory.
63+func (s *Store) Dir() string { return s.dir }
64+
65+// Close releases the base directory handle.
66+func (s *Store) Close() error { return s.root.Close() }
67+
68+// Write replaces the container's directory with one 0444 file per key.
69+func (s *Store) Write(_ context.Context, app, container string, values map[string]string) (map[string]string, error) {
70+ if err := validateSet(app, container); err != nil {
71+ return nil, err
72+ }
73+ for key := range values {
74+ if err := domain.ValidateEnvKey(key); err != nil {
75+ return nil, fmt.Errorf("secret files: %w", err)
76+ }
77+ }
78+ if err := s.root.MkdirAll(app, dirMode); err != nil {
79+ return nil, fmt.Errorf("secret files: create app directory: %w", err)
80+ }
81+ if err := s.root.Chmod(app, dirMode); err != nil {
82+ return nil, fmt.Errorf("secret files: chmod app directory: %w", err)
83+ }
84+ rel := filepath.Join(app, container)
85+ if err := s.root.RemoveAll(rel); err != nil {
86+ return nil, fmt.Errorf("secret files: clear container directory: %w", err)
87+ }
88+ paths, err := s.writeFiles(rel, values)
89+ if err != nil {
90+ _ = s.root.RemoveAll(rel)
91+ return nil, err
92+ }
93+ return paths, nil
94+}
95+
96+func (s *Store) writeFiles(rel string, values map[string]string) (map[string]string, error) {
97+ if err := s.root.Mkdir(rel, dirMode); err != nil {
98+ return nil, fmt.Errorf("secret files: create container directory: %w", err)
99+ }
100+ if err := s.root.Chmod(rel, dirMode); err != nil {
101+ return nil, fmt.Errorf("secret files: chmod container directory: %w", err)
102+ }
103+ paths := make(map[string]string, len(values))
104+ for _, key := range slices.Sorted(maps.Keys(values)) {
105+ name := filepath.Join(rel, key)
106+ if err := s.writeFile(name, values[key]); err != nil {
107+ return nil, fmt.Errorf("secret files: write %s: %w", key, err)
108+ }
109+ paths[key] = filepath.Join(s.dir, name)
110+ }
111+ return paths, nil
112+}
113+
114+func (s *Store) writeFile(name, value string) error {
115+ f, err := s.root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode)
116+ if err != nil {
117+ return err
118+ }
119+ if _, err := f.WriteString(value); err != nil {
120+ _ = f.Close()
121+ return err
122+ }
123+ if err := f.Close(); err != nil {
124+ return err
125+ }
126+ return s.root.Chmod(name, fileMode)
127+}
128+
129+// Read returns one file's content.
130+func (s *Store) Read(_ context.Context, app, container, key string) (string, error) {
131+ if err := validateSet(app, container); err != nil {
132+ return "", err
133+ }
134+ if err := domain.ValidateEnvKey(key); err != nil {
135+ return "", fmt.Errorf("secret files: %w", err)
136+ }
137+ data, err := s.root.ReadFile(filepath.Join(app, container, key))
138+ if err != nil {
139+ return "", fmt.Errorf("secret files: read %s: %w", key, err)
140+ }
141+ return string(data), nil
142+}
143+
144+// Remove deletes the container's directory and the app directory when it
145+// becomes empty. A missing directory is not an error.
146+func (s *Store) Remove(_ context.Context, app, container string) error {
147+ if err := validateSet(app, container); err != nil {
148+ return err
149+ }
150+ if err := s.root.RemoveAll(filepath.Join(app, container)); err != nil {
151+ return fmt.Errorf("secret files: remove container directory: %w", err)
152+ }
153+ if err := s.root.Remove(app); err != nil && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, syscall.ENOTEMPTY) && !errors.Is(err, syscall.EEXIST) {
154+ return fmt.Errorf("secret files: remove app directory: %w", err)
155+ }
156+ return nil
157+}
158+
159+// List returns every container directory, sorted by app then container.
160+func (s *Store) List(_ context.Context) ([]out.SecretFileSet, error) {
161+ fsys := s.root.FS()
162+ apps, err := fs.ReadDir(fsys, ".")
163+ if err != nil {
164+ return nil, fmt.Errorf("secret files: list apps: %w", err)
165+ }
166+ var sets []out.SecretFileSet
167+ for _, app := range apps {
168+ if !app.IsDir() {
169+ continue
170+ }
171+ containers, err := fs.ReadDir(fsys, app.Name())
172+ if err != nil {
173+ return nil, fmt.Errorf("secret files: list containers: %w", err)
174+ }
175+ for _, c := range containers {
176+ if c.IsDir() {
177+ sets = append(sets, out.SecretFileSet{App: app.Name(), Container: c.Name()})
178+ }
179+ }
180+ }
181+ // fs.ReadDir returns entries sorted by name, so sets are already ordered.
182+ return sets, nil
183+}
184+
185+func validateSet(app, container string) error {
186+ if err := domain.ValidateAppName(app); err != nil {
187+ return fmt.Errorf("secret files: %w", err)
188+ }
189+ if container == "" || len(container) > maxContainerName || container == "." || container == ".." ||
190+ filepath.Base(container) != container || strings.ContainsAny(container, "/\\\x00") {
191+ return errors.New("secret files: invalid container name")
192+ }
193+ return nil
194+}
@@ -0,0 +1,194 @@
1+// Package secretfiles stores file-delivered app secrets on the host.
2+//
3+// Layout: <data_dir>/secrets-run/<app>/<container-name>/<KEY>, directories
4+// 0700 and files 0444. All access goes through an os.Root so symlinks
5+// cannot escape the base directory. Values and paths are never logged.
6+package secretfiles
7+
8+import (
9+ "context"
10+ "errors"
11+ "fmt"
12+ "io/fs"
13+ "maps"
14+ "os"
15+ "path/filepath"
16+ "slices"
17+ "strings"
18+ "syscall"
19+
20+ "github.com/bnema/gordon/internal/boundaries/out"
21+ "github.com/bnema/gordon/internal/domain"
22+)
23+
24+const (
25+ dirMode os.FileMode = 0o700
26+ fileMode os.FileMode = 0o444
27+ // maxContainerName bounds a container directory name.
28+ maxContainerName = 255
29+)
30+
31+// Store implements out.SecretFileStore on the local filesystem.
32+type Store struct {
33+ dir string
34+ root *os.Root
35+}
36+
37+var _ out.SecretFileStore = (*Store)(nil)
38+
39+// NewStore opens (creating if needed) <dataDir>/secrets-run.
40+func NewStore(dataDir string) (*Store, error) {
41+ dir := filepath.Join(dataDir, "secrets-run")
42+ if err := os.MkdirAll(dir, dirMode); err != nil {
43+ return nil, fmt.Errorf("secret files: create base directory: %w", err)
44+ }
45+ info, err := os.Lstat(dir)
46+ if err != nil {
47+ return nil, fmt.Errorf("secret files: stat base directory: %w", err)
48+ }
49+ if !info.IsDir() {
50+ return nil, errors.New("secret files: base directory must be a directory, not a symlink")
51+ }
52+ if err := os.Chmod(dir, dirMode); err != nil {
53+ return nil, fmt.Errorf("secret files: chmod base directory: %w", err)
54+ }
55+ root, err := os.OpenRoot(dir)
56+ if err != nil {
57+ return nil, fmt.Errorf("secret files: open base directory: %w", err)
58+ }
59+ return &Store{dir: dir, root: root}, nil
60+}
61+
62+// Dir returns the absolute base directory.
63+func (s *Store) Dir() string { return s.dir }
64+
65+// Close releases the base directory handle.
66+func (s *Store) Close() error { return s.root.Close() }
67+
68+// Write replaces the container's directory with one 0444 file per key.
69+func (s *Store) Write(_ context.Context, app, container string, values map[string]string) (map[string]string, error) {
70+ if err := validateSet(app, container); err != nil {
71+ return nil, err
72+ }
73+ for key := range values {
74+ if err := domain.ValidateEnvKey(key); err != nil {
75+ return nil, fmt.Errorf("secret files: %w", err)
76+ }
77+ }
78+ if err := s.root.MkdirAll(app, dirMode); err != nil {
79+ return nil, fmt.Errorf("secret files: create app directory: %w", err)
80+ }
81+ if err := s.root.Chmod(app, dirMode); err != nil {
82+ return nil, fmt.Errorf("secret files: chmod app directory: %w", err)
83+ }
84+ rel := filepath.Join(app, container)
85+ if err := s.root.RemoveAll(rel); err != nil {
86+ return nil, fmt.Errorf("secret files: clear container directory: %w", err)
87+ }
88+ paths, err := s.writeFiles(rel, values)
89+ if err != nil {
90+ _ = s.root.RemoveAll(rel)
91+ return nil, err
92+ }
93+ return paths, nil
94+}
95+
96+func (s *Store) writeFiles(rel string, values map[string]string) (map[string]string, error) {
97+ if err := s.root.Mkdir(rel, dirMode); err != nil {
98+ return nil, fmt.Errorf("secret files: create container directory: %w", err)
99+ }
100+ if err := s.root.Chmod(rel, dirMode); err != nil {
101+ return nil, fmt.Errorf("secret files: chmod container directory: %w", err)
102+ }
103+ paths := make(map[string]string, len(values))
104+ for _, key := range slices.Sorted(maps.Keys(values)) {
105+ name := filepath.Join(rel, key)
106+ if err := s.writeFile(name, values[key]); err != nil {
107+ return nil, fmt.Errorf("secret files: write %s: %w", key, err)
108+ }
109+ paths[key] = filepath.Join(s.dir, name)
110+ }
111+ return paths, nil
112+}
113+
114+func (s *Store) writeFile(name, value string) error {
115+ f, err := s.root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode)
116+ if err != nil {
117+ return err
118+ }
119+ if _, err := f.WriteString(value); err != nil {
120+ _ = f.Close()
121+ return err
122+ }
123+ if err := f.Close(); err != nil {
124+ return err
125+ }
126+ return s.root.Chmod(name, fileMode)
127+}
128+
129+// Read returns one file's content.
130+func (s *Store) Read(_ context.Context, app, container, key string) (string, error) {
131+ if err := validateSet(app, container); err != nil {
132+ return "", err
133+ }
134+ if err := domain.ValidateEnvKey(key); err != nil {
135+ return "", fmt.Errorf("secret files: %w", err)
136+ }
137+ data, err := s.root.ReadFile(filepath.Join(app, container, key))
138+ if err != nil {
139+ return "", fmt.Errorf("secret files: read %s: %w", key, err)
140+ }
141+ return string(data), nil
142+}
143+
144+// Remove deletes the container's directory and the app directory when it
145+// becomes empty. A missing directory is not an error.
146+func (s *Store) Remove(_ context.Context, app, container string) error {
147+ if err := validateSet(app, container); err != nil {
148+ return err
149+ }
150+ if err := s.root.RemoveAll(filepath.Join(app, container)); err != nil {
151+ return fmt.Errorf("secret files: remove container directory: %w", err)
152+ }
153+ if err := s.root.Remove(app); err != nil && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, syscall.ENOTEMPTY) && !errors.Is(err, syscall.EEXIST) {
154+ return fmt.Errorf("secret files: remove app directory: %w", err)
155+ }
156+ return nil
157+}
158+
159+// List returns every container directory, sorted by app then container.
160+func (s *Store) List(_ context.Context) ([]out.SecretFileSet, error) {
161+ fsys := s.root.FS()
162+ apps, err := fs.ReadDir(fsys, ".")
163+ if err != nil {
164+ return nil, fmt.Errorf("secret files: list apps: %w", err)
165+ }
166+ var sets []out.SecretFileSet
167+ for _, app := range apps {
168+ if !app.IsDir() {
169+ continue
170+ }
171+ containers, err := fs.ReadDir(fsys, app.Name())
172+ if err != nil {
173+ return nil, fmt.Errorf("secret files: list containers: %w", err)
174+ }
175+ for _, c := range containers {
176+ if c.IsDir() {
177+ sets = append(sets, out.SecretFileSet{App: app.Name(), Container: c.Name()})
178+ }
179+ }
180+ }
181+ // fs.ReadDir returns entries sorted by name, so sets are already ordered.
182+ return sets, nil
183+}
184+
185+func validateSet(app, container string) error {
186+ if err := domain.ValidateAppName(app); err != nil {
187+ return fmt.Errorf("secret files: %w", err)
188+ }
189+ if container == "" || len(container) > maxContainerName || container == "." || container == ".." ||
190+ filepath.Base(container) != container || strings.ContainsAny(container, "/\\\x00") {
191+ return errors.New("secret files: invalid container name")
192+ }
193+ return nil
194+}
A internal/adapters/out/secretfiles/store_test.go +138 -0
@@ -0,0 +1,138 @@
1+package secretfiles_test
2+
3+import (
4+ "context"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
11+
12+ "github.com/bnema/gordon/internal/adapters/out/secretfiles"
13+ "github.com/bnema/gordon/internal/boundaries/out"
14+)
15+
16+func newStore(t *testing.T) (*secretfiles.Store, string) {
17+ t.Helper()
18+ dataDir := t.TempDir()
19+ store, err := secretfiles.NewStore(dataDir)
20+ require.NoError(t, err)
21+ t.Cleanup(func() { _ = store.Close() })
22+ return store, dataDir
23+}
24+
25+func mode(t *testing.T, p string) os.FileMode {
26+ t.Helper()
27+ info, err := os.Stat(p)
28+ require.NoError(t, err)
29+ return info.Mode().Perm()
30+}
31+
32+func TestStore_WriteModesAndRead(t *testing.T) {
33+ store, dataDir := newStore(t)
34+ ctx := context.Background()
35+ assert.Equal(t, filepath.Join(dataDir, "secrets-run"), store.Dir())
36+
37+ paths, err := store.Write(ctx, "blog", "gordon-blog--web--a", map[string]string{"DB": "s3cr3t", "API": "k"})
38+ require.NoError(t, err)
39+ assert.Equal(t, filepath.Join(store.Dir(), "blog", "gordon-blog--web--a", "DB"), paths["DB"])
40+ assert.Len(t, paths, 2)
41+
42+ assert.Equal(t, os.FileMode(0o700), mode(t, store.Dir()))
43+ assert.Equal(t, os.FileMode(0o700), mode(t, filepath.Join(store.Dir(), "blog")))
44+ assert.Equal(t, os.FileMode(0o700), mode(t, filepath.Join(store.Dir(), "blog", "gordon-blog--web--a")))
45+ assert.Equal(t, os.FileMode(0o444), mode(t, paths["DB"]))
46+
47+ got, err := store.Read(ctx, "blog", "gordon-blog--web--a", "DB")
48+ require.NoError(t, err)
49+ assert.Equal(t, "s3cr3t", got)
50+}
51+
52+func TestStore_WriteReplacesStaleKeys(t *testing.T) {
53+ store, _ := newStore(t)
54+ ctx := context.Background()
55+ _, err := store.Write(ctx, "blog", "c1", map[string]string{"OLD": "x"})
56+ require.NoError(t, err)
57+ _, err = store.Write(ctx, "blog", "c1", map[string]string{"NEW": "y"})
58+ require.NoError(t, err)
59+
60+ _, err = store.Read(ctx, "blog", "c1", "OLD")
61+ require.Error(t, err)
62+ got, err := store.Read(ctx, "blog", "c1", "NEW")
63+ require.NoError(t, err)
64+ assert.Equal(t, "y", got)
65+}
66+
67+func TestStore_RemoveAndList(t *testing.T) {
68+ store, _ := newStore(t)
69+ ctx := context.Background()
70+ for _, set := range []out.SecretFileSet{{App: "blog", Container: "c2"}, {App: "blog", Container: "c1"}, {App: "alpha", Container: "z"}} {
71+ _, err := store.Write(ctx, set.App, set.Container, map[string]string{"K": "v"})
72+ require.NoError(t, err)
73+ }
74+
75+ sets, err := store.List(ctx)
76+ require.NoError(t, err)
77+ assert.Equal(t, []out.SecretFileSet{{App: "alpha", Container: "z"}, {App: "blog", Container: "c1"}, {App: "blog", Container: "c2"}}, sets)
78+
79+ require.NoError(t, store.Remove(ctx, "blog", "c1"))
80+ assert.NoDirExists(t, filepath.Join(store.Dir(), "blog", "c1"))
81+ assert.DirExists(t, filepath.Join(store.Dir(), "blog", "c2"))
82+
83+ require.NoError(t, store.Remove(ctx, "blog", "c2"))
84+ assert.NoDirExists(t, filepath.Join(store.Dir(), "blog"))
85+
86+ require.NoError(t, store.Remove(ctx, "blog", "missing"))
87+ require.NoError(t, store.Remove(ctx, "ghost", "missing"))
88+
89+ sets, err = store.List(ctx)
90+ require.NoError(t, err)
91+ assert.Equal(t, []out.SecretFileSet{{App: "alpha", Container: "z"}}, sets)
92+}
93+
94+func TestStore_InvalidNamesRejected(t *testing.T) {
95+ store, _ := newStore(t)
96+ ctx := context.Background()
97+ cases := []struct {
98+ name, app, container, key string
99+ }{
100+ {"app traversal", "../x", "c", "K"},
101+ {"app slash", "a/b", "c", "K"},
102+ {"container dot", "blog", ".", "K"},
103+ {"container dotdot", "blog", "..", "K"},
104+ {"container slash", "blog", "a/b", "K"},
105+ {"container empty", "blog", "", "K"},
106+ {"bad key", "blog", "c", "bad-KEY!"},
107+ {"key traversal", "blog", "c", "../K"},
108+ }
109+ for _, tc := range cases {
110+ t.Run(tc.name, func(t *testing.T) {
111+ _, err := store.Write(ctx, tc.app, tc.container, map[string]string{tc.key: "v"})
112+ require.Error(t, err)
113+ assert.NotContains(t, err.Error(), store.Dir())
114+ })
115+ }
116+ sets, err := store.List(ctx)
117+ require.NoError(t, err)
118+ assert.Empty(t, sets)
119+}
120+
121+func TestStore_SymlinkEscapeRefused(t *testing.T) {
122+ store, _ := newStore(t)
123+ outside := t.TempDir()
124+ require.NoError(t, os.Symlink(outside, filepath.Join(store.Dir(), "blog")))
125+
126+ _, err := store.Write(context.Background(), "blog", "c1", map[string]string{"K": "v"})
127+ require.Error(t, err)
128+ entries, err := os.ReadDir(outside)
129+ require.NoError(t, err)
130+ assert.Empty(t, entries)
131+}
132+
133+func TestNewStore_RefusesSymlinkBase(t *testing.T) {
134+ dataDir := t.TempDir()
135+ require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(dataDir, "secrets-run")))
136+ _, err := secretfiles.NewStore(dataDir)
137+ require.Error(t, err)
138+}
@@ -0,0 +1,138 @@
1+package secretfiles_test
2+
3+import (
4+ "context"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
11+
12+ "github.com/bnema/gordon/internal/adapters/out/secretfiles"
13+ "github.com/bnema/gordon/internal/boundaries/out"
14+)
15+
16+func newStore(t *testing.T) (*secretfiles.Store, string) {
17+ t.Helper()
18+ dataDir := t.TempDir()
19+ store, err := secretfiles.NewStore(dataDir)
20+ require.NoError(t, err)
21+ t.Cleanup(func() { _ = store.Close() })
22+ return store, dataDir
23+}
24+
25+func mode(t *testing.T, p string) os.FileMode {
26+ t.Helper()
27+ info, err := os.Stat(p)
28+ require.NoError(t, err)
29+ return info.Mode().Perm()
30+}
31+
32+func TestStore_WriteModesAndRead(t *testing.T) {
33+ store, dataDir := newStore(t)
34+ ctx := context.Background()
35+ assert.Equal(t, filepath.Join(dataDir, "secrets-run"), store.Dir())
36+
37+ paths, err := store.Write(ctx, "blog", "gordon-blog--web--a", map[string]string{"DB": "s3cr3t", "API": "k"})
38+ require.NoError(t, err)
39+ assert.Equal(t, filepath.Join(store.Dir(), "blog", "gordon-blog--web--a", "DB"), paths["DB"])
40+ assert.Len(t, paths, 2)
41+
42+ assert.Equal(t, os.FileMode(0o700), mode(t, store.Dir()))
43+ assert.Equal(t, os.FileMode(0o700), mode(t, filepath.Join(store.Dir(), "blog")))
44+ assert.Equal(t, os.FileMode(0o700), mode(t, filepath.Join(store.Dir(), "blog", "gordon-blog--web--a")))
45+ assert.Equal(t, os.FileMode(0o444), mode(t, paths["DB"]))
46+
47+ got, err := store.Read(ctx, "blog", "gordon-blog--web--a", "DB")
48+ require.NoError(t, err)
49+ assert.Equal(t, "s3cr3t", got)
50+}
51+
52+func TestStore_WriteReplacesStaleKeys(t *testing.T) {
53+ store, _ := newStore(t)
54+ ctx := context.Background()
55+ _, err := store.Write(ctx, "blog", "c1", map[string]string{"OLD": "x"})
56+ require.NoError(t, err)
57+ _, err = store.Write(ctx, "blog", "c1", map[string]string{"NEW": "y"})
58+ require.NoError(t, err)
59+
60+ _, err = store.Read(ctx, "blog", "c1", "OLD")
61+ require.Error(t, err)
62+ got, err := store.Read(ctx, "blog", "c1", "NEW")
63+ require.NoError(t, err)
64+ assert.Equal(t, "y", got)
65+}
66+
67+func TestStore_RemoveAndList(t *testing.T) {
68+ store, _ := newStore(t)
69+ ctx := context.Background()
70+ for _, set := range []out.SecretFileSet{{App: "blog", Container: "c2"}, {App: "blog", Container: "c1"}, {App: "alpha", Container: "z"}} {
71+ _, err := store.Write(ctx, set.App, set.Container, map[string]string{"K": "v"})
72+ require.NoError(t, err)
73+ }
74+
75+ sets, err := store.List(ctx)
76+ require.NoError(t, err)
77+ assert.Equal(t, []out.SecretFileSet{{App: "alpha", Container: "z"}, {App: "blog", Container: "c1"}, {App: "blog", Container: "c2"}}, sets)
78+
79+ require.NoError(t, store.Remove(ctx, "blog", "c1"))
80+ assert.NoDirExists(t, filepath.Join(store.Dir(), "blog", "c1"))
81+ assert.DirExists(t, filepath.Join(store.Dir(), "blog", "c2"))
82+
83+ require.NoError(t, store.Remove(ctx, "blog", "c2"))
84+ assert.NoDirExists(t, filepath.Join(store.Dir(), "blog"))
85+
86+ require.NoError(t, store.Remove(ctx, "blog", "missing"))
87+ require.NoError(t, store.Remove(ctx, "ghost", "missing"))
88+
89+ sets, err = store.List(ctx)
90+ require.NoError(t, err)
91+ assert.Equal(t, []out.SecretFileSet{{App: "alpha", Container: "z"}}, sets)
92+}
93+
94+func TestStore_InvalidNamesRejected(t *testing.T) {
95+ store, _ := newStore(t)
96+ ctx := context.Background()
97+ cases := []struct {
98+ name, app, container, key string
99+ }{
100+ {"app traversal", "../x", "c", "K"},
101+ {"app slash", "a/b", "c", "K"},
102+ {"container dot", "blog", ".", "K"},
103+ {"container dotdot", "blog", "..", "K"},
104+ {"container slash", "blog", "a/b", "K"},
105+ {"container empty", "blog", "", "K"},
106+ {"bad key", "blog", "c", "bad-KEY!"},
107+ {"key traversal", "blog", "c", "../K"},
108+ }
109+ for _, tc := range cases {
110+ t.Run(tc.name, func(t *testing.T) {
111+ _, err := store.Write(ctx, tc.app, tc.container, map[string]string{tc.key: "v"})
112+ require.Error(t, err)
113+ assert.NotContains(t, err.Error(), store.Dir())
114+ })
115+ }
116+ sets, err := store.List(ctx)
117+ require.NoError(t, err)
118+ assert.Empty(t, sets)
119+}
120+
121+func TestStore_SymlinkEscapeRefused(t *testing.T) {
122+ store, _ := newStore(t)
123+ outside := t.TempDir()
124+ require.NoError(t, os.Symlink(outside, filepath.Join(store.Dir(), "blog")))
125+
126+ _, err := store.Write(context.Background(), "blog", "c1", map[string]string{"K": "v"})
127+ require.Error(t, err)
128+ entries, err := os.ReadDir(outside)
129+ require.NoError(t, err)
130+ assert.Empty(t, entries)
131+}
132+
133+func TestNewStore_RefusesSymlinkBase(t *testing.T) {
134+ dataDir := t.TempDir()
135+ require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(dataDir, "secrets-run")))
136+ _, err := secretfiles.NewStore(dataDir)
137+ require.Error(t, err)
138+}
A internal/boundaries/out/mocks/mock_secret_file_store.go +331 -0
@@ -0,0 +1,331 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/gordon/internal/boundaries/out"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockSecretFileStore creates a new instance of MockSecretFileStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockSecretFileStore(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockSecretFileStore {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &MockSecretFileStore{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// MockSecretFileStore is an autogenerated mock type for the SecretFileStore type
38+type MockSecretFileStore struct {
39+ mock.Mock
40+}
41+
42+type MockSecretFileStore_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *MockSecretFileStore) EXPECT() *MockSecretFileStore_Expecter {
47+ return &MockSecretFileStore_Expecter{mock: &_m.Mock}
48+}
49+
50+// List provides a mock function for the type MockSecretFileStore
51+func (_mock *MockSecretFileStore) List(ctx context.Context) ([]out.SecretFileSet, error) {
52+ ret := _mock.Called(ctx)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for List")
56+ }
57+
58+ var r0 []out.SecretFileSet
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]out.SecretFileSet, error)); ok {
61+ return returnFunc(ctx)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context) []out.SecretFileSet); ok {
64+ r0 = returnFunc(ctx)
65+ } else {
66+ if ret.Get(0) != nil {
67+ r0 = ret.Get(0).([]out.SecretFileSet)
68+ }
69+ }
70+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
71+ r1 = returnFunc(ctx)
72+ } else {
73+ r1 = ret.Error(1)
74+ }
75+ return r0, r1
76+}
77+
78+// MockSecretFileStore_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
79+type MockSecretFileStore_List_Call struct {
80+ *mock.Call
81+}
82+
83+// List is a helper method to define mock.On call
84+// - ctx context.Context
85+func (_e *MockSecretFileStore_Expecter) List(ctx any) *MockSecretFileStore_List_Call {
86+ return &MockSecretFileStore_List_Call{Call: _e.mock.On("List", ctx)}
87+}
88+
89+func (_c *MockSecretFileStore_List_Call) Run(run func(ctx context.Context)) *MockSecretFileStore_List_Call {
90+ _c.Call.Run(func(args mock.Arguments) {
91+ var arg0 context.Context
92+ if args[0] != nil {
93+ arg0 = args[0].(context.Context)
94+ }
95+ run(
96+ arg0,
97+ )
98+ })
99+ return _c
100+}
101+
102+func (_c *MockSecretFileStore_List_Call) Return(secretFileSets []out.SecretFileSet, err error) *MockSecretFileStore_List_Call {
103+ _c.Call.Return(secretFileSets, err)
104+ return _c
105+}
106+
107+func (_c *MockSecretFileStore_List_Call) RunAndReturn(run func(ctx context.Context) ([]out.SecretFileSet, error)) *MockSecretFileStore_List_Call {
108+ _c.Call.Return(run)
109+ return _c
110+}
111+
112+// Read provides a mock function for the type MockSecretFileStore
113+func (_mock *MockSecretFileStore) Read(ctx context.Context, app string, container string, key string) (string, error) {
114+ ret := _mock.Called(ctx, app, container, key)
115+
116+ if len(ret) == 0 {
117+ panic("no return value specified for Read")
118+ }
119+
120+ var r0 string
121+ var r1 error
122+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok {
123+ return returnFunc(ctx, app, container, key)
124+ }
125+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok {
126+ r0 = returnFunc(ctx, app, container, key)
127+ } else {
128+ r0 = ret.Get(0).(string)
129+ }
130+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
131+ r1 = returnFunc(ctx, app, container, key)
132+ } else {
133+ r1 = ret.Error(1)
134+ }
135+ return r0, r1
136+}
137+
138+// MockSecretFileStore_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
139+type MockSecretFileStore_Read_Call struct {
140+ *mock.Call
141+}
142+
143+// Read is a helper method to define mock.On call
144+// - ctx context.Context
145+// - app string
146+// - container string
147+// - key string
148+func (_e *MockSecretFileStore_Expecter) Read(ctx any, app any, container any, key any) *MockSecretFileStore_Read_Call {
149+ return &MockSecretFileStore_Read_Call{Call: _e.mock.On("Read", ctx, app, container, key)}
150+}
151+
152+func (_c *MockSecretFileStore_Read_Call) Run(run func(ctx context.Context, app string, container string, key string)) *MockSecretFileStore_Read_Call {
153+ _c.Call.Run(func(args mock.Arguments) {
154+ var arg0 context.Context
155+ if args[0] != nil {
156+ arg0 = args[0].(context.Context)
157+ }
158+ var arg1 string
159+ if args[1] != nil {
160+ arg1 = args[1].(string)
161+ }
162+ var arg2 string
163+ if args[2] != nil {
164+ arg2 = args[2].(string)
165+ }
166+ var arg3 string
167+ if args[3] != nil {
168+ arg3 = args[3].(string)
169+ }
170+ run(
171+ arg0,
172+ arg1,
173+ arg2,
174+ arg3,
175+ )
176+ })
177+ return _c
178+}
179+
180+func (_c *MockSecretFileStore_Read_Call) Return(s string, err error) *MockSecretFileStore_Read_Call {
181+ _c.Call.Return(s, err)
182+ return _c
183+}
184+
185+func (_c *MockSecretFileStore_Read_Call) RunAndReturn(run func(ctx context.Context, app string, container string, key string) (string, error)) *MockSecretFileStore_Read_Call {
186+ _c.Call.Return(run)
187+ return _c
188+}
189+
190+// Remove provides a mock function for the type MockSecretFileStore
191+func (_mock *MockSecretFileStore) Remove(ctx context.Context, app string, container string) error {
192+ ret := _mock.Called(ctx, app, container)
193+
194+ if len(ret) == 0 {
195+ panic("no return value specified for Remove")
196+ }
197+
198+ var r0 error
199+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
200+ r0 = returnFunc(ctx, app, container)
201+ } else {
202+ r0 = ret.Error(0)
203+ }
204+ return r0
205+}
206+
207+// MockSecretFileStore_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
208+type MockSecretFileStore_Remove_Call struct {
209+ *mock.Call
210+}
211+
212+// Remove is a helper method to define mock.On call
213+// - ctx context.Context
214+// - app string
215+// - container string
216+func (_e *MockSecretFileStore_Expecter) Remove(ctx any, app any, container any) *MockSecretFileStore_Remove_Call {
217+ return &MockSecretFileStore_Remove_Call{Call: _e.mock.On("Remove", ctx, app, container)}
218+}
219+
220+func (_c *MockSecretFileStore_Remove_Call) Run(run func(ctx context.Context, app string, container string)) *MockSecretFileStore_Remove_Call {
221+ _c.Call.Run(func(args mock.Arguments) {
222+ var arg0 context.Context
223+ if args[0] != nil {
224+ arg0 = args[0].(context.Context)
225+ }
226+ var arg1 string
227+ if args[1] != nil {
228+ arg1 = args[1].(string)
229+ }
230+ var arg2 string
231+ if args[2] != nil {
232+ arg2 = args[2].(string)
233+ }
234+ run(
235+ arg0,
236+ arg1,
237+ arg2,
238+ )
239+ })
240+ return _c
241+}
242+
243+func (_c *MockSecretFileStore_Remove_Call) Return(err error) *MockSecretFileStore_Remove_Call {
244+ _c.Call.Return(err)
245+ return _c
246+}
247+
248+func (_c *MockSecretFileStore_Remove_Call) RunAndReturn(run func(ctx context.Context, app string, container string) error) *MockSecretFileStore_Remove_Call {
249+ _c.Call.Return(run)
250+ return _c
251+}
252+
253+// Write provides a mock function for the type MockSecretFileStore
254+func (_mock *MockSecretFileStore) Write(ctx context.Context, app string, container string, values map[string]string) (map[string]string, error) {
255+ ret := _mock.Called(ctx, app, container, values)
256+
257+ if len(ret) == 0 {
258+ panic("no return value specified for Write")
259+ }
260+
261+ var r0 map[string]string
262+ var r1 error
263+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, map[string]string) (map[string]string, error)); ok {
264+ return returnFunc(ctx, app, container, values)
265+ }
266+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, map[string]string) map[string]string); ok {
267+ r0 = returnFunc(ctx, app, container, values)
268+ } else {
269+ if ret.Get(0) != nil {
270+ r0 = ret.Get(0).(map[string]string)
271+ }
272+ }
273+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, map[string]string) error); ok {
274+ r1 = returnFunc(ctx, app, container, values)
275+ } else {
276+ r1 = ret.Error(1)
277+ }
278+ return r0, r1
279+}
280+
281+// MockSecretFileStore_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
282+type MockSecretFileStore_Write_Call struct {
283+ *mock.Call
284+}
285+
286+// Write is a helper method to define mock.On call
287+// - ctx context.Context
288+// - app string
289+// - container string
290+// - values map[string]string
291+func (_e *MockSecretFileStore_Expecter) Write(ctx any, app any, container any, values any) *MockSecretFileStore_Write_Call {
292+ return &MockSecretFileStore_Write_Call{Call: _e.mock.On("Write", ctx, app, container, values)}
293+}
294+
295+func (_c *MockSecretFileStore_Write_Call) Run(run func(ctx context.Context, app string, container string, values map[string]string)) *MockSecretFileStore_Write_Call {
296+ _c.Call.Run(func(args mock.Arguments) {
297+ var arg0 context.Context
298+ if args[0] != nil {
299+ arg0 = args[0].(context.Context)
300+ }
301+ var arg1 string
302+ if args[1] != nil {
303+ arg1 = args[1].(string)
304+ }
305+ var arg2 string
306+ if args[2] != nil {
307+ arg2 = args[2].(string)
308+ }
309+ var arg3 map[string]string
310+ if args[3] != nil {
311+ arg3 = args[3].(map[string]string)
312+ }
313+ run(
314+ arg0,
315+ arg1,
316+ arg2,
317+ arg3,
318+ )
319+ })
320+ return _c
321+}
322+
323+func (_c *MockSecretFileStore_Write_Call) Return(stringToString map[string]string, err error) *MockSecretFileStore_Write_Call {
324+ _c.Call.Return(stringToString, err)
325+ return _c
326+}
327+
328+func (_c *MockSecretFileStore_Write_Call) RunAndReturn(run func(ctx context.Context, app string, container string, values map[string]string) (map[string]string, error)) *MockSecretFileStore_Write_Call {
329+ _c.Call.Return(run)
330+ return _c
331+}
@@ -0,0 +1,331 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/gordon/internal/boundaries/out"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockSecretFileStore creates a new instance of MockSecretFileStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockSecretFileStore(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockSecretFileStore {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &MockSecretFileStore{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// MockSecretFileStore is an autogenerated mock type for the SecretFileStore type
38+type MockSecretFileStore struct {
39+ mock.Mock
40+}
41+
42+type MockSecretFileStore_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *MockSecretFileStore) EXPECT() *MockSecretFileStore_Expecter {
47+ return &MockSecretFileStore_Expecter{mock: &_m.Mock}
48+}
49+
50+// List provides a mock function for the type MockSecretFileStore
51+func (_mock *MockSecretFileStore) List(ctx context.Context) ([]out.SecretFileSet, error) {
52+ ret := _mock.Called(ctx)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for List")
56+ }
57+
58+ var r0 []out.SecretFileSet
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]out.SecretFileSet, error)); ok {
61+ return returnFunc(ctx)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context) []out.SecretFileSet); ok {
64+ r0 = returnFunc(ctx)
65+ } else {
66+ if ret.Get(0) != nil {
67+ r0 = ret.Get(0).([]out.SecretFileSet)
68+ }
69+ }
70+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
71+ r1 = returnFunc(ctx)
72+ } else {
73+ r1 = ret.Error(1)
74+ }
75+ return r0, r1
76+}
77+
78+// MockSecretFileStore_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
79+type MockSecretFileStore_List_Call struct {
80+ *mock.Call
81+}
82+
83+// List is a helper method to define mock.On call
84+// - ctx context.Context
85+func (_e *MockSecretFileStore_Expecter) List(ctx any) *MockSecretFileStore_List_Call {
86+ return &MockSecretFileStore_List_Call{Call: _e.mock.On("List", ctx)}
87+}
88+
89+func (_c *MockSecretFileStore_List_Call) Run(run func(ctx context.Context)) *MockSecretFileStore_List_Call {
90+ _c.Call.Run(func(args mock.Arguments) {
91+ var arg0 context.Context
92+ if args[0] != nil {
93+ arg0 = args[0].(context.Context)
94+ }
95+ run(
96+ arg0,
97+ )
98+ })
99+ return _c
100+}
101+
102+func (_c *MockSecretFileStore_List_Call) Return(secretFileSets []out.SecretFileSet, err error) *MockSecretFileStore_List_Call {
103+ _c.Call.Return(secretFileSets, err)
104+ return _c
105+}
106+
107+func (_c *MockSecretFileStore_List_Call) RunAndReturn(run func(ctx context.Context) ([]out.SecretFileSet, error)) *MockSecretFileStore_List_Call {
108+ _c.Call.Return(run)
109+ return _c
110+}
111+
112+// Read provides a mock function for the type MockSecretFileStore
113+func (_mock *MockSecretFileStore) Read(ctx context.Context, app string, container string, key string) (string, error) {
114+ ret := _mock.Called(ctx, app, container, key)
115+
116+ if len(ret) == 0 {
117+ panic("no return value specified for Read")
118+ }
119+
120+ var r0 string
121+ var r1 error
122+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok {
123+ return returnFunc(ctx, app, container, key)
124+ }
125+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok {
126+ r0 = returnFunc(ctx, app, container, key)
127+ } else {
128+ r0 = ret.Get(0).(string)
129+ }
130+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
131+ r1 = returnFunc(ctx, app, container, key)
132+ } else {
133+ r1 = ret.Error(1)
134+ }
135+ return r0, r1
136+}
137+
138+// MockSecretFileStore_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
139+type MockSecretFileStore_Read_Call struct {
140+ *mock.Call
141+}
142+
143+// Read is a helper method to define mock.On call
144+// - ctx context.Context
145+// - app string
146+// - container string
147+// - key string
148+func (_e *MockSecretFileStore_Expecter) Read(ctx any, app any, container any, key any) *MockSecretFileStore_Read_Call {
149+ return &MockSecretFileStore_Read_Call{Call: _e.mock.On("Read", ctx, app, container, key)}
150+}
151+
152+func (_c *MockSecretFileStore_Read_Call) Run(run func(ctx context.Context, app string, container string, key string)) *MockSecretFileStore_Read_Call {
153+ _c.Call.Run(func(args mock.Arguments) {
154+ var arg0 context.Context
155+ if args[0] != nil {
156+ arg0 = args[0].(context.Context)
157+ }
158+ var arg1 string
159+ if args[1] != nil {
160+ arg1 = args[1].(string)
161+ }
162+ var arg2 string
163+ if args[2] != nil {
164+ arg2 = args[2].(string)
165+ }
166+ var arg3 string
167+ if args[3] != nil {
168+ arg3 = args[3].(string)
169+ }
170+ run(
171+ arg0,
172+ arg1,
173+ arg2,
174+ arg3,
175+ )
176+ })
177+ return _c
178+}
179+
180+func (_c *MockSecretFileStore_Read_Call) Return(s string, err error) *MockSecretFileStore_Read_Call {
181+ _c.Call.Return(s, err)
182+ return _c
183+}
184+
185+func (_c *MockSecretFileStore_Read_Call) RunAndReturn(run func(ctx context.Context, app string, container string, key string) (string, error)) *MockSecretFileStore_Read_Call {
186+ _c.Call.Return(run)
187+ return _c
188+}
189+
190+// Remove provides a mock function for the type MockSecretFileStore
191+func (_mock *MockSecretFileStore) Remove(ctx context.Context, app string, container string) error {
192+ ret := _mock.Called(ctx, app, container)
193+
194+ if len(ret) == 0 {
195+ panic("no return value specified for Remove")
196+ }
197+
198+ var r0 error
199+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
200+ r0 = returnFunc(ctx, app, container)
201+ } else {
202+ r0 = ret.Error(0)
203+ }
204+ return r0
205+}
206+
207+// MockSecretFileStore_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
208+type MockSecretFileStore_Remove_Call struct {
209+ *mock.Call
210+}
211+
212+// Remove is a helper method to define mock.On call
213+// - ctx context.Context
214+// - app string
215+// - container string
216+func (_e *MockSecretFileStore_Expecter) Remove(ctx any, app any, container any) *MockSecretFileStore_Remove_Call {
217+ return &MockSecretFileStore_Remove_Call{Call: _e.mock.On("Remove", ctx, app, container)}
218+}
219+
220+func (_c *MockSecretFileStore_Remove_Call) Run(run func(ctx context.Context, app string, container string)) *MockSecretFileStore_Remove_Call {
221+ _c.Call.Run(func(args mock.Arguments) {
222+ var arg0 context.Context
223+ if args[0] != nil {
224+ arg0 = args[0].(context.Context)
225+ }
226+ var arg1 string
227+ if args[1] != nil {
228+ arg1 = args[1].(string)
229+ }
230+ var arg2 string
231+ if args[2] != nil {
232+ arg2 = args[2].(string)
233+ }
234+ run(
235+ arg0,
236+ arg1,
237+ arg2,
238+ )
239+ })
240+ return _c
241+}
242+
243+func (_c *MockSecretFileStore_Remove_Call) Return(err error) *MockSecretFileStore_Remove_Call {
244+ _c.Call.Return(err)
245+ return _c
246+}
247+
248+func (_c *MockSecretFileStore_Remove_Call) RunAndReturn(run func(ctx context.Context, app string, container string) error) *MockSecretFileStore_Remove_Call {
249+ _c.Call.Return(run)
250+ return _c
251+}
252+
253+// Write provides a mock function for the type MockSecretFileStore
254+func (_mock *MockSecretFileStore) Write(ctx context.Context, app string, container string, values map[string]string) (map[string]string, error) {
255+ ret := _mock.Called(ctx, app, container, values)
256+
257+ if len(ret) == 0 {
258+ panic("no return value specified for Write")
259+ }
260+
261+ var r0 map[string]string
262+ var r1 error
263+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, map[string]string) (map[string]string, error)); ok {
264+ return returnFunc(ctx, app, container, values)
265+ }
266+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, map[string]string) map[string]string); ok {
267+ r0 = returnFunc(ctx, app, container, values)
268+ } else {
269+ if ret.Get(0) != nil {
270+ r0 = ret.Get(0).(map[string]string)
271+ }
272+ }
273+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, map[string]string) error); ok {
274+ r1 = returnFunc(ctx, app, container, values)
275+ } else {
276+ r1 = ret.Error(1)
277+ }
278+ return r0, r1
279+}
280+
281+// MockSecretFileStore_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
282+type MockSecretFileStore_Write_Call struct {
283+ *mock.Call
284+}
285+
286+// Write is a helper method to define mock.On call
287+// - ctx context.Context
288+// - app string
289+// - container string
290+// - values map[string]string
291+func (_e *MockSecretFileStore_Expecter) Write(ctx any, app any, container any, values any) *MockSecretFileStore_Write_Call {
292+ return &MockSecretFileStore_Write_Call{Call: _e.mock.On("Write", ctx, app, container, values)}
293+}
294+
295+func (_c *MockSecretFileStore_Write_Call) Run(run func(ctx context.Context, app string, container string, values map[string]string)) *MockSecretFileStore_Write_Call {
296+ _c.Call.Run(func(args mock.Arguments) {
297+ var arg0 context.Context
298+ if args[0] != nil {
299+ arg0 = args[0].(context.Context)
300+ }
301+ var arg1 string
302+ if args[1] != nil {
303+ arg1 = args[1].(string)
304+ }
305+ var arg2 string
306+ if args[2] != nil {
307+ arg2 = args[2].(string)
308+ }
309+ var arg3 map[string]string
310+ if args[3] != nil {
311+ arg3 = args[3].(map[string]string)
312+ }
313+ run(
314+ arg0,
315+ arg1,
316+ arg2,
317+ arg3,
318+ )
319+ })
320+ return _c
321+}
322+
323+func (_c *MockSecretFileStore_Write_Call) Return(stringToString map[string]string, err error) *MockSecretFileStore_Write_Call {
324+ _c.Call.Return(stringToString, err)
325+ return _c
326+}
327+
328+func (_c *MockSecretFileStore_Write_Call) RunAndReturn(run func(ctx context.Context, app string, container string, values map[string]string) (map[string]string, error)) *MockSecretFileStore_Write_Call {
329+ _c.Call.Return(run)
330+ return _c
331+}
A internal/boundaries/out/secret_files.go +21 -0
@@ -0,0 +1,21 @@
1+package out
2+
3+import "context"
4+
5+// SecretFileSet identifies one container's secret file directory.
6+type SecretFileSet struct{ App, Container string }
7+
8+// SecretFileStore persists per-container secret files on the host. Values
9+// cross this boundary in memory only; nothing is logged.
10+type SecretFileStore interface {
11+ // Write replaces the container's directory with one 0444 file per key
12+ // and returns key -> absolute host path.
13+ Write(ctx context.Context, app, container string, values map[string]string) (map[string]string, error)
14+ // Read returns one file's content.
15+ Read(ctx context.Context, app, container, key string) (string, error)
16+ // Remove deletes the container's directory (missing is not an error) and
17+ // the app directory when it becomes empty.
18+ Remove(ctx context.Context, app, container string) error
19+ // List returns every existing container directory, sorted by app then container.
20+ List(ctx context.Context) ([]SecretFileSet, error)
21+}
@@ -0,0 +1,21 @@
1+package out
2+
3+import "context"
4+
5+// SecretFileSet identifies one container's secret file directory.
6+type SecretFileSet struct{ App, Container string }
7+
8+// SecretFileStore persists per-container secret files on the host. Values
9+// cross this boundary in memory only; nothing is logged.
10+type SecretFileStore interface {
11+ // Write replaces the container's directory with one 0444 file per key
12+ // and returns key -> absolute host path.
13+ Write(ctx context.Context, app, container string, values map[string]string) (map[string]string, error)
14+ // Read returns one file's content.
15+ Read(ctx context.Context, app, container, key string) (string, error)
16+ // Remove deletes the container's directory (missing is not an error) and
17+ // the app directory when it becomes empty.
18+ Remove(ctx context.Context, app, container string) error
19+ // List returns every existing container directory, sorted by app then container.
20+ List(ctx context.Context) ([]SecretFileSet, error)
21+}

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