feat(secrets): initial support for 'x-command' and 'driver: exec' secrets, referenced as secret://name in environment (#403)

This commit is contained in:
Pasha Sviderski
2026-06-26 12:00:16 +10:00
committed by GitHub
parent e5b205391e
commit 925695a44a
7 changed files with 809 additions and 1 deletions
+138
View File
@@ -8,6 +8,7 @@ import (
"strings"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -346,3 +347,140 @@ volumes:
})
}
}
// TestLoadProject_Secrets covers loading and validation of all top-level secret source combinations.
func TestLoadProject_Secrets(t *testing.T) {
t.Parallel()
tests := []struct {
name string
secret string // YAML body under 'secrets.token'
want types.SecretConfig
errContains string
}{
// Valid sources.
{
name: "file source",
secret: " file: /tmp/token",
want: types.SecretConfig{File: "/tmp/token"},
},
{
name: "environment source",
secret: " environment: UNSET_SECRET_VAR",
want: types.SecretConfig{Environment: "UNSET_SECRET_VAR"},
},
{
name: "x-command short form expands to exec driver",
secret: " x-command: printf abc",
want: types.SecretConfig{Driver: "exec", DriverOpts: map[string]string{"command": "printf abc"}},
},
{
name: "exec driver long form",
secret: ` driver: exec
driver_opts:
command: printf abc`,
want: types.SecretConfig{Driver: "exec", DriverOpts: map[string]string{"command": "printf abc"}},
},
// Invalid combinations.
{
name: "x-command with driver",
secret: ` x-command: printf abc
driver: exec`,
errContains: "cannot be combined with 'driver'",
},
{
name: "x-command with driver_opts",
secret: ` x-command: printf abc
driver_opts:
command: printf abc`,
errContains: "cannot be combined with 'driver'",
},
{
name: "x-command with file",
secret: ` x-command: printf abc
file: /tmp/token`,
errContains: "cannot be combined with 'file' or 'environment'",
},
{
name: "x-command with environment",
secret: ` x-command: printf abc
environment: SOME_VAR`,
errContains: "cannot be combined with 'file' or 'environment'",
},
{
name: "x-command empty",
secret: ` x-command: ""`,
errContains: "must be a non-empty string",
},
{
name: "unsupported driver",
secret: ` driver: vault
driver_opts:
key: token`,
errContains: "unsupported driver 'vault'",
},
{
name: "exec driver without command",
secret: " driver: exec",
errContains: "requires 'driver_opts.command'",
},
{
name: "exec driver with file",
secret: ` driver: exec
driver_opts:
command: printf abc
file: /tmp/token`,
errContains: "cannot also define 'file' or 'environment'",
},
{
name: "external not supported",
secret: " external: true",
errContains: "external secrets are not supported",
},
{
name: "external with exec driver not supported",
secret: ` driver: exec
driver_opts:
command: printf abc
external: true`,
errContains: "external secrets are not supported",
},
{
name: "file and environment mutually exclusive",
secret: ` file: /tmp/token
environment: SOME_VAR`,
errContains: "mutually exclusive",
},
{
name: "no source",
secret: " name: token",
errContains: "must be set",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
content := `
services:
foo:
image: foo
secrets:
token:
` + tt.secret + "\n"
project, err := LoadProjectFromContent(context.Background(), content)
if tt.errContains != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.errContains)
return
}
require.NoError(t, err)
got := project.Secrets["token"]
got.Name = ""
assert.Equal(t, tt.want, got)
})
}
}