Skip to content
Merged
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
84 changes: 65 additions & 19 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import (
"github.com/docker/secrets-engine/x/api"
healthv1 "github.com/docker/secrets-engine/x/api/health/v1"
"github.com/docker/secrets-engine/x/api/health/v1/healthv1connect"
pluginsv1 "github.com/docker/secrets-engine/x/api/plugins/v1"
"github.com/docker/secrets-engine/x/api/plugins/v1/pluginsv1connect"
"github.com/docker/secrets-engine/x/api/resolver"
v1 "github.com/docker/secrets-engine/x/api/resolver/v1"
"github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect"
"github.com/docker/secrets-engine/x/secrets"
)

Expand Down Expand Up @@ -83,7 +83,7 @@ func WithDialContext(dialContext func(ctx context.Context, network, addr string)
// It is useful to set if there are hard-limits to when the client must wait
// for the server to accept the request.
//
// A timout of 0 means no request timeout will be applied.
// A timeout of 0 means no request timeout will be applied.
// Negative durations are not allowed and will result in an error.
func WithTimeout(timeout time.Duration) Option {
return func(s *config) error {
Expand Down Expand Up @@ -120,9 +120,14 @@ type config struct {
responseTimeout time.Duration
}

var (
_ Client = &client{}
_ PluginManagement = &client{}
)

type client struct {
resolverClient secrets.Resolver
listClient resolverv1connect.ListServiceClient
engineClient pluginsv1connect.PluginManagementServiceClient
versionClient healthv1connect.VersionServiceClient
}

Expand Down Expand Up @@ -158,8 +163,20 @@ type Client interface {

// Version returns the name and version reported by the daemon.
Version(ctx context.Context) (DaemonVersion, error)
}

type PluginManagement interface {
ListPlugins(ctx context.Context) ([]PluginInfo, error)
EnablePlugin(ctx context.Context, name string) error
DisablePlugin(ctx context.Context, name string) error
}

func PluginManagementFromClient(c Client) (PluginManagement, error) {
m, ok := c.(PluginManagement)
if !ok {
return nil, errors.New("client does not implement PluginManagement")
}
return m, nil
}

func isDialError(err error) bool {
Expand Down Expand Up @@ -209,14 +226,14 @@ func New(options ...Option) (Client, error) {
}
return &client{
resolverClient: resolver.NewResolverClient(c),
listClient: resolverv1connect.NewListServiceClient(c, "http://unix"),
engineClient: pluginsv1connect.NewPluginManagementServiceClient(c, "http://unix"),
versionClient: healthv1connect.NewVersionServiceClient(c, "http://unix"),
}, nil
}

func (c client) ListPlugins(ctx context.Context) ([]PluginInfo, error) {
req := connect.NewRequest(v1.ListPluginsRequest_builder{}.Build())
resp, err := c.listClient.ListPlugins(ctx, req)
req := connect.NewRequest(pluginsv1.ListPluginsRequest_builder{}.Build())
resp, err := c.engineClient.ListPlugins(ctx, req)
if isDialError(err) {
return nil, fmt.Errorf("%w: %w", ErrSecretsEngineNotAvailable, err)
}
Expand All @@ -233,27 +250,56 @@ func (c client) ListPlugins(ctx context.Context) ([]PluginInfo, error) {
if err != nil {
continue
}
pattern, err := secrets.ParsePattern(item.GetPattern())
if err != nil {
continue
}
result = append(result, PluginInfo{
info := PluginInfo{
Name: name,
Version: version,
Pattern: pattern,
Disabled: item.GetDisabled(),
External: item.GetExternal(),
Configurable: item.GetConfigurable(),
})
}
if sp := item.GetSecretsProvider(); sp != nil {
pattern, err := secrets.ParsePattern(sp.GetPattern())
if err != nil {
continue
}
info.SecretsProvider = &SecretsProviderMetadata{Pattern: pattern}
}
result = append(result, info)
}
return result, nil
}

func (c client) EnablePlugin(ctx context.Context, name string) error {
r := pluginsv1.EnablePluginRequest_builder{}.Build()
r.SetName(name)
_, err := c.engineClient.EnablePlugin(ctx, connect.NewRequest(r))
if isDialError(err) {
return fmt.Errorf("%w: %w", ErrSecretsEngineNotAvailable, err)
}
return err
}

func (c client) DisablePlugin(ctx context.Context, name string) error {
r := pluginsv1.DisablePluginRequest_builder{}.Build()
r.SetName(name)
_, err := c.engineClient.DisablePlugin(ctx, connect.NewRequest(r))
if isDialError(err) {
return fmt.Errorf("%w: %w", ErrSecretsEngineNotAvailable, err)
}
return err
}

type PluginInfo struct {
Name api.Name
Version api.Version
Pattern secrets.Pattern
External bool
Configurable bool
Name api.Name
Version api.Version
Disabled bool
External bool
Configurable bool
SecretsProvider *SecretsProviderMetadata
}

type SecretsProviderMetadata struct {
Pattern secrets.Pattern
}

func dialFromPath(path string) dial {
Expand Down
84 changes: 52 additions & 32 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import (
"github.com/docker/secrets-engine/x/api"
healthv1 "github.com/docker/secrets-engine/x/api/health/v1"
"github.com/docker/secrets-engine/x/api/health/v1/healthv1connect"
resolverv1 "github.com/docker/secrets-engine/x/api/resolver/v1"
"github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect"
pluginsv1 "github.com/docker/secrets-engine/x/api/plugins/v1"
"github.com/docker/secrets-engine/x/api/plugins/v1/pluginsv1connect"
"github.com/docker/secrets-engine/x/secrets"
"github.com/docker/secrets-engine/x/testhelper"
)
Expand Down Expand Up @@ -60,14 +60,14 @@ func mockVersionEngine(t *testing.T, version, date, commitHash string) string {
return socketPath
}

var _ resolverv1connect.ListServiceHandler = &mockPluginsList{}
var _ pluginsv1connect.PluginManagementServiceHandler = &mockPluginsList{}

type mockPluginsList struct {
list []PluginInfo
}

func (m mockPluginsList) ListPlugins(context.Context, *connect.Request[resolverv1.ListPluginsRequest]) (*connect.Response[resolverv1.ListPluginsResponse], error) {
var plugins []*resolverv1.Plugin
func (m mockPluginsList) ListPlugins(_ context.Context, _ *connect.Request[pluginsv1.ListPluginsRequest]) (*connect.Response[pluginsv1.ListPluginsResponse], error) {
var plugins []*pluginsv1.Plugin
for _, plugin := range m.list {
var name string
if plugin.Name != nil {
Expand All @@ -77,23 +77,33 @@ func (m mockPluginsList) ListPlugins(context.Context, *connect.Request[resolverv
if plugin.Version != nil {
version = plugin.Version.String()
}
var pattern string
if plugin.Pattern != nil {
pattern = plugin.Pattern.String()
}
plugins = append(plugins, resolverv1.Plugin_builder{
b := pluginsv1.Plugin_builder{
Name: proto.String(name),
Version: proto.String(version),
Pattern: proto.String(pattern),
Disabled: proto.Bool(plugin.Disabled),
External: proto.Bool(plugin.External),
Configurable: proto.Bool(plugin.Configurable),
}.Build())
}
if plugin.SecretsProvider != nil {
b.SecretsProvider = pluginsv1.SecretsProvider_builder{
Pattern: proto.String(plugin.SecretsProvider.Pattern.String()),
}.Build()
}
plugins = append(plugins, b.Build())
}
return connect.NewResponse(resolverv1.ListPluginsResponse_builder{
return connect.NewResponse(pluginsv1.ListPluginsResponse_builder{
Plugins: plugins,
}.Build()), nil
}

func (m mockPluginsList) EnablePlugin(_ context.Context, _ *connect.Request[pluginsv1.EnablePluginRequest]) (*connect.Response[pluginsv1.EnablePluginResponse], error) {
return connect.NewResponse(pluginsv1.EnablePluginResponse_builder{}.Build()), nil
}

func (m mockPluginsList) DisablePlugin(_ context.Context, _ *connect.Request[pluginsv1.DisablePluginRequest]) (*connect.Response[pluginsv1.DisablePluginResponse], error) {
return connect.NewResponse(pluginsv1.DisablePluginResponse_builder{}.Build()), nil
}

type handler struct {
pattern string
handler http.Handler
Expand Down Expand Up @@ -133,7 +143,7 @@ func wrapHandler(pattern string, h http.Handler) handler {
func mockListPluginsEngine(t *testing.T, plugins []PluginInfo) string {
t.Helper()
socketPath := testhelper.RandomShortSocketName()
muxServer(t, socketPath, []handler{wrapHandler(resolverv1connect.NewListServiceHandler(&mockPluginsList{list: plugins}))})
muxServer(t, socketPath, []handler{wrapHandler(pluginsv1connect.NewPluginManagementServiceHandler(&mockPluginsList{list: plugins}))})
return socketPath
}

Expand All @@ -142,22 +152,25 @@ func Test_ListPlugins(t *testing.T) {
t.Run("external and internal configurable plugins", func(t *testing.T) {
plugins := []PluginInfo{
{
Name: api.MustNewName("foo"),
Version: api.MustNewVersion("v1"),
Pattern: secrets.MustParsePattern("**"),
Configurable: true,
Name: api.MustNewName("foo"),
Version: api.MustNewVersion("v1"),
SecretsProvider: &SecretsProviderMetadata{Pattern: secrets.MustParsePattern("**")},
Configurable: true,
Disabled: true,
},
{
Name: api.MustNewName("bar"),
Version: api.MustNewVersion("v1"),
Pattern: secrets.MustParsePattern("**"),
External: true,
Name: api.MustNewName("bar"),
Version: api.MustNewVersion("v1"),
SecretsProvider: &SecretsProviderMetadata{Pattern: secrets.MustParsePattern("**")},
External: true,
},
}
socket := mockListPluginsEngine(t, plugins)
client, err := New(WithSocketPath(socket))
require.NoError(t, err)
result, err := client.ListPlugins(t.Context())
m, err := PluginManagementFromClient(client)
require.NoError(t, err)
result, err := m.ListPlugins(t.Context())
require.NoError(t, err)
assert.Equal(t, plugins, result)
})
Expand All @@ -166,7 +179,9 @@ func Test_ListPlugins(t *testing.T) {
socket := mockListPluginsEngine(t, plugins)
client, err := New(WithSocketPath(socket))
require.NoError(t, err)
result, err := client.ListPlugins(t.Context())
m, err := PluginManagementFromClient(client)
require.NoError(t, err)
result, err := m.ListPlugins(t.Context())
require.NoError(t, err)
assert.Empty(t, result)
})
Expand All @@ -176,21 +191,24 @@ func Test_ListPlugins(t *testing.T) {
Name: api.MustNewName("foo"),
},
{
Name: api.MustNewName("bar"),
Version: api.MustNewVersion("v1"),
Pattern: secrets.MustParsePattern("**"),
Name: api.MustNewName("bar"),
Version: api.MustNewVersion("v1"),
SecretsProvider: &SecretsProviderMetadata{Pattern: secrets.MustParsePattern("**")},
},
}
socket := mockListPluginsEngine(t, plugins)
client, err := New(WithSocketPath(socket))
require.NoError(t, err)
result, err := client.ListPlugins(t.Context())
m, err := PluginManagementFromClient(client)
require.NoError(t, err)
result, err := m.ListPlugins(t.Context())
require.NoError(t, err)

assert.Equal(t, []PluginInfo{
{
Name: api.MustNewName("bar"),
Version: api.MustNewVersion("v1"),
Pattern: secrets.MustParsePattern("**"),
Name: api.MustNewName("bar"),
Version: api.MustNewVersion("v1"),
SecretsProvider: &SecretsProviderMetadata{Pattern: secrets.MustParsePattern("**")},
},
}, result)
})
Expand Down Expand Up @@ -221,7 +239,9 @@ func TestSecretsEngineUnavailable(t *testing.T) {
socketPath := testhelper.RandomShortSocketName()
client, err := New(WithSocketPath(socketPath))
require.NoError(t, err)
_, err = client.ListPlugins(t.Context())
m, err := PluginManagementFromClient(client)
require.NoError(t, err)
_, err = m.ListPlugins(t.Context())
require.ErrorIs(t, err, ErrSecretsEngineNotAvailable)
_, err = client.GetSecrets(t.Context(), secrets.MustParsePattern("**"))
require.ErrorIs(t, err, ErrSecretsEngineNotAvailable)
Expand Down
Loading
Loading