Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 16
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
d06a5ecac2c5e1c9b44856724b687f6fff71736c4d292648eFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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.
Uh oh!
There was an error while loading. Please reload this page.
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.
| 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)") | ||
CollaboratorAuthor 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 | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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.