Skip to content

Commit 66c1a73

Browse files
committed
feat(api): add daemon version endpoint client and version comparison
Add GetDaemonVersion to query the /version endpoint exposed by the engine, returning a DaemonVersion (name + version) or ErrVersionNotSupported when the daemon predates the version API. Add Compare(a, b Version) for semver ordering, enabling callers to gate feature use on the daemon version. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
1 parent 88eb7a4 commit 66c1a73

3 files changed

Lines changed: 96 additions & 3 deletions

File tree

client/client.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ import (
3232
)
3333

3434
type (
35-
Envelope = secrets.Envelope
36-
ID = secrets.ID
37-
Pattern = secrets.Pattern
35+
Envelope = secrets.Envelope
36+
ID = secrets.ID
37+
Pattern = secrets.Pattern
38+
DaemonVersion = api.DaemonVersion
3839
)
3940

4041
var (
@@ -118,6 +119,7 @@ type config struct {
118119
}
119120

120121
type client struct {
122+
httpClient *http.Client
121123
resolverClient secrets.Resolver
122124
listClient resolverv1connect.ListServiceClient
123125
}
@@ -133,9 +135,21 @@ func (c client) GetSecrets(ctx context.Context, pattern secrets.Pattern) ([]secr
133135
return envelopes, nil
134136
}
135137

138+
func (c client) Version(ctx context.Context) (DaemonVersion, error) {
139+
dv, err := api.GetDaemonVersion(ctx, c.httpClient)
140+
if isDialError(err) {
141+
return DaemonVersion{}, fmt.Errorf("%w: %w", ErrSecretsEngineNotAvailable, err)
142+
}
143+
return dv, err
144+
}
145+
146+
// Client is the interface for interacting with the secrets engine daemon.
136147
type Client interface {
137148
secrets.Resolver
138149

150+
// Version returns the name and version reported by the daemon.
151+
Version(ctx context.Context) (DaemonVersion, error)
152+
139153
ListPlugins(ctx context.Context) ([]PluginInfo, error)
140154
}
141155

@@ -185,6 +199,7 @@ func New(options ...Option) (Client, error) {
185199
Timeout: cfg.requestTimeout,
186200
}
187201
return &client{
202+
httpClient: c,
188203
resolverClient: resolver.NewResolverClient(c),
189204
listClient: resolverv1connect.NewListServiceClient(c, "http://unix"),
190205
}, nil

x/api/daemon.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 api
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"errors"
21+
"fmt"
22+
"net/http"
23+
)
24+
25+
// ErrVersionNotSupported is returned when the daemon does not expose a version
26+
// endpoint (i.e. it predates the version API).
27+
var ErrVersionNotSupported = errors.New("daemon does not support version endpoint")
28+
29+
// DaemonVersion holds the name and version reported by the daemon.
30+
type DaemonVersion struct {
31+
Name string
32+
Version Version
33+
}
34+
35+
// GetDaemonVersion fetches the daemon version from the /version endpoint using
36+
// the provided HTTP client. The client must be configured to dial the engine
37+
// unix socket.
38+
func GetDaemonVersion(ctx context.Context, httpClient *http.Client) (DaemonVersion, error) {
39+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://unix/version", nil)
40+
if err != nil {
41+
return DaemonVersion{}, fmt.Errorf("building version request: %w", err)
42+
}
43+
44+
resp, err := httpClient.Do(req)
45+
if err != nil {
46+
return DaemonVersion{}, err
47+
}
48+
defer resp.Body.Close()
49+
50+
if resp.StatusCode == http.StatusNotFound {
51+
return DaemonVersion{}, ErrVersionNotSupported
52+
}
53+
if resp.StatusCode != http.StatusOK {
54+
return DaemonVersion{}, fmt.Errorf("unexpected status from /version: %d", resp.StatusCode)
55+
}
56+
57+
var body struct {
58+
Name string `json:"name"`
59+
Version string `json:"version"`
60+
}
61+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
62+
return DaemonVersion{}, fmt.Errorf("decoding version response: %w", err)
63+
}
64+
65+
v, err := NewVersion(body.Version)
66+
if err != nil {
67+
return DaemonVersion{}, fmt.Errorf("parsing daemon version %q: %w", body.Version, err)
68+
}
69+
70+
return DaemonVersion{Name: body.Name, Version: v}, nil
71+
}

x/api/version.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ func MustNewVersion(s string) Version {
5959
return v
6060
}
6161

62+
// Compare returns an integer comparing two versions according to semantic versioning.
63+
// The result will be 0 if a == b, -1 if a < b, or +1 if a > b.
64+
// An invalid semver string is considered less than a valid one.
65+
func Compare(a, b Version) int {
66+
return semver.Compare(a.String(), b.String())
67+
}
68+
6269
func valid(s string) error {
6370
if len(s) > 0 && s[0] != 'v' {
6471
return ErrVPrefix

0 commit comments

Comments
 (0)