detached b69c6b0

feat(secrets): implement attachment secrets for pass store backend

Add full support for attachment container secrets in pass store:
- SetAttachment, GetAllAttachment, DeleteAttachment, ListAttachmentSecrets
- Atomic writes with manifest tracking
- Backwards compatibility with legacy container naming
- Comprehensive test coverage
commit
b69c6b000ec6a6ba2b9b8c20f33c400a1d692a1b
author
bnema · · 2026-01-31 22:21 UTC
parent
ff1b922
signature
unsigned
4 files · +191 -1
M internal/adapters/out/domainsecrets/helpers_test.go +19 -1
@@ -1,7 +1,25 @@
11 package domainsecrets
22
3-import "github.com/bnema/zerowrap"
3+import (
4+ "fmt"
5+ "testing"
6+
7+ "github.com/bnema/zerowrap"
8+)
49
510 func testLogger() zerowrap.Logger {
611 return zerowrap.Default()
712 }
13+
14+// CleanupPassAttachment removes attachment secrets from pass for testing.
15+// It's a test helper that should be called with defer to ensure cleanup.
16+// This function is exported to be shared across test packages.
17+func CleanupPassAttachment(_ *testing.T, containerName string, keys []string) {
18+ for _, key := range keys {
19+ path := fmt.Sprintf("%s/%s/%s", PassAttachmentPath, containerName, key)
20+ _ = passCmd("rm", "-f", path)
21+ }
22+
23+ manifestPath := fmt.Sprintf("%s/%s/.keys", PassAttachmentPath, containerName)
24+ _ = passCmd("rm", "-f", manifestPath)
25+}
@@ -1,7 +1,25 @@
1 package domainsecrets1 package domainsecrets
2 2
3-import "github.com/bnema/zerowrap"3+import (
4+ "fmt"
5+ "testing"
6+
7+ "github.com/bnema/zerowrap"
8+)
4 9
5 func testLogger() zerowrap.Logger {10 func testLogger() zerowrap.Logger {
6 return zerowrap.Default()11 return zerowrap.Default()
7 }12 }
13+
14+// CleanupPassAttachment removes attachment secrets from pass for testing.
15+// It's a test helper that should be called with defer to ensure cleanup.
16+// This function is exported to be shared across test packages.
17+func CleanupPassAttachment(_ *testing.T, containerName string, keys []string) {
18+ for _, key := range keys {
19+ path := fmt.Sprintf("%s/%s/%s", PassAttachmentPath, containerName, key)
20+ _ = passCmd("rm", "-f", path)
21+ }
22+
23+ manifestPath := fmt.Sprintf("%s/%s/.keys", PassAttachmentPath, containerName)
24+ _ = passCmd("rm", "-f", manifestPath)
25+}
M internal/adapters/out/domainsecrets/pass_store_test.go +47 -0
@@ -79,6 +79,53 @@ func TestPassStore_SetGetDelete(t *testing.T) {
7979 assert.ElementsMatch(t, []string{"API_KEY"}, keysList)
8080 }
8181
82+func TestPassStore_SetGetAttachment(t *testing.T) {
83+ requirePass(t)
84+
85+ store, err := NewPassStore(testLogger())
86+ require.NoError(t, err)
87+
88+ containerName := fmt.Sprintf("gitea-postgres-%d", time.Now().UnixNano())
89+ keys := []string{"POSTGRES_USER", "POSTGRES_PASSWORD"}
90+ defer CleanupPassAttachment(t, containerName, keys)
91+
92+ secretsMap := map[string]string{
93+ "POSTGRES_USER": "gitea",
94+ "POSTGRES_PASSWORD": "secret123",
95+ }
96+
97+ err = store.SetAttachment(containerName, secretsMap)
98+ require.NoError(t, err)
99+
100+ values, err := store.GetAllAttachment(containerName)
101+ require.NoError(t, err)
102+ assert.Equal(t, "gitea", values["POSTGRES_USER"])
103+ assert.Equal(t, "secret123", values["POSTGRES_PASSWORD"])
104+}
105+
106+func TestPassStore_ListAttachmentKeys_AfterSet(t *testing.T) {
107+ requirePass(t)
108+
109+ store, err := NewPassStore(testLogger())
110+ require.NoError(t, err)
111+
112+ containerName := fmt.Sprintf("redis-cache-%d", time.Now().UnixNano())
113+ keys := []string{"REDIS_PASSWORD"}
114+ defer CleanupPassAttachment(t, containerName, keys)
115+
116+ secretsMap := map[string]string{
117+ "REDIS_PASSWORD": "redis123",
118+ }
119+
120+ err = store.SetAttachment(containerName, secretsMap)
121+ require.NoError(t, err)
122+
123+ values, err := store.GetAllAttachment(containerName)
124+ require.NoError(t, err)
125+ assert.Len(t, values, 1)
126+ assert.Equal(t, "redis123", values["REDIS_PASSWORD"])
127+}
128+
82129 func TestParsePassListOutput(t *testing.T) {
83130 tests := []struct {
84131 name string
@@ -79,6 +79,53 @@ func TestPassStore_SetGetDelete(t *testing.T) {
79 assert.ElementsMatch(t, []string{"API_KEY"}, keysList)79 assert.ElementsMatch(t, []string{"API_KEY"}, keysList)
80 }80 }
81 81
82+func TestPassStore_SetGetAttachment(t *testing.T) {
83+ requirePass(t)
84+
85+ store, err := NewPassStore(testLogger())
86+ require.NoError(t, err)
87+
88+ containerName := fmt.Sprintf("gitea-postgres-%d", time.Now().UnixNano())
89+ keys := []string{"POSTGRES_USER", "POSTGRES_PASSWORD"}
90+ defer CleanupPassAttachment(t, containerName, keys)
91+
92+ secretsMap := map[string]string{
93+ "POSTGRES_USER": "gitea",
94+ "POSTGRES_PASSWORD": "secret123",
95+ }
96+
97+ err = store.SetAttachment(containerName, secretsMap)
98+ require.NoError(t, err)
99+
100+ values, err := store.GetAllAttachment(containerName)
101+ require.NoError(t, err)
102+ assert.Equal(t, "gitea", values["POSTGRES_USER"])
103+ assert.Equal(t, "secret123", values["POSTGRES_PASSWORD"])
104+}
105+
106+func TestPassStore_ListAttachmentKeys_AfterSet(t *testing.T) {
107+ requirePass(t)
108+
109+ store, err := NewPassStore(testLogger())
110+ require.NoError(t, err)
111+
112+ containerName := fmt.Sprintf("redis-cache-%d", time.Now().UnixNano())
113+ keys := []string{"REDIS_PASSWORD"}
114+ defer CleanupPassAttachment(t, containerName, keys)
115+
116+ secretsMap := map[string]string{
117+ "REDIS_PASSWORD": "redis123",
118+ }
119+
120+ err = store.SetAttachment(containerName, secretsMap)
121+ require.NoError(t, err)
122+
123+ values, err := store.GetAllAttachment(containerName)
124+ require.NoError(t, err)
125+ assert.Len(t, values, 1)
126+ assert.Equal(t, "redis123", values["REDIS_PASSWORD"])
127+}
128+
82 func TestParsePassListOutput(t *testing.T) {129 func TestParsePassListOutput(t *testing.T) {
83 tests := []struct {130 tests := []struct {
84 name string131 name string
M internal/boundaries/out/domain_secrets.go +6 -0
@@ -23,6 +23,12 @@ type DomainSecretStore interface {
2323 // Delete removes a specific secret key from a domain.
2424 Delete(domain, key string) error
2525
26+ // SetAttachment sets or updates multiple secrets for an attachment container.
27+ SetAttachment(containerName string, secrets map[string]string) error
28+
29+ // GetAllAttachment returns all secrets for an attachment container as a key-value map.
30+ GetAllAttachment(containerName string) (map[string]string, error)
31+
2632 // ListAttachmentKeys finds and returns secret keys for attachment containers
2733 // associated with the given domain. Returns a list of AttachmentSecrets, one
2834 // for each attachment that has secrets configured.
@@ -23,6 +23,12 @@ type DomainSecretStore interface {
23 // Delete removes a specific secret key from a domain.23 // Delete removes a specific secret key from a domain.
24 Delete(domain, key string) error24 Delete(domain, key string) error
25 25
26+ // SetAttachment sets or updates multiple secrets for an attachment container.
27+ SetAttachment(containerName string, secrets map[string]string) error
28+
29+ // GetAllAttachment returns all secrets for an attachment container as a key-value map.
30+ GetAllAttachment(containerName string) (map[string]string, error)
31+
26 // ListAttachmentKeys finds and returns secret keys for attachment containers32 // ListAttachmentKeys finds and returns secret keys for attachment containers
27 // associated with the given domain. Returns a list of AttachmentSecrets, one33 // associated with the given domain. Returns a list of AttachmentSecrets, one
28 // for each attachment that has secrets configured.34 // for each attachment that has secrets configured.
M internal/boundaries/out/mocks/mock_domain_secret_store.go +119 -0
@@ -155,6 +155,68 @@ func (_c *MockDomainSecretStore_GetAll_Call) RunAndReturn(run func(domain string
155155 return _c
156156 }
157157
158+// GetAllAttachment provides a mock function for the type MockDomainSecretStore
159+func (_mock *MockDomainSecretStore) GetAllAttachment(containerName string) (map[string]string, error) {
160+ ret := _mock.Called(containerName)
161+
162+ if len(ret) == 0 {
163+ panic("no return value specified for GetAllAttachment")
164+ }
165+
166+ var r0 map[string]string
167+ var r1 error
168+ if returnFunc, ok := ret.Get(0).(func(string) (map[string]string, error)); ok {
169+ return returnFunc(containerName)
170+ }
171+ if returnFunc, ok := ret.Get(0).(func(string) map[string]string); ok {
172+ r0 = returnFunc(containerName)
173+ } else {
174+ if ret.Get(0) != nil {
175+ r0 = ret.Get(0).(map[string]string)
176+ }
177+ }
178+ if returnFunc, ok := ret.Get(1).(func(string) error); ok {
179+ r1 = returnFunc(containerName)
180+ } else {
181+ r1 = ret.Error(1)
182+ }
183+ return r0, r1
184+}
185+
186+// MockDomainSecretStore_GetAllAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllAttachment'
187+type MockDomainSecretStore_GetAllAttachment_Call struct {
188+ *mock.Call
189+}
190+
191+// GetAllAttachment is a helper method to define mock.On call
192+// - containerName string
193+func (_e *MockDomainSecretStore_Expecter) GetAllAttachment(containerName interface{}) *MockDomainSecretStore_GetAllAttachment_Call {
194+ return &MockDomainSecretStore_GetAllAttachment_Call{Call: _e.mock.On("GetAllAttachment", containerName)}
195+}
196+
197+func (_c *MockDomainSecretStore_GetAllAttachment_Call) Run(run func(containerName string)) *MockDomainSecretStore_GetAllAttachment_Call {
198+ _c.Call.Run(func(args mock.Arguments) {
199+ var arg0 string
200+ if args[0] != nil {
201+ arg0 = args[0].(string)
202+ }
203+ run(
204+ arg0,
205+ )
206+ })
207+ return _c
208+}
209+
210+func (_c *MockDomainSecretStore_GetAllAttachment_Call) Return(stringToString map[string]string, err error) *MockDomainSecretStore_GetAllAttachment_Call {
211+ _c.Call.Return(stringToString, err)
212+ return _c
213+}
214+
215+func (_c *MockDomainSecretStore_GetAllAttachment_Call) RunAndReturn(run func(containerName string) (map[string]string, error)) *MockDomainSecretStore_GetAllAttachment_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
158220 // ListAttachmentKeys provides a mock function for the type MockDomainSecretStore
159221 func (_mock *MockDomainSecretStore) ListAttachmentKeys(domain string) ([]out.AttachmentSecrets, error) {
160222 ret := _mock.Called(domain)
@@ -335,3 +397,60 @@ func (_c *MockDomainSecretStore_Set_Call) RunAndReturn(run func(domain string, s
335397 _c.Call.Return(run)
336398 return _c
337399 }
400+
401+// SetAttachment provides a mock function for the type MockDomainSecretStore
402+func (_mock *MockDomainSecretStore) SetAttachment(containerName string, secrets map[string]string) error {
403+ ret := _mock.Called(containerName, secrets)
404+
405+ if len(ret) == 0 {
406+ panic("no return value specified for SetAttachment")
407+ }
408+
409+ var r0 error
410+ if returnFunc, ok := ret.Get(0).(func(string, map[string]string) error); ok {
411+ r0 = returnFunc(containerName, secrets)
412+ } else {
413+ r0 = ret.Error(0)
414+ }
415+ return r0
416+}
417+
418+// MockDomainSecretStore_SetAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAttachment'
419+type MockDomainSecretStore_SetAttachment_Call struct {
420+ *mock.Call
421+}
422+
423+// SetAttachment is a helper method to define mock.On call
424+// - containerName string
425+// - secrets map[string]string
426+func (_e *MockDomainSecretStore_Expecter) SetAttachment(containerName interface{}, secrets interface{}) *MockDomainSecretStore_SetAttachment_Call {
427+ return &MockDomainSecretStore_SetAttachment_Call{Call: _e.mock.On("SetAttachment", containerName, secrets)}
428+}
429+
430+func (_c *MockDomainSecretStore_SetAttachment_Call) Run(run func(containerName string, secrets map[string]string)) *MockDomainSecretStore_SetAttachment_Call {
431+ _c.Call.Run(func(args mock.Arguments) {
432+ var arg0 string
433+ if args[0] != nil {
434+ arg0 = args[0].(string)
435+ }
436+ var arg1 map[string]string
437+ if args[1] != nil {
438+ arg1 = args[1].(map[string]string)
439+ }
440+ run(
441+ arg0,
442+ arg1,
443+ )
444+ })
445+ return _c
446+}
447+
448+func (_c *MockDomainSecretStore_SetAttachment_Call) Return(err error) *MockDomainSecretStore_SetAttachment_Call {
449+ _c.Call.Return(err)
450+ return _c
451+}
452+
453+func (_c *MockDomainSecretStore_SetAttachment_Call) RunAndReturn(run func(containerName string, secrets map[string]string) error) *MockDomainSecretStore_SetAttachment_Call {
454+ _c.Call.Return(run)
455+ return _c
456+}
@@ -155,6 +155,68 @@ func (_c *MockDomainSecretStore_GetAll_Call) RunAndReturn(run func(domain string
155 return _c155 return _c
156 }156 }
157 157
158+// GetAllAttachment provides a mock function for the type MockDomainSecretStore
159+func (_mock *MockDomainSecretStore) GetAllAttachment(containerName string) (map[string]string, error) {
160+ ret := _mock.Called(containerName)
161+
162+ if len(ret) == 0 {
163+ panic("no return value specified for GetAllAttachment")
164+ }
165+
166+ var r0 map[string]string
167+ var r1 error
168+ if returnFunc, ok := ret.Get(0).(func(string) (map[string]string, error)); ok {
169+ return returnFunc(containerName)
170+ }
171+ if returnFunc, ok := ret.Get(0).(func(string) map[string]string); ok {
172+ r0 = returnFunc(containerName)
173+ } else {
174+ if ret.Get(0) != nil {
175+ r0 = ret.Get(0).(map[string]string)
176+ }
177+ }
178+ if returnFunc, ok := ret.Get(1).(func(string) error); ok {
179+ r1 = returnFunc(containerName)
180+ } else {
181+ r1 = ret.Error(1)
182+ }
183+ return r0, r1
184+}
185+
186+// MockDomainSecretStore_GetAllAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllAttachment'
187+type MockDomainSecretStore_GetAllAttachment_Call struct {
188+ *mock.Call
189+}
190+
191+// GetAllAttachment is a helper method to define mock.On call
192+// - containerName string
193+func (_e *MockDomainSecretStore_Expecter) GetAllAttachment(containerName interface{}) *MockDomainSecretStore_GetAllAttachment_Call {
194+ return &MockDomainSecretStore_GetAllAttachment_Call{Call: _e.mock.On("GetAllAttachment", containerName)}
195+}
196+
197+func (_c *MockDomainSecretStore_GetAllAttachment_Call) Run(run func(containerName string)) *MockDomainSecretStore_GetAllAttachment_Call {
198+ _c.Call.Run(func(args mock.Arguments) {
199+ var arg0 string
200+ if args[0] != nil {
201+ arg0 = args[0].(string)
202+ }
203+ run(
204+ arg0,
205+ )
206+ })
207+ return _c
208+}
209+
210+func (_c *MockDomainSecretStore_GetAllAttachment_Call) Return(stringToString map[string]string, err error) *MockDomainSecretStore_GetAllAttachment_Call {
211+ _c.Call.Return(stringToString, err)
212+ return _c
213+}
214+
215+func (_c *MockDomainSecretStore_GetAllAttachment_Call) RunAndReturn(run func(containerName string) (map[string]string, error)) *MockDomainSecretStore_GetAllAttachment_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
158 // ListAttachmentKeys provides a mock function for the type MockDomainSecretStore220 // ListAttachmentKeys provides a mock function for the type MockDomainSecretStore
159 func (_mock *MockDomainSecretStore) ListAttachmentKeys(domain string) ([]out.AttachmentSecrets, error) {221 func (_mock *MockDomainSecretStore) ListAttachmentKeys(domain string) ([]out.AttachmentSecrets, error) {
160 ret := _mock.Called(domain)222 ret := _mock.Called(domain)
@@ -335,3 +397,60 @@ func (_c *MockDomainSecretStore_Set_Call) RunAndReturn(run func(domain string, s
335 _c.Call.Return(run)397 _c.Call.Return(run)
336 return _c398 return _c
337 }399 }
400+
401+// SetAttachment provides a mock function for the type MockDomainSecretStore
402+func (_mock *MockDomainSecretStore) SetAttachment(containerName string, secrets map[string]string) error {
403+ ret := _mock.Called(containerName, secrets)
404+
405+ if len(ret) == 0 {
406+ panic("no return value specified for SetAttachment")
407+ }
408+
409+ var r0 error
410+ if returnFunc, ok := ret.Get(0).(func(string, map[string]string) error); ok {
411+ r0 = returnFunc(containerName, secrets)
412+ } else {
413+ r0 = ret.Error(0)
414+ }
415+ return r0
416+}
417+
418+// MockDomainSecretStore_SetAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAttachment'
419+type MockDomainSecretStore_SetAttachment_Call struct {
420+ *mock.Call
421+}
422+
423+// SetAttachment is a helper method to define mock.On call
424+// - containerName string
425+// - secrets map[string]string
426+func (_e *MockDomainSecretStore_Expecter) SetAttachment(containerName interface{}, secrets interface{}) *MockDomainSecretStore_SetAttachment_Call {
427+ return &MockDomainSecretStore_SetAttachment_Call{Call: _e.mock.On("SetAttachment", containerName, secrets)}
428+}
429+
430+func (_c *MockDomainSecretStore_SetAttachment_Call) Run(run func(containerName string, secrets map[string]string)) *MockDomainSecretStore_SetAttachment_Call {
431+ _c.Call.Run(func(args mock.Arguments) {
432+ var arg0 string
433+ if args[0] != nil {
434+ arg0 = args[0].(string)
435+ }
436+ var arg1 map[string]string
437+ if args[1] != nil {
438+ arg1 = args[1].(map[string]string)
439+ }
440+ run(
441+ arg0,
442+ arg1,
443+ )
444+ })
445+ return _c
446+}
447+
448+func (_c *MockDomainSecretStore_SetAttachment_Call) Return(err error) *MockDomainSecretStore_SetAttachment_Call {
449+ _c.Call.Return(err)
450+ return _c
451+}
452+
453+func (_c *MockDomainSecretStore_SetAttachment_Call) RunAndReturn(run func(containerName string, secrets map[string]string) error) *MockDomainSecretStore_SetAttachment_Call {
454+ _c.Call.Return(run)
455+ return _c
456+}

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