From 52cfec58ab725faacab82c0f1a1540eed4487062 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Thu, 18 Jun 2026 11:51:31 +0200 Subject: [PATCH 1/6] added Signed-off-by: Miek Gieben --- pkg/client/compose/secrets.go | 12 ++++++++++++ pkg/secrets/secrets.go | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 pkg/client/compose/secrets.go create mode 100644 pkg/secrets/secrets.go diff --git a/pkg/client/compose/secrets.go b/pkg/client/compose/secrets.go new file mode 100644 index 00000000..70d13672 --- /dev/null +++ b/pkg/client/compose/secrets.go @@ -0,0 +1,12 @@ +package compose + +import ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func secretsEngine(data any, _ tree.Path, _ bool) (any, error) { + fmt.Printf("HALLO %v\n", data) + return data, nil +} diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go new file mode 100644 index 00000000..2972af21 --- /dev/null +++ b/pkg/secrets/secrets.go @@ -0,0 +1,20 @@ +// Package secrets defines the interface to 3rd party secret managers. It resolves secrets to their actual +// data. This only reads secrets, settings and updating them is left out. +package secrets + +import "context" + +// Secret is a secret as resolved by a providing plugin. +type Secret struct { + ID string + Value []byte + Provider string +} + +// Providers is the list of plugins we have to resolve a secret. +var Providers = []string{"fnox"} + +// Resolver is the interface all providers should implement. +type Resolver interface { + Secrets(ctx context.Context, pattern string) ([]Secret, error) +} From e5fda74db725668813718a2d9ce08ee404b99696 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Thu, 18 Jun 2026 12:18:24 +0200 Subject: [PATCH 2/6] secret engine Signed-off-by: Miek Gieben --- pkg/client/compose/project.go | 1 + pkg/client/compose/secrets.go | 9 ++++++-- pkg/secrets/fnox.go | 42 +++++++++++++++++++++++++++++++++++ pkg/secrets/secrets.go | 34 ++++++++++++++++++++++++++-- 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 pkg/secrets/fnox.go diff --git a/pkg/client/compose/project.go b/pkg/client/compose/project.go index 3d29d653..e042b8e0 100644 --- a/pkg/client/compose/project.go +++ b/pkg/client/compose/project.go @@ -23,6 +23,7 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project registerComposeOverrides.Do(func() { transform.RegisterDefaultValue("services.*.deploy.update_config", setUpdateConfigDefaults) transform.RegisterDefaultValue("services.*.volumes.*.source", checkRelativeVolumeMount) + transform.RegisterDefaultValue("services.*.environment", setSecrets) }) defaultOpts := []composecli.ProjectOptionsFn{ diff --git a/pkg/client/compose/secrets.go b/pkg/client/compose/secrets.go index 70d13672..eaf90a28 100644 --- a/pkg/client/compose/secrets.go +++ b/pkg/client/compose/secrets.go @@ -6,7 +6,12 @@ import ( "github.com/compose-spec/compose-go/v2/tree" ) -func secretsEngine(data any, _ tree.Path, _ bool) (any, error) { - fmt.Printf("HALLO %v\n", data) +func setSecrets(data any, _ tree.Path, _ bool) (any, error) { + switch v := data.(type) { + case map[string]any: + for name, secret := range v { + fmt.Printf("%s %s\n", name, secret) + } + } return data, nil } diff --git a/pkg/secrets/fnox.go b/pkg/secrets/fnox.go new file mode 100644 index 00000000..537a04ed --- /dev/null +++ b/pkg/secrets/fnox.go @@ -0,0 +1,42 @@ +package secrets + +import ( + "context" + "os/exec" + "strings" +) + +// Fnox implements the use of the fnox secret manager. +// +// The pattern uc://fnox/profile/foo, will end up there as profile/foo. Where profile is optional, so +// foo is valid as well. +type Fnox struct{} + +const fnox = "fnox" + +func (f *Fnox) Secrets(ctx context.Context, pattern string) ([]Secret, error) { + fields := strings.Split(pattern, "/") + args := []string{} + switch len(fields) { + case 1: + args = []string{"get", fields[0]} + case 2: + args = []string{"-P", fields[0], "get", fields[1]} + default: + return nil, ErrNotFound + } + + cmd := exec.CommandContext(ctx, fnox, args...) + out, err := cmd.Output() + if err != nil { + return nil, ErrAccessDenied + } + // only support a single secret + return []Secret{ + { + ID: pattern, + Value: out, + Provider: fnox, + }, + }, nil +} diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go index 2972af21..e517f30f 100644 --- a/pkg/secrets/secrets.go +++ b/pkg/secrets/secrets.go @@ -2,7 +2,15 @@ // data. This only reads secrets, settings and updating them is left out. package secrets -import "context" +import ( + "context" + "errors" + "fmt" + "net/url" + "path" +) + +const Scheme = "uc" // Secret is a secret as resolved by a providing plugin. type Secret struct { @@ -11,10 +19,32 @@ type Secret struct { Provider string } +var ( + ErrNotFound = errors.New("secret not found") + ErrAccessDenied = errors.New("access denied") +) + // Providers is the list of plugins we have to resolve a secret. -var Providers = []string{"fnox"} +var Providers = map[string]Resolver{ + fnox: &Fnox{}, +} // Resolver is the interface all providers should implement. type Resolver interface { Secrets(ctx context.Context, pattern string) ([]Secret, error) } + +// Parse parses a pattern uc:///bla/foo and returns the provider +// and the secrets pattern that is used. The provider implementation knows how to deal with the pattern. +func Parse(pointer string) (provider, pattern string, err error) { + u, err := url.Parse(pointer) + if err != nil { + return "", "", err + } + if u.Scheme != Scheme { + return "", "", fmt.Errorf("unknown scheme: %s", u.Scheme) + } + // first path element is provider, rest is the key's pattern. + provider, pattern = path.Split(u.Path) + return provider, pattern, nil +} From ba00b64ee63fe36102a2a724765e635b1aab8707 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Thu, 18 Jun 2026 13:38:05 +0200 Subject: [PATCH 3/6] Add secrets engine A secret can be specified with `uc:///`. As an example we support 'fnox', so `uc://fnox//my_secret will use fnox to resolve `my_secret` With ```yaml services: service1: environment: BLA: uc://fnox/geheim ``` with set the environment var `BLA` to the contents of the secret. Any errors will stop the deploy. Interactive use, i.e. enter a password will probably not work. Signed-off-by: Miek Gieben --- pkg/client/compose/secrets.go | 18 +++++++++++++++--- pkg/secrets/fnox.go | 18 ++++++++---------- pkg/secrets/secrets.go | 23 +++++++++++++---------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/pkg/client/compose/secrets.go b/pkg/client/compose/secrets.go index eaf90a28..a2e14b26 100644 --- a/pkg/client/compose/secrets.go +++ b/pkg/client/compose/secrets.go @@ -1,16 +1,28 @@ package compose import ( - "fmt" + "context" "github.com/compose-spec/compose-go/v2/tree" + "github.com/psviderski/uncloud/pkg/secrets" ) func setSecrets(data any, _ tree.Path, _ bool) (any, error) { switch v := data.(type) { case map[string]any: - for name, secret := range v { - fmt.Printf("%s %s\n", name, secret) + // environment + for name, pattern := range v { + if x, ok := pattern.(string); ok { + resolver, pattern, err := secrets.Parse(x) + if err != nil { + continue + } + secret, err := resolver.Secrets(context.TODO(), pattern) + if err != nil { + return data, err + } + v[name] = secret.Value + } } } return data, nil diff --git a/pkg/secrets/fnox.go b/pkg/secrets/fnox.go index 537a04ed..655464d0 100644 --- a/pkg/secrets/fnox.go +++ b/pkg/secrets/fnox.go @@ -6,7 +6,7 @@ import ( "strings" ) -// Fnox implements the use of the fnox secret manager. +// Fnox implements the use of the fnox secret manager. It calls fnox directly so that should be set up. // // The pattern uc://fnox/profile/foo, will end up there as profile/foo. Where profile is optional, so // foo is valid as well. @@ -14,7 +14,7 @@ type Fnox struct{} const fnox = "fnox" -func (f *Fnox) Secrets(ctx context.Context, pattern string) ([]Secret, error) { +func (f *Fnox) Secrets(ctx context.Context, pattern string) (Secret, error) { fields := strings.Split(pattern, "/") args := []string{} switch len(fields) { @@ -23,20 +23,18 @@ func (f *Fnox) Secrets(ctx context.Context, pattern string) ([]Secret, error) { case 2: args = []string{"-P", fields[0], "get", fields[1]} default: - return nil, ErrNotFound + return Secret{}, ErrNotFound } cmd := exec.CommandContext(ctx, fnox, args...) out, err := cmd.Output() if err != nil { - return nil, ErrAccessDenied + return Secret{}, ErrAccessDenied } // only support a single secret - return []Secret{ - { - ID: pattern, - Value: out, - Provider: fnox, - }, + return Secret{ + ID: pattern, + Value: out, + Provider: fnox, }, nil } diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go index e517f30f..77260424 100644 --- a/pkg/secrets/secrets.go +++ b/pkg/secrets/secrets.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "net/url" - "path" ) const Scheme = "uc" @@ -22,29 +21,33 @@ type Secret struct { var ( ErrNotFound = errors.New("secret not found") ErrAccessDenied = errors.New("access denied") + ErrNoProvider = errors.New("provider not found") ) -// Providers is the list of plugins we have to resolve a secret. -var Providers = map[string]Resolver{ +// providers is the list of plugins we have to resolve a secret. +var providers = map[string]Resolver{ fnox: &Fnox{}, } // Resolver is the interface all providers should implement. type Resolver interface { - Secrets(ctx context.Context, pattern string) ([]Secret, error) + Secrets(ctx context.Context, pattern string) (Secret, error) } // Parse parses a pattern uc:///bla/foo and returns the provider // and the secrets pattern that is used. The provider implementation knows how to deal with the pattern. -func Parse(pointer string) (provider, pattern string, err error) { +func Parse(pointer string) (resolver Resolver, pattern string, err error) { u, err := url.Parse(pointer) if err != nil { - return "", "", err + return nil, "", err } if u.Scheme != Scheme { - return "", "", fmt.Errorf("unknown scheme: %s", u.Scheme) + return nil, "", fmt.Errorf("unknown scheme: %s", u.Scheme) } - // first path element is provider, rest is the key's pattern. - provider, pattern = path.Split(u.Path) - return provider, pattern, nil + r, ok := providers[u.Hostname()] + if !ok { + return nil, "", fmt.Errorf("%s %w", u.Hostname(), ErrNoProvider) + } + + return r, u.Path, nil } From 25af3f700a4f0d7995374551278cdb6b7024039a Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Thu, 18 Jun 2026 13:46:13 +0200 Subject: [PATCH 4/6] typo Signed-off-by: Miek Gieben --- pkg/secrets/fnox.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/secrets/fnox.go b/pkg/secrets/fnox.go index 655464d0..66e0b0f2 100644 --- a/pkg/secrets/fnox.go +++ b/pkg/secrets/fnox.go @@ -31,7 +31,6 @@ func (f *Fnox) Secrets(ctx context.Context, pattern string) (Secret, error) { if err != nil { return Secret{}, ErrAccessDenied } - // only support a single secret return Secret{ ID: pattern, Value: out, From 831307065a7b16fe3475999885e5239de622cfe8 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Thu, 18 Jun 2026 17:34:18 +0200 Subject: [PATCH 5/6] slightly better Signed-off-by: Miek Gieben --- pkg/secrets/fnox.go | 2 +- pkg/secrets/secrets.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/secrets/fnox.go b/pkg/secrets/fnox.go index 66e0b0f2..9dba8401 100644 --- a/pkg/secrets/fnox.go +++ b/pkg/secrets/fnox.go @@ -16,7 +16,7 @@ const fnox = "fnox" func (f *Fnox) Secrets(ctx context.Context, pattern string) (Secret, error) { fields := strings.Split(pattern, "/") - args := []string{} + var args []string switch len(fields) { case 1: args = []string{"get", fields[0]} diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go index 77260424..fcccfa84 100644 --- a/pkg/secrets/secrets.go +++ b/pkg/secrets/secrets.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net/url" + "strings" ) const Scheme = "uc" @@ -49,5 +50,5 @@ func Parse(pointer string) (resolver Resolver, pattern string, err error) { return nil, "", fmt.Errorf("%s %w", u.Hostname(), ErrNoProvider) } - return r, u.Path, nil + return r, strings.TrimPrefix(u.Path, "/"), nil } From d03fb1063b1cafae3542e053f47cce18390d47e5 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Thu, 18 Jun 2026 21:30:05 +0200 Subject: [PATCH 6/6] move in better place Signed-off-by: Miek Gieben --- pkg/client/compose/secrets.go | 2 +- pkg/{ => client}/secrets/fnox.go | 0 pkg/{ => client}/secrets/secrets.go | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename pkg/{ => client}/secrets/fnox.go (100%) rename pkg/{ => client}/secrets/secrets.go (100%) diff --git a/pkg/client/compose/secrets.go b/pkg/client/compose/secrets.go index a2e14b26..e48159a7 100644 --- a/pkg/client/compose/secrets.go +++ b/pkg/client/compose/secrets.go @@ -4,7 +4,7 @@ import ( "context" "github.com/compose-spec/compose-go/v2/tree" - "github.com/psviderski/uncloud/pkg/secrets" + "github.com/psviderski/uncloud/pkg/client/secrets" ) func setSecrets(data any, _ tree.Path, _ bool) (any, error) { diff --git a/pkg/secrets/fnox.go b/pkg/client/secrets/fnox.go similarity index 100% rename from pkg/secrets/fnox.go rename to pkg/client/secrets/fnox.go diff --git a/pkg/secrets/secrets.go b/pkg/client/secrets/secrets.go similarity index 100% rename from pkg/secrets/secrets.go rename to pkg/client/secrets/secrets.go