detached bb9656b

fix(secrets): address review feedback

- Return error instead of nil on invalid KEY=value format in CLI
- Fix docs to use 'gordon deploy' instead of 'gordon push'
- Add Docker 255-char limit protection to resolveContainerName
- Add tests for service name length (63 chars) and trailing hyphen validation
commit
bb9656b70719bcb7788ca708e7c93c92473df92c
author
brice · · 2026-02-06 12:45 UTC
parent
95aa1f7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +51 -4
M docs/cli/secrets.md +1 -1
@@ -212,7 +212,7 @@ gordon secrets set app.mydomain.com -a redis REDIS_PASSWORD=cache-secret
212212 gordon secrets list app.mydomain.com
213213
214214 # Redeploy to pick up new secrets
215-gordon push app.mydomain.com
215+gordon deploy app.mydomain.com
216216 ```
217217
218218 ## Related
@@ -212,7 +212,7 @@ gordon secrets set app.mydomain.com -a redis REDIS_PASSWORD=cache-secret
212 gordon secrets list app.mydomain.com212 gordon secrets list app.mydomain.com
213 213
214 # Redeploy to pick up new secrets214 # Redeploy to pick up new secrets
215-gordon push app.mydomain.com215+gordon deploy app.mydomain.com
216 ```216 ```
217 217
218 ## Related218 ## Related
M internal/adapters/in/cli/secrets.go +1 -2
@@ -242,8 +242,7 @@ Examples:
242242 for _, pair := range pairs {
243243 parts := strings.SplitN(pair, "=", 2)
244244 if len(parts) != 2 {
245- fmt.Println(styles.RenderError(fmt.Sprintf("Invalid format: %s (expected KEY=value)", pair)))
246- return nil
245+ return fmt.Errorf("invalid format: %s (expected KEY=value)", pair)
247246 }
248247 secrets[parts[0]] = parts[1]
249248 }
@@ -242,8 +242,7 @@ Examples:
242 for _, pair := range pairs {242 for _, pair := range pairs {
243 parts := strings.SplitN(pair, "=", 2)243 parts := strings.SplitN(pair, "=", 2)
244 if len(parts) != 2 {244 if len(parts) != 2 {
245- fmt.Println(styles.RenderError(fmt.Sprintf("Invalid format: %s (expected KEY=value)", pair)))245+ return fmt.Errorf("invalid format: %s (expected KEY=value)", pair)
246- return nil
247 }246 }
248 secrets[parts[0]] = parts[1]247 secrets[parts[0]] = parts[1]
249 }248 }
M internal/usecase/secrets/service.go +13 -1
@@ -260,8 +260,20 @@ func validateServiceName(service string) error {
260260 }
261261
262262 // resolveContainerName builds the container name from a domain and service.
263+// Ensures the total name does not exceed Docker's 255-character limit.
263264 func resolveContainerName(domainName, service string) string {
264- return domain.SanitizeDomainForContainer(domainName) + "-" + service
265+ sanitized := domain.SanitizeDomainForContainer(domainName)
266+ name := sanitized + "-" + service
267+
268+ // Docker container names are limited to 255 characters
269+ if len(name) > 255 {
270+ // Truncate the domain part to fit, keeping room for "-" and service name
271+ maxDomainLen := 255 - 1 - len(service)
272+ if maxDomainLen > 0 {
273+ name = sanitized[:maxDomainLen] + "-" + service
274+ }
275+ }
276+ return name
265277 }
266278
267279 // ValidateDomain validates that a domain is safe to use for secret storage.
@@ -260,8 +260,20 @@ func validateServiceName(service string) error {
260 }260 }
261 261
262 // resolveContainerName builds the container name from a domain and service.262 // resolveContainerName builds the container name from a domain and service.
263+// Ensures the total name does not exceed Docker's 255-character limit.
263 func resolveContainerName(domainName, service string) string {264 func resolveContainerName(domainName, service string) string {
264- return domain.SanitizeDomainForContainer(domainName) + "-" + service265+ sanitized := domain.SanitizeDomainForContainer(domainName)
266+ name := sanitized + "-" + service
267+
268+ // Docker container names are limited to 255 characters
269+ if len(name) > 255 {
270+ // Truncate the domain part to fit, keeping room for "-" and service name
271+ maxDomainLen := 255 - 1 - len(service)
272+ if maxDomainLen > 0 {
273+ name = sanitized[:maxDomainLen] + "-" + service
274+ }
275+ }
276+ return name
265 }277 }
266 278
267 // ValidateDomain validates that a domain is safe to use for secret storage.279 // ValidateDomain validates that a domain is safe to use for secret storage.
M internal/usecase/secrets/service_test.go +36 -0
@@ -529,6 +529,42 @@ func TestService_DeleteAttachment_InvalidServiceName(t *testing.T) {
529529 assert.ErrorIs(t, err, ErrInvalidServiceName)
530530 }
531531
532+func TestService_SetAttachment_ServiceNameTooLong(t *testing.T) {
533+ store := outmocks.NewMockDomainSecretStore(t)
534+ svc := NewService(store, testLogger())
535+
536+ // Service name exceeds DNS label limit of 63 characters
537+ longService := strings.Repeat("a", 64)
538+ err := svc.SetAttachment(context.Background(), "app.example.com", longService, map[string]string{"K": "V"})
539+ assert.ErrorIs(t, err, ErrInvalidServiceName)
540+}
541+
542+func TestService_SetAttachment_ServiceNameTrailingHyphen(t *testing.T) {
543+ store := outmocks.NewMockDomainSecretStore(t)
544+ svc := NewService(store, testLogger())
545+
546+ err := svc.SetAttachment(context.Background(), "app.example.com", "postgres-", map[string]string{"K": "V"})
547+ assert.ErrorIs(t, err, ErrInvalidServiceName)
548+}
549+
550+func TestService_DeleteAttachment_ServiceNameTooLong(t *testing.T) {
551+ store := outmocks.NewMockDomainSecretStore(t)
552+ svc := NewService(store, testLogger())
553+
554+ // Service name exceeds DNS label limit of 63 characters
555+ longService := strings.Repeat("a", 64)
556+ err := svc.DeleteAttachment(context.Background(), "app.example.com", longService, "SOME_KEY")
557+ assert.ErrorIs(t, err, ErrInvalidServiceName)
558+}
559+
560+func TestService_DeleteAttachment_ServiceNameTrailingHyphen(t *testing.T) {
561+ store := outmocks.NewMockDomainSecretStore(t)
562+ svc := NewService(store, testLogger())
563+
564+ err := svc.DeleteAttachment(context.Background(), "app.example.com", "redis-", "SOME_KEY")
565+ assert.ErrorIs(t, err, ErrInvalidServiceName)
566+}
567+
532568 func TestService_AttachmentValidationHappensBeforeStore(t *testing.T) {
533569 store := outmocks.NewMockDomainSecretStore(t)
534570 svc := NewService(store, testLogger())
@@ -529,6 +529,42 @@ func TestService_DeleteAttachment_InvalidServiceName(t *testing.T) {
529 assert.ErrorIs(t, err, ErrInvalidServiceName)529 assert.ErrorIs(t, err, ErrInvalidServiceName)
530 }530 }
531 531
532+func TestService_SetAttachment_ServiceNameTooLong(t *testing.T) {
533+ store := outmocks.NewMockDomainSecretStore(t)
534+ svc := NewService(store, testLogger())
535+
536+ // Service name exceeds DNS label limit of 63 characters
537+ longService := strings.Repeat("a", 64)
538+ err := svc.SetAttachment(context.Background(), "app.example.com", longService, map[string]string{"K": "V"})
539+ assert.ErrorIs(t, err, ErrInvalidServiceName)
540+}
541+
542+func TestService_SetAttachment_ServiceNameTrailingHyphen(t *testing.T) {
543+ store := outmocks.NewMockDomainSecretStore(t)
544+ svc := NewService(store, testLogger())
545+
546+ err := svc.SetAttachment(context.Background(), "app.example.com", "postgres-", map[string]string{"K": "V"})
547+ assert.ErrorIs(t, err, ErrInvalidServiceName)
548+}
549+
550+func TestService_DeleteAttachment_ServiceNameTooLong(t *testing.T) {
551+ store := outmocks.NewMockDomainSecretStore(t)
552+ svc := NewService(store, testLogger())
553+
554+ // Service name exceeds DNS label limit of 63 characters
555+ longService := strings.Repeat("a", 64)
556+ err := svc.DeleteAttachment(context.Background(), "app.example.com", longService, "SOME_KEY")
557+ assert.ErrorIs(t, err, ErrInvalidServiceName)
558+}
559+
560+func TestService_DeleteAttachment_ServiceNameTrailingHyphen(t *testing.T) {
561+ store := outmocks.NewMockDomainSecretStore(t)
562+ svc := NewService(store, testLogger())
563+
564+ err := svc.DeleteAttachment(context.Background(), "app.example.com", "redis-", "SOME_KEY")
565+ assert.ErrorIs(t, err, ErrInvalidServiceName)
566+}
567+
532 func TestService_AttachmentValidationHappensBeforeStore(t *testing.T) {568 func TestService_AttachmentValidationHappensBeforeStore(t *testing.T) {
533 store := outmocks.NewMockDomainSecretStore(t)569 store := outmocks.NewMockDomainSecretStore(t)
534 svc := NewService(store, testLogger())570 svc := NewService(store, testLogger())

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