Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/client/compose/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
29 changes: 29 additions & 0 deletions pkg/client/compose/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package compose

import (
"context"

"github.com/compose-spec/compose-go/v2/tree"
"github.com/psviderski/uncloud/pkg/client/secrets"
)

func setSecrets(data any, _ tree.Path, _ bool) (any, error) {
switch v := data.(type) {
case map[string]any:
// 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
}
39 changes: 39 additions & 0 deletions pkg/client/secrets/fnox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package secrets

import (
"context"
"os/exec"
"strings"
)

// 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.
type Fnox struct{}

const fnox = "fnox"

func (f *Fnox) Secrets(ctx context.Context, pattern string) (Secret, error) {
fields := strings.Split(pattern, "/")
var args []string
switch len(fields) {
case 1:
args = []string{"get", fields[0]}
case 2:
args = []string{"-P", fields[0], "get", fields[1]}
default:
return Secret{}, ErrNotFound
}

cmd := exec.CommandContext(ctx, fnox, args...)
out, err := cmd.Output()
if err != nil {
return Secret{}, ErrAccessDenied
}
return Secret{
ID: pattern,
Value: out,
Provider: fnox,
}, nil
}
54 changes: 54 additions & 0 deletions pkg/client/secrets/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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"
"errors"
"fmt"
"net/url"
"strings"
)

const Scheme = "uc"

// Secret is a secret as resolved by a providing plugin.
type Secret struct {
ID string
Value []byte
Provider string
}

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{
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://<provider>/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) (resolver Resolver, pattern string, err error) {
u, err := url.Parse(pointer)
if err != nil {
return nil, "", err
}
if u.Scheme != Scheme {
return nil, "", fmt.Errorf("unknown scheme: %s", u.Scheme)
}
r, ok := providers[u.Hostname()]
if !ok {
return nil, "", fmt.Errorf("%s %w", u.Hostname(), ErrNoProvider)
}

return r, strings.TrimPrefix(u.Path, "/"), nil
}
Loading