-
Notifications
You must be signed in to change notification settings - Fork 6
feat(sdk): static stub configuration #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d06a5ec
ac2c5e1
c9b4485
6724b68
7f6fff7
1736c4d
292648e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package adaptation | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "time" | ||
| ) | ||
|
|
||
| const ( | ||
| // PluginLaunchedByEngineVar is used to inform engine-launched plugins about their name. | ||
| PluginLaunchedByEngineVar = "DOCKER_SECRETS_ENGINE_PLUGIN_LAUNCH_CFG" | ||
| // DefaultPluginRegistrationTimeout is the default timeout for plugin registration. | ||
| DefaultPluginRegistrationTimeout = 5 * time.Second | ||
| ) | ||
|
|
||
| func DefaultSocketPath() string { | ||
| if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" { | ||
| return filepath.Join(dir, "secrets-engine", "engine.sock") | ||
| } | ||
| if home, err := os.UserHomeDir(); err == nil { | ||
| return filepath.Join(home, ".cache", "secrets-engine", "engine.sock") | ||
| } | ||
| return filepath.Join(os.TempDir(), "secrets-engine", "engine.sock") | ||
| } | ||
|
|
||
| type PluginConfigFromEngine struct { | ||
| Name string `json:"name"` | ||
| RegistrationTimeout time.Duration `json:"timeout"` | ||
| Fd int `json:"fd"` | ||
| } | ||
|
|
||
| func (c *PluginConfigFromEngine) ToString() (string, error) { | ||
| result, err := json.Marshal(c) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return string(result), nil | ||
| } | ||
|
|
||
| func NewPluginConfigFromEngineEnv(in string) (*PluginConfigFromEngine, error) { | ||
| var result PluginConfigFromEngine | ||
| if err := json.Unmarshal([]byte(in), &result); err != nil { | ||
| return nil, fmt.Errorf("failed to decode plugin config from engine %q: %w", PluginLaunchedByEngineVar, err) | ||
| } | ||
| if result.Name == "" { | ||
| return nil, errors.New("plugin name is required") | ||
| } | ||
| if result.RegistrationTimeout == 0 { | ||
| return nil, errors.New("plugin registration timeout is required") | ||
| } | ||
| if result.Fd <= 2 { | ||
| // File descriptors 0, 1, and 2 are reserved for stdin, stdout, and stderr. | ||
| return nil, errors.New("invalid file descriptor for plugin connection") | ||
| } | ||
| return &result, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package adaptation | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestPluginConfigFromEngine_ToString(t *testing.T) { | ||
| in := PluginConfigFromEngine{ | ||
| Name: strings.Repeat("ab", 250), // 500 characters | ||
| RegistrationTimeout: 27 * time.Nanosecond, | ||
| Fd: 10, | ||
| } | ||
| out, err := in.ToString() | ||
| assert.NoError(t, err) | ||
| // This is coming from here: https://superuser.com/questions/1070272/why-does-windows-have-a-limit-on-environment-variables-at-all | ||
| // -> we verify that a plugin name of 500 characters is still within the limit | ||
| assert.LessOrEqual(t, len(out), 2048) | ||
| restored, err := NewPluginConfigFromEngineEnv(out) | ||
| assert.NoError(t, err) | ||
| assert.Equal(t, in, *restored) | ||
| } | ||
|
|
||
| func TestNewPluginConfigFromEngineFromString(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| in PluginConfigFromEngine | ||
| err string | ||
| }{ | ||
| { | ||
| name: "name is empty", | ||
| err: "name is required", | ||
| }, | ||
| { | ||
| name: "registration timeout is zero", | ||
| in: PluginConfigFromEngine{ | ||
| Name: "test-plugin", | ||
| }, | ||
| err: "registration timeout is required", | ||
| }, | ||
| { | ||
| name: "fd is nonsense", | ||
| in: PluginConfigFromEngine{ | ||
| Name: "test-plugin", | ||
| RegistrationTimeout: 10 * time.Second, | ||
| Fd: 2, | ||
| }, | ||
| err: "invalid file descriptor", | ||
| }, | ||
| { | ||
| name: "valid config", | ||
| in: PluginConfigFromEngine{ | ||
| Name: "test-plugin", | ||
| RegistrationTimeout: 10 * time.Second, | ||
| Fd: 10, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| out, err := tt.in.ToString() | ||
| require.NoError(t, err) | ||
| _, err = NewPluginConfigFromEngineEnv(out) | ||
| if tt.err != "" { | ||
| assert.ErrorContains(t, err, tt.err) | ||
| } else { | ||
| assert.NoError(t, err) | ||
| } | ||
| }) | ||
| } | ||
| } |
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| package stub | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "net" | ||
| "os" | ||
| "path/filepath" | ||
| "strconv" | ||
| "time" | ||
|
|
||
| "github.com/docker/secrets-engine/pkg/adaptation" | ||
| ) | ||
|
|
||
| // ManualLaunchOption to apply to a plugin during its creation | ||
| // when it's manually launched (not by the secrets engine). | ||
| type ManualLaunchOption func(c *cfg) error | ||
|
|
||
| // WithPluginName sets the name to use in plugin registration. | ||
| func WithPluginName(name string) ManualLaunchOption { | ||
| return func(s *cfg) error { | ||
| if name == "" { | ||
| return errors.New("plugin name cannot be empty") | ||
| } | ||
| s.name = name | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // WithRegistrationTimeout sets custom registration timeout. | ||
| func WithRegistrationTimeout(timeout time.Duration) ManualLaunchOption { | ||
| return func(s *cfg) error { | ||
| s.registrationTimeout = timeout | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // WithConnection sets an existing secrets engine connection to use. | ||
| func WithConnection(conn net.Conn) ManualLaunchOption { | ||
| return func(s *cfg) error { | ||
| if s.conn != nil { | ||
| return errors.New("connection already set") | ||
| } | ||
| s.conn = conn | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| type cfg struct { | ||
| plugin Plugin | ||
| name string | ||
| conn net.Conn | ||
| registrationTimeout time.Duration | ||
| } | ||
|
|
||
| func newCfg(p Plugin, opts ...ManualLaunchOption) (*cfg, error) { | ||
| engineCfg, err := restoreConfig(p) | ||
| if errors.Is(err, errPluginNotLaunchedByEngine) { | ||
| return newCfgForManualLaunch(p, opts...) | ||
| } | ||
| return engineCfg, err | ||
| } | ||
|
|
||
| func newCfgForManualLaunch(p Plugin, opts ...ManualLaunchOption) (*cfg, error) { | ||
| cfg := &cfg{ | ||
| plugin: p, | ||
| registrationTimeout: adaptation.DefaultPluginRegistrationTimeout, | ||
| } | ||
| for _, o := range opts { | ||
| if err := o(cfg); err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
| if cfg.conn == nil { | ||
| defaultSocketPath := adaptation.DefaultSocketPath() | ||
| conn, err := net.Dial("unix", defaultSocketPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to connect to default socket %q: %w", defaultSocketPath, err) | ||
| } | ||
| cfg.conn = conn | ||
| } | ||
| if cfg.name == "" { | ||
| if len(os.Args) == 0 { | ||
| // This should never happen in practice but can happen in tests or when something else empties os.Args for whatever reason. | ||
| return nil, errors.New("plugin name must be specified (could not derive from os.Args)") | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit unfortunate. But I believe eg |
||
| } | ||
| cfg.name = filepath.Base(os.Args[0]) | ||
| } | ||
| return cfg, nil | ||
| } | ||
|
|
||
| var ( | ||
| errPluginNotLaunchedByEngine = errors.New("plugin not launched by secrets engine") | ||
| ) | ||
|
|
||
| func restoreConfig(p Plugin) (*cfg, error) { | ||
| cfgString := os.Getenv(adaptation.PluginLaunchedByEngineVar) | ||
| if cfgString == "" { | ||
| return nil, errPluginNotLaunchedByEngine | ||
| } | ||
| c, err := adaptation.NewPluginConfigFromEngineEnv(cfgString) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| conn, err := connectionFromFileDescriptor(c.Fd) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("invalid socket (%d) in environment: %w", c.Fd, err) | ||
| } | ||
| return &cfg{ | ||
| plugin: p, | ||
| name: c.Name, | ||
| conn: conn, | ||
| registrationTimeout: c.RegistrationTimeout, | ||
| }, nil | ||
| } | ||
|
|
||
| func connectionFromFileDescriptor(fd int) (net.Conn, error) { | ||
| f := os.NewFile(uintptr(fd), "fd #"+strconv.Itoa(fd)) | ||
| if f == nil { | ||
| return nil, fmt.Errorf("failed to open FD %d", fd) | ||
| } | ||
| defer f.Close() | ||
| conn, err := net.FileConn(f) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create net.Conn for fd #%d: %w", fd, err) | ||
| } | ||
| return conn, nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder if it's necessary to error on a timeout value, perhaps just fallback to a sane default instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep for now. In NRI they didn't error on this which made them miss that in NRI configuring the registration timeout is actually bugged / doesn't work.