Skip to content

Commit 7da9be4

Browse files
committed
feat: engine management API
Signed-off-by: Johannes Großmann <grossmann.johannes@t-online.de>
1 parent a56837f commit 7da9be4

9 files changed

Lines changed: 1283 additions & 551 deletions

File tree

client/client.go

Lines changed: 13 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ import (
2828
healthv1 "github.com/docker/secrets-engine/x/api/health/v1"
2929
"github.com/docker/secrets-engine/x/api/health/v1/healthv1connect"
3030
"github.com/docker/secrets-engine/x/api/resolver"
31-
v1 "github.com/docker/secrets-engine/x/api/resolver/v1"
32-
"github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect"
3331
"github.com/docker/secrets-engine/x/secrets"
3432
)
3533

@@ -122,7 +120,6 @@ type config struct {
122120

123121
type client struct {
124122
resolverClient secrets.Resolver
125-
listClient resolverv1connect.ListServiceClient
126123
versionClient healthv1connect.VersionServiceClient
127124
}
128125

@@ -158,8 +155,6 @@ type Client interface {
158155

159156
// Version returns the name and version reported by the daemon.
160157
Version(ctx context.Context) (DaemonVersion, error)
161-
162-
ListPlugins(ctx context.Context) ([]PluginInfo, error)
163158
}
164159

165160
func isDialError(err error) bool {
@@ -173,7 +168,7 @@ func isDialError(err error) bool {
173168
return false
174169
}
175170

176-
func New(options ...Option) (Client, error) {
171+
func applyOptions(options []Option) (*config, error) {
177172
cfg := &config{
178173
requestTimeout: api.DefaultClientRequestTimeout,
179174
responseTimeout: api.DefaultClientResponseHeaderTimeout,
@@ -186,7 +181,11 @@ func New(options ...Option) (Client, error) {
186181
if cfg.dialContext == nil {
187182
cfg.dialContext = dialFromPath(api.DaemonSocketPath())
188183
}
189-
c := &http.Client{
184+
return cfg, nil
185+
}
186+
187+
func newHTTPClient(cfg *config) *http.Client {
188+
return &http.Client{
190189
Transport: &http.Transport{
191190
// re-use the same connection to the runtime, this speeds up subsequent
192191
// calls.
@@ -207,53 +206,18 @@ func New(options ...Option) (Client, error) {
207206
// it can be overwritten with [WithTimeout]
208207
Timeout: cfg.requestTimeout,
209208
}
210-
return &client{
211-
resolverClient: resolver.NewResolverClient(c),
212-
listClient: resolverv1connect.NewListServiceClient(c, "http://unix"),
213-
versionClient: healthv1connect.NewVersionServiceClient(c, "http://unix"),
214-
}, nil
215209
}
216210

217-
func (c client) ListPlugins(ctx context.Context) ([]PluginInfo, error) {
218-
req := connect.NewRequest(v1.ListPluginsRequest_builder{}.Build())
219-
resp, err := c.listClient.ListPlugins(ctx, req)
220-
if isDialError(err) {
221-
return nil, fmt.Errorf("%w: %w", ErrSecretsEngineNotAvailable, err)
222-
}
211+
func New(options ...Option) (Client, error) {
212+
cfg, err := applyOptions(options)
223213
if err != nil {
224214
return nil, err
225215
}
226-
var result []PluginInfo
227-
for _, item := range resp.Msg.GetPlugins() {
228-
name, err := api.NewName(item.GetName())
229-
if err != nil {
230-
continue
231-
}
232-
version, err := api.NewVersion(item.GetVersion())
233-
if err != nil {
234-
continue
235-
}
236-
pattern, err := secrets.ParsePattern(item.GetPattern())
237-
if err != nil {
238-
continue
239-
}
240-
result = append(result, PluginInfo{
241-
Name: name,
242-
Version: version,
243-
Pattern: pattern,
244-
External: item.GetExternal(),
245-
Configurable: item.GetConfigurable(),
246-
})
247-
}
248-
return result, nil
249-
}
250-
251-
type PluginInfo struct {
252-
Name api.Name
253-
Version api.Version
254-
Pattern secrets.Pattern
255-
External bool
256-
Configurable bool
216+
c := newHTTPClient(cfg)
217+
return &client{
218+
resolverClient: resolver.NewResolverClient(c),
219+
versionClient: healthv1connect.NewVersionServiceClient(c, "http://unix"),
220+
}, nil
257221
}
258222

259223
func dialFromPath(path string) dial {

client/client_test.go

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ import (
2929
"google.golang.org/protobuf/proto"
3030

3131
"github.com/docker/secrets-engine/x/api"
32+
enginev1 "github.com/docker/secrets-engine/x/api/engine/v1"
33+
"github.com/docker/secrets-engine/x/api/engine/v1/enginev1connect"
3234
healthv1 "github.com/docker/secrets-engine/x/api/health/v1"
3335
"github.com/docker/secrets-engine/x/api/health/v1/healthv1connect"
34-
resolverv1 "github.com/docker/secrets-engine/x/api/resolver/v1"
35-
"github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect"
3636
"github.com/docker/secrets-engine/x/secrets"
3737
"github.com/docker/secrets-engine/x/testhelper"
3838
)
@@ -60,14 +60,14 @@ func mockVersionEngine(t *testing.T, version, date, commitHash string) string {
6060
return socketPath
6161
}
6262

63-
var _ resolverv1connect.ListServiceHandler = &mockPluginsList{}
63+
var _ enginev1connect.PluginManagementServiceHandler = &mockPluginsList{}
6464

6565
type mockPluginsList struct {
6666
list []PluginInfo
6767
}
6868

69-
func (m mockPluginsList) ListPlugins(context.Context, *connect.Request[resolverv1.ListPluginsRequest]) (*connect.Response[resolverv1.ListPluginsResponse], error) {
70-
var plugins []*resolverv1.Plugin
69+
func (m mockPluginsList) ListPlugins(_ context.Context, _ *connect.Request[enginev1.ListPluginsRequest]) (*connect.Response[enginev1.ListPluginsResponse], error) {
70+
var plugins []*enginev1.Plugin
7171
for _, plugin := range m.list {
7272
var name string
7373
if plugin.Name != nil {
@@ -77,23 +77,32 @@ func (m mockPluginsList) ListPlugins(context.Context, *connect.Request[resolverv
7777
if plugin.Version != nil {
7878
version = plugin.Version.String()
7979
}
80-
var pattern string
81-
if plugin.Pattern != nil {
82-
pattern = plugin.Pattern.String()
83-
}
84-
plugins = append(plugins, resolverv1.Plugin_builder{
80+
b := enginev1.Plugin_builder{
8581
Name: proto.String(name),
8682
Version: proto.String(version),
87-
Pattern: proto.String(pattern),
8883
External: proto.Bool(plugin.External),
8984
Configurable: proto.Bool(plugin.Configurable),
90-
}.Build())
85+
}
86+
if plugin.SecretsProvider != nil {
87+
b.SecretsProvider = enginev1.SecretsProvider_builder{
88+
Pattern: proto.String(plugin.SecretsProvider.Pattern.String()),
89+
}.Build()
90+
}
91+
plugins = append(plugins, b.Build())
9192
}
92-
return connect.NewResponse(resolverv1.ListPluginsResponse_builder{
93+
return connect.NewResponse(enginev1.ListPluginsResponse_builder{
9394
Plugins: plugins,
9495
}.Build()), nil
9596
}
9697

98+
func (m mockPluginsList) EnablePlugin(_ context.Context, _ *connect.Request[enginev1.EnablePluginRequest]) (*connect.Response[enginev1.EnablePluginResponse], error) {
99+
return connect.NewResponse(enginev1.EnablePluginResponse_builder{}.Build()), nil
100+
}
101+
102+
func (m mockPluginsList) DisablePlugin(_ context.Context, _ *connect.Request[enginev1.DisablePluginRequest]) (*connect.Response[enginev1.DisablePluginResponse], error) {
103+
return connect.NewResponse(enginev1.DisablePluginResponse_builder{}.Build()), nil
104+
}
105+
97106
type handler struct {
98107
pattern string
99108
handler http.Handler
@@ -133,7 +142,7 @@ func wrapHandler(pattern string, h http.Handler) handler {
133142
func mockListPluginsEngine(t *testing.T, plugins []PluginInfo) string {
134143
t.Helper()
135144
socketPath := testhelper.RandomShortSocketName()
136-
muxServer(t, socketPath, []handler{wrapHandler(resolverv1connect.NewListServiceHandler(&mockPluginsList{list: plugins}))})
145+
muxServer(t, socketPath, []handler{wrapHandler(enginev1connect.NewPluginManagementServiceHandler(&mockPluginsList{list: plugins}))})
137146
return socketPath
138147
}
139148

@@ -142,20 +151,20 @@ func Test_ListPlugins(t *testing.T) {
142151
t.Run("external and internal configurable plugins", func(t *testing.T) {
143152
plugins := []PluginInfo{
144153
{
145-
Name: api.MustNewName("foo"),
146-
Version: api.MustNewVersion("v1"),
147-
Pattern: secrets.MustParsePattern("**"),
148-
Configurable: true,
154+
Name: api.MustNewName("foo"),
155+
Version: api.MustNewVersion("v1"),
156+
SecretsProvider: &SecretsProviderMetadata{Pattern: secrets.MustParsePattern("**")},
157+
Configurable: true,
149158
},
150159
{
151-
Name: api.MustNewName("bar"),
152-
Version: api.MustNewVersion("v1"),
153-
Pattern: secrets.MustParsePattern("**"),
154-
External: true,
160+
Name: api.MustNewName("bar"),
161+
Version: api.MustNewVersion("v1"),
162+
SecretsProvider: &SecretsProviderMetadata{Pattern: secrets.MustParsePattern("**")},
163+
External: true,
155164
},
156165
}
157166
socket := mockListPluginsEngine(t, plugins)
158-
client, err := New(WithSocketPath(socket))
167+
client, err := NewPluginManagement(WithSocketPath(socket))
159168
require.NoError(t, err)
160169
result, err := client.ListPlugins(t.Context())
161170
require.NoError(t, err)
@@ -164,7 +173,7 @@ func Test_ListPlugins(t *testing.T) {
164173
t.Run("no plugins", func(t *testing.T) {
165174
var plugins []PluginInfo
166175
socket := mockListPluginsEngine(t, plugins)
167-
client, err := New(WithSocketPath(socket))
176+
client, err := NewPluginManagement(WithSocketPath(socket))
168177
require.NoError(t, err)
169178
result, err := client.ListPlugins(t.Context())
170179
require.NoError(t, err)
@@ -176,21 +185,21 @@ func Test_ListPlugins(t *testing.T) {
176185
Name: api.MustNewName("foo"),
177186
},
178187
{
179-
Name: api.MustNewName("bar"),
180-
Version: api.MustNewVersion("v1"),
181-
Pattern: secrets.MustParsePattern("**"),
188+
Name: api.MustNewName("bar"),
189+
Version: api.MustNewVersion("v1"),
190+
SecretsProvider: &SecretsProviderMetadata{Pattern: secrets.MustParsePattern("**")},
182191
},
183192
}
184193
socket := mockListPluginsEngine(t, plugins)
185-
client, err := New(WithSocketPath(socket))
194+
client, err := NewPluginManagement(WithSocketPath(socket))
186195
require.NoError(t, err)
187196
result, err := client.ListPlugins(t.Context())
188197
require.NoError(t, err)
189198
assert.Equal(t, []PluginInfo{
190199
{
191-
Name: api.MustNewName("bar"),
192-
Version: api.MustNewVersion("v1"),
193-
Pattern: secrets.MustParsePattern("**"),
200+
Name: api.MustNewName("bar"),
201+
Version: api.MustNewVersion("v1"),
202+
SecretsProvider: &SecretsProviderMetadata{Pattern: secrets.MustParsePattern("**")},
194203
},
195204
}, result)
196205
})
@@ -219,11 +228,14 @@ func Test_Version(t *testing.T) {
219228

220229
func TestSecretsEngineUnavailable(t *testing.T) {
221230
socketPath := testhelper.RandomShortSocketName()
222-
client, err := New(WithSocketPath(socketPath))
231+
c, err := New(WithSocketPath(socketPath))
223232
require.NoError(t, err)
224-
_, err = client.ListPlugins(t.Context())
233+
_, err = c.GetSecrets(t.Context(), secrets.MustParsePattern("**"))
225234
require.ErrorIs(t, err, ErrSecretsEngineNotAvailable)
226-
_, err = client.GetSecrets(t.Context(), secrets.MustParsePattern("**"))
235+
236+
pm, err := NewPluginManagement(WithSocketPath(socketPath))
237+
require.NoError(t, err)
238+
_, err = pm.ListPlugins(t.Context())
227239
require.ErrorIs(t, err, ErrSecretsEngineNotAvailable)
228240
}
229241

client/plugin_management.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2025-2026 Docker, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package client
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
"connectrpc.com/connect"
22+
23+
"github.com/docker/secrets-engine/x/api"
24+
enginev1 "github.com/docker/secrets-engine/x/api/engine/v1"
25+
"github.com/docker/secrets-engine/x/api/engine/v1/enginev1connect"
26+
"github.com/docker/secrets-engine/x/secrets"
27+
)
28+
29+
// PluginManagement is the interface for managing plugins in the secrets engine daemon.
30+
type PluginManagement interface {
31+
ListPlugins(ctx context.Context) ([]PluginInfo, error)
32+
EnablePlugin(ctx context.Context, name string) error
33+
DisablePlugin(ctx context.Context, name string) error
34+
}
35+
36+
type pluginManagementClient struct {
37+
engineClient enginev1connect.PluginManagementServiceClient
38+
}
39+
40+
func NewPluginManagement(options ...Option) (PluginManagement, error) {
41+
cfg, err := applyOptions(options)
42+
if err != nil {
43+
return nil, err
44+
}
45+
c := newHTTPClient(cfg)
46+
return &pluginManagementClient{
47+
engineClient: enginev1connect.NewPluginManagementServiceClient(c, "http://unix"),
48+
}, nil
49+
}
50+
51+
func (c pluginManagementClient) ListPlugins(ctx context.Context) ([]PluginInfo, error) {
52+
req := connect.NewRequest(enginev1.ListPluginsRequest_builder{}.Build())
53+
resp, err := c.engineClient.ListPlugins(ctx, req)
54+
if isDialError(err) {
55+
return nil, fmt.Errorf("%w: %w", ErrSecretsEngineNotAvailable, err)
56+
}
57+
if err != nil {
58+
return nil, err
59+
}
60+
var result []PluginInfo
61+
for _, item := range resp.Msg.GetPlugins() {
62+
name, err := api.NewName(item.GetName())
63+
if err != nil {
64+
continue
65+
}
66+
version, err := api.NewVersion(item.GetVersion())
67+
if err != nil {
68+
continue
69+
}
70+
info := PluginInfo{
71+
Name: name,
72+
Version: version,
73+
External: item.GetExternal(),
74+
Configurable: item.GetConfigurable(),
75+
}
76+
if sp := item.GetSecretsProvider(); sp != nil {
77+
pattern, err := secrets.ParsePattern(sp.GetPattern())
78+
if err != nil {
79+
continue
80+
}
81+
info.SecretsProvider = &SecretsProviderMetadata{Pattern: pattern}
82+
}
83+
result = append(result, info)
84+
}
85+
return result, nil
86+
}
87+
88+
func (c pluginManagementClient) EnablePlugin(ctx context.Context, name string) error {
89+
r := enginev1.EnablePluginRequest_builder{}.Build()
90+
r.SetName(name)
91+
_, err := c.engineClient.EnablePlugin(ctx, connect.NewRequest(r))
92+
if isDialError(err) {
93+
return fmt.Errorf("%w: %w", ErrSecretsEngineNotAvailable, err)
94+
}
95+
return err
96+
}
97+
98+
func (c pluginManagementClient) DisablePlugin(ctx context.Context, name string) error {
99+
r := enginev1.DisablePluginRequest_builder{}.Build()
100+
r.SetName(name)
101+
_, err := c.engineClient.DisablePlugin(ctx, connect.NewRequest(r))
102+
if isDialError(err) {
103+
return fmt.Errorf("%w: %w", ErrSecretsEngineNotAvailable, err)
104+
}
105+
return err
106+
}
107+
108+
type PluginInfo struct {
109+
Name api.Name
110+
Version api.Version
111+
External bool
112+
Configurable bool
113+
SecretsProvider *SecretsProviderMetadata
114+
}
115+
116+
type SecretsProviderMetadata struct {
117+
Pattern secrets.Pattern
118+
}

0 commit comments

Comments
 (0)