feat(plugin/agent): add configurable secretstore backend HTTP timeouts (#22118)
Ilya Mashchenko committed
Apr 3, 2026 at 07:05 UTC
f09bef03f98010e26cbc7e1abccb70b3656402d2
28 files changed
+527
-149
src/go/plugin/agent/secrets/secretstore/backends/aws/config_schema.json
+7
@@ -19,6 +19,13 @@
19
"title": "Region",
20
"description": "AWS region used for Secrets Manager requests.",
21
"type": "string"
22
+ },
23
+ "timeout": {
24
+ "title": "Timeout",
25
+ "description": "Timeout in seconds for HTTP requests made by this secretstore backend.",
26
+ "type": "number",
27
+ "minimum": 0,
28
+ "default": 3
29
}
30
},
31
"required": [
src/go/plugin/agent/secrets/secretstore/backends/aws/init.go
+13
-1
@@ -8,6 +8,7 @@ import (
8
"strings"
9
10
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
11
+ "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
12
)
13
14
func (s *store) init(_ context.Context) error {
@@ -24,8 +25,19 @@ func (s *store) init(_ context.Context) error {
25
}
26
s.Config.Region = region
27
28
+ switch {
29
+ case s.Config.Timeout.Duration() < 0:
30
+ return fmt.Errorf("timeout cannot be negative")
31
+ case s.Config.Timeout.Duration() == 0:
32
+ s.Config.Timeout = defaultTimeout
33
+ }
34
+ s.runtime = &runtime{
35
+ apiClient: httpx.APIClient(s.Config.Timeout.Duration()),
36
+ imdsClient: httpx.NoProxyClient(s.Config.Timeout.Duration()),
37
+ }
38
+
39
published := &publishedStore{
28
- provider: s.provider,
40
+ runtime: s.runtime,
41
mode: s.Config.AuthMode,
42
regionValue: region,
43
}
src/go/plugin/agent/secrets/secretstore/backends/aws/init_test.go
new
+83
@@ -0,0 +1,83 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package aws
4
+
5
+import (
6
+ "context"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
11
+ "github.com/stretchr/testify/assert"
12
+ "github.com/stretchr/testify/require"
13
+)
14
+
15
+func TestStoreInitTimeout(t *testing.T) {
16
+ tests := map[string]struct {
17
+ timeout confopt.Duration
18
+ wantTimeout time.Duration
19
+ wantErrContains string
20
+ }{
21
+ "default timeout": {
22
+ wantTimeout: defaultTimeout.Duration(),
23
+ },
24
+ "configured timeout": {
25
+ timeout: confopt.Duration(7 * time.Second),
26
+ wantTimeout: 7 * time.Second,
27
+ },
28
+ "negative timeout": {
29
+ timeout: confopt.Duration(-time.Second),
30
+ wantErrContains: "timeout cannot be negative",
31
+ },
32
+ }
33
+
34
+ for name, tc := range tests {
35
+ t.Run(name, func(t *testing.T) {
36
+ s := &store{
37
+ Config: Config{
38
+ AuthMode: "env",
39
+ Region: "us-east-1",
40
+ Timeout: tc.timeout,
41
+ },
42
+ }
43
+
44
+ err := s.init(context.Background())
45
+ if tc.wantErrContains != "" {
46
+ require.Error(t, err)
47
+ assert.ErrorContains(t, err, tc.wantErrContains)
48
+ return
49
+ }
50
+
51
+ require.NoError(t, err)
52
+ assert.Equal(t, tc.wantTimeout, s.runtime.apiClient.Timeout)
53
+ assert.Equal(t, tc.wantTimeout, s.runtime.imdsClient.Timeout)
54
+ assert.Equal(t, confopt.Duration(tc.wantTimeout), s.Config.Timeout)
55
+ })
56
+ }
57
+}
58
+
59
+func TestInitBuildsStoreScopedRuntime(t *testing.T) {
60
+ creator := New()
61
+
62
+ first, ok := creator.Create().(*store)
63
+ require.True(t, ok)
64
+ second, ok := creator.Create().(*store)
65
+ require.True(t, ok)
66
+
67
+ assert.Equal(t, defaultTimeout, first.Config.Timeout)
68
+ first.AuthMode = "env"
69
+ first.Region = "us-east-1"
70
+ second.AuthMode = "env"
71
+ second.Region = "us-east-1"
72
+
73
+ require.NoError(t, first.init(context.Background()))
74
+ require.NoError(t, second.init(context.Background()))
75
+
76
+ require.NotNil(t, first.runtime)
77
+ require.NotNil(t, second.runtime)
78
+ assert.NotSame(t, first.runtime, second.runtime)
79
+ assert.NotSame(t, first.runtime.apiClient, second.runtime.apiClient)
80
+ assert.NotSame(t, first.runtime.imdsClient, second.runtime.imdsClient)
81
+ assert.Equal(t, defaultTimeout.Duration(), first.runtime.apiClient.Timeout)
82
+ assert.Equal(t, defaultTimeout.Duration(), first.runtime.imdsClient.Timeout)
83
+}
src/go/plugin/agent/secrets/secretstore/backends/aws/metadata.yaml
+4
@@ -62,6 +62,10 @@ setup:
62
description: 'AWS region used for Secrets Manager requests. There is no automatic region detection — you must always set this explicitly.'
63
default_value: ''
64
required: true
65
+ - name: 'timeout'
66
+ description: 'Timeout in seconds for HTTP requests made by this secretstore backend.'
67
+ default_value: 3
68
+ required: false
69
examples:
70
folding:
71
title: 'Example configuration'
src/go/plugin/agent/secrets/secretstore/backends/aws/provider.go
+16
-19
@@ -8,16 +8,19 @@ import (
8
"net/http"
9
"time"
10
11
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
12
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
12
- "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
13
)
14
15
//go:embed config_schema.json
16
var configSchema string
17
18
+var defaultTimeout = confopt.Duration(3 * time.Second)
19
+
20
type Config struct {
19
- AuthMode string `json:"auth_mode" yaml:"auth_mode"`
20
- Region string `json:"region" yaml:"region"`
21
+ AuthMode string `json:"auth_mode" yaml:"auth_mode"`
22
+ Region string `json:"region" yaml:"region"`
23
+ Timeout confopt.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
24
}
25
26
type credentials struct {
@@ -26,44 +29,38 @@ type credentials struct {
29
sessionToken string
30
}
31
29
-type provider struct {
32
+type runtime struct {
33
apiClient *http.Client
34
imdsClient *http.Client
32
- endpoint string
33
- now func() time.Time
35
}
36
37
type store struct {
38
Config `yaml:",inline" json:""`
38
- provider *provider
39
+ runtime *runtime
40
published *publishedStore
41
}
42
43
type publishedStore struct {
43
- provider *provider
44
+ runtime *runtime
45
mode string
46
regionValue string
47
}
48
49
func New() secretstore.Creator {
49
- p := &provider{
50
- apiClient: httpx.APIClient(10 * time.Second),
51
- imdsClient: httpx.NoProxyClient(2 * time.Second),
52
- now: time.Now,
53
- }
54
-
50
return secretstore.Creator{
51
Kind: secretstore.KindAWSSM,
52
DisplayName: "AWS Secrets Manager",
53
Schema: configSchema,
59
- Create: p.create,
54
+ Create: func() secretstore.Store {
55
+ return &store{
56
+ Config: Config{
57
+ Timeout: defaultTimeout,
58
+ },
59
+ }
60
+ },
61
}
62
}
63
63
-func (p *provider) create() secretstore.Store {
64
- return &store{provider: p}
65
-}
66
-
64
func (s *store) Configuration() any { return &s.Config }
65
66
func (s *store) Init(ctx context.Context) error { return s.init(ctx) }
src/go/plugin/agent/secrets/secretstore/backends/aws/resolve.go
+13
-10
@@ -11,8 +11,10 @@ import (
11
"fmt"
12
"io"
13
"net/http"
14
+ "net/url"
15
"sort"
16
"strings"
17
+ "time"
18
19
"github.com/netdata/netdata/go/plugins/logger"
20
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
@@ -124,7 +126,7 @@ func (s *publishedStore) ecsCredentials(ctx context.Context, relativeURI string)
126
if err != nil {
127
return nil, fmt.Errorf("creating ECS credentials request: %w", err)
128
}
127
- resp, err := s.provider.imdsClient.Do(req)
129
+ resp, err := s.runtime.imdsClient.Do(req)
130
if err != nil {
131
return nil, fmt.Errorf("ECS credentials request failed: %w", err)
132
}
@@ -160,7 +162,7 @@ func (s *publishedStore) imdsCredentials(ctx context.Context) (*credentials, err
162
return nil, fmt.Errorf("creating IMDS token request: %w", err)
163
}
164
tokenReq.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600")
163
- tokenResp, err := s.provider.imdsClient.Do(tokenReq)
165
+ tokenResp, err := s.runtime.imdsClient.Do(tokenReq)
166
if err != nil {
167
return nil, fmt.Errorf("IMDS token request failed: %w", err)
168
}
@@ -179,7 +181,7 @@ func (s *publishedStore) imdsCredentials(ctx context.Context) (*credentials, err
181
return nil, fmt.Errorf("creating IMDS role request: %w", err)
182
}
183
roleReq.Header.Set("X-aws-ec2-metadata-token", imdsToken)
182
- roleResp, err := s.provider.imdsClient.Do(roleReq)
184
+ roleResp, err := s.runtime.imdsClient.Do(roleReq)
185
if err != nil {
186
return nil, fmt.Errorf("IMDS role request failed: %w", err)
187
}
@@ -201,7 +203,7 @@ func (s *publishedStore) imdsCredentials(ctx context.Context) (*credentials, err
203
return nil, fmt.Errorf("creating IMDS credentials request: %w", err)
204
}
205
credReq.Header.Set("X-aws-ec2-metadata-token", imdsToken)
204
- credResp, err := s.provider.imdsClient.Do(credReq)
206
+ credResp, err := s.runtime.imdsClient.Do(credReq)
207
if err != nil {
208
return nil, fmt.Errorf("IMDS credentials request failed: %w", err)
209
}
@@ -228,17 +230,18 @@ func (s *publishedStore) imdsCredentials(ctx context.Context) (*credentials, err
230
}
231
232
func (s *publishedStore) secretValue(ctx context.Context, creds *credentials, region, secretName, original string) (string, error) {
231
- endpoint := s.provider.endpoint
233
host := secretsManagerHost(region)
233
- if endpoint == "" {
234
- endpoint = "https://" + host + "/"
235
- }
234
+ endpoint := (&url.URL{
235
+ Scheme: "https",
236
+ Host: host,
237
+ Path: "/",
238
+ }).String()
239
secretIDJSON, err := json.Marshal(secretName)
240
if err != nil {
241
return "", fmt.Errorf("resolving secret '%s': encoding secret name: %w", original, err)
242
}
243
payload := `{"SecretId":` + string(secretIDJSON) + `}`
241
- now := s.provider.now().UTC()
244
+ now := time.Now().UTC()
245
timestamp := now.Format("20060102T150405Z")
246
datestamp := now.Format("20060102")
247
headers := map[string]string{
@@ -260,7 +263,7 @@ func (s *publishedStore) secretValue(ctx context.Context, creds *credentials, re
263
}
264
httpReq.Host = host
265
httpReq.Header.Set("Authorization", authHeader)
263
- resp, err := s.provider.apiClient.Do(httpReq)
266
+ resp, err := s.runtime.apiClient.Do(httpReq)
267
if err != nil {
268
return "", fmt.Errorf("resolving secret '%s': request failed: %w", original, err)
269
}
src/go/plugin/agent/secrets/secretstore/backends/aws/resolve_test.go
+12
-21
@@ -7,9 +7,7 @@ import (
7
"context"
8
"io"
9
"net/http"
10
- "net/http/httptest"
10
"testing"
12
- "time"
11
12
"github.com/netdata/netdata/go/plugins/logger"
13
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
@@ -45,7 +43,7 @@ func TestSecretsManagerHost(t *testing.T) {
43
}
44
}
45
48
-func TestSecretValue_CustomEndpointUsesSignedHostHeader(t *testing.T) {
46
+func TestSecretValue_UsesSignedHostHeader(t *testing.T) {
47
tests := map[string]struct {
48
region string
49
wantHost string
@@ -62,20 +60,16 @@ func TestSecretValue_CustomEndpointUsesSignedHostHeader(t *testing.T) {
60
61
for name, tc := range tests {
62
t.Run(name, func(t *testing.T) {
65
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66
- assert.Equal(t, tc.wantHost, r.Host)
67
- w.Header().Set("Content-Type", "application/x-amz-json-1.1")
68
- _, _ = w.Write([]byte(`{"SecretString":"value"}`))
69
- }))
70
- defer srv.Close()
71
-
63
store := &publishedStore{
73
- provider: &provider{
74
- apiClient: srv.Client(),
75
- endpoint: srv.URL + "/",
76
- now: func() time.Time {
77
- return time.Date(2026, time.March, 18, 12, 0, 0, 0, time.UTC)
78
- },
64
+ runtime: &runtime{
65
+ apiClient: &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
66
+ assert.Equal(t, tc.wantHost, r.Host)
67
+ return &http.Response{
68
+ StatusCode: http.StatusOK,
69
+ Body: io.NopCloser(bytes.NewBufferString(`{"SecretString":"value"}`)),
70
+ Header: make(http.Header),
71
+ }, nil
72
+ })},
73
},
74
}
75
@@ -94,18 +88,15 @@ func TestPublishedStoreResolve_LogsDetailedResolution(t *testing.T) {
88
t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET")
89
90
store := &publishedStore{
97
- provider: &provider{
91
+ runtime: &runtime{
92
apiClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
93
+ assert.Equal(t, "secretsmanager.us-east-1.amazonaws.com", req.Host)
94
return &http.Response{
95
StatusCode: http.StatusOK,
96
Body: io.NopCloser(bytes.NewBufferString(`{"SecretString":"{\"password\":\"secret-value\"}"}`)),
97
Header: make(http.Header),
98
}, nil
99
})},
105
- endpoint: "https://example.test/",
106
- now: func() time.Time {
107
- return time.Date(2026, time.March, 18, 12, 0, 0, 0, time.UTC)
108
- },
100
},
101
mode: "env",
102
regionValue: "us-east-1",
src/go/plugin/agent/secrets/secretstore/backends/azure/config_schema.json
+7
@@ -14,6 +14,13 @@
14
"default"
15
],
16
"default": "default"
17
+ },
18
+ "timeout": {
19
+ "title": "Timeout",
20
+ "description": "Timeout in seconds for HTTP requests made by this secretstore backend.",
21
+ "type": "number",
22
+ "minimum": 0,
23
+ "default": 3
24
}
25
},
26
"required": [
src/go/plugin/agent/secrets/secretstore/backends/azure/init.go
+27
-15
@@ -12,15 +12,27 @@ import (
12
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
13
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
14
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
15
+ "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
16
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
17
)
18
19
const azureKeyVaultScope = "https://vault.azure.net/.default"
20
21
func (s *store) init(_ context.Context) error {
22
+ switch {
23
+ case s.Config.Timeout.Duration() < 0:
24
+ return fmt.Errorf("timeout cannot be negative")
25
+ case s.Config.Timeout.Duration() == 0:
26
+ s.Config.Timeout = defaultTimeout
27
+ }
28
+
29
if err := s.Config.ValidateWithPath(""); err != nil {
30
return err
31
}
32
+ s.runtime = &runtime{
33
+ apiClient: httpx.APIClient(s.Config.Timeout.Duration()),
34
+ imdsClient: httpx.NoProxyClient(s.Config.Timeout.Duration()),
35
+ }
36
37
cred, err := s.Config.NewCredentialWithOptions(s.credentialOptions())
38
if err != nil {
@@ -41,7 +53,7 @@ func (s *store) init(_ context.Context) error {
53
}
54
55
s.published = &publishedStore{
44
- provider: s.provider,
56
+ runtime: s.runtime,
57
tokenProvider: tokenProvider,
58
}
59
return nil
@@ -50,19 +62,19 @@ func (s *store) init(_ context.Context) error {
62
func (s *store) authTimeout() time.Duration {
63
switch s.Config.NormalizedMode() {
64
case cloudauth.AzureADAuthModeServicePrincipal:
53
- if s.provider.apiClient != nil {
54
- return s.provider.apiClient.Timeout
65
+ if s.runtime.apiClient != nil {
66
+ return s.runtime.apiClient.Timeout
67
}
68
case cloudauth.AzureADAuthModeManagedIdentity:
57
- if s.provider.imdsClient != nil {
58
- return s.provider.imdsClient.Timeout
69
+ if s.runtime.imdsClient != nil {
70
+ return s.runtime.imdsClient.Timeout
71
}
72
case cloudauth.AzureADAuthModeDefault:
61
- if s.provider.apiClient != nil && s.provider.apiClient.Timeout > 0 {
62
- return s.provider.apiClient.Timeout
73
+ if s.runtime.apiClient != nil && s.runtime.apiClient.Timeout > 0 {
74
+ return s.runtime.apiClient.Timeout
75
}
64
- if s.provider.imdsClient != nil {
65
- return s.provider.imdsClient.Timeout
76
+ if s.runtime.imdsClient != nil {
77
+ return s.runtime.imdsClient.Timeout
78
}
79
}
80
@@ -74,17 +86,17 @@ func (s *store) credentialOptions() *cloudauth.AzureADCredentialOptions {
86
87
switch s.Config.NormalizedMode() {
88
case cloudauth.AzureADAuthModeServicePrincipal:
77
- if s.provider.apiClient != nil && s.provider.apiClient.Transport != nil {
78
- opts.ClientOptions.Transport = transportAdapter{s.provider.apiClient.Transport}
89
+ if s.runtime.apiClient != nil && s.runtime.apiClient.Transport != nil {
90
+ opts.ClientOptions.Transport = transportAdapter{s.runtime.apiClient.Transport}
91
}
92
case cloudauth.AzureADAuthModeManagedIdentity:
81
- if s.provider.imdsClient != nil && s.provider.imdsClient.Transport != nil {
82
- opts.ClientOptions.Transport = transportAdapter{s.provider.imdsClient.Transport}
93
+ if s.runtime.imdsClient != nil && s.runtime.imdsClient.Transport != nil {
94
+ opts.ClientOptions.Transport = transportAdapter{s.runtime.imdsClient.Transport}
95
}
96
case cloudauth.AzureADAuthModeDefault:
97
opts.ClientOptions.Transport = routingTransportAdapter{
86
- defaultRoundTripper: roundTripperForClient(s.provider.apiClient),
87
- noProxyRoundTripper: roundTripperForClient(s.provider.imdsClient),
98
+ defaultRoundTripper: roundTripperForClient(s.runtime.apiClient),
99
+ noProxyRoundTripper: roundTripperForClient(s.runtime.imdsClient),
100
}
101
}
102
src/go/plugin/agent/secrets/secretstore/backends/azure/init_test.go
+68
-18
@@ -11,6 +11,7 @@ import (
11
12
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
13
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
14
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
15
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
16
"github.com/stretchr/testify/assert"
17
"github.com/stretchr/testify/require"
@@ -19,43 +20,58 @@ import (
20
func TestStoreInit(t *testing.T) {
21
tests := map[string]struct {
22
cfg Config
23
+ wantTimeout time.Duration
24
wantErrContains string
25
}{
26
"service principal": {
27
cfg: Config{
26
- Mode: cloudauth.AzureADAuthModeServicePrincipal,
27
- ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
28
- TenantID: "tenant-id",
29
- ClientID: "client-id",
30
- ClientSecret: "client-secret",
28
+ AzureADAuthConfig: cloudauth.AzureADAuthConfig{
29
+ Mode: cloudauth.AzureADAuthModeServicePrincipal,
30
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
31
+ TenantID: "tenant-id",
32
+ ClientID: "client-id",
33
+ ClientSecret: "client-secret",
34
+ },
35
},
36
+ Timeout: confopt.Duration(7 * time.Second),
37
},
38
+ wantTimeout: 7 * time.Second,
39
},
40
"default": {
41
cfg: Config{
36
- Mode: cloudauth.AzureADAuthModeDefault,
42
+ AzureADAuthConfig: cloudauth.AzureADAuthConfig{
43
+ Mode: cloudauth.AzureADAuthModeDefault,
44
+ },
45
},
46
+ wantTimeout: defaultTimeout.Duration(),
47
},
48
"service principal validation": {
49
cfg: Config{
41
- Mode: cloudauth.AzureADAuthModeServicePrincipal,
42
- ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
43
- TenantID: "tenant-id",
44
- ClientID: "client-id",
50
+ AzureADAuthConfig: cloudauth.AzureADAuthConfig{
51
+ Mode: cloudauth.AzureADAuthModeServicePrincipal,
52
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
53
+ TenantID: "tenant-id",
54
+ ClientID: "client-id",
55
+ },
56
},
57
},
58
wantErrContains: "mode_service_principal.client_secret is required",
59
},
60
+ "negative timeout": {
61
+ cfg: Config{
62
+ AzureADAuthConfig: cloudauth.AzureADAuthConfig{
63
+ Mode: cloudauth.AzureADAuthModeDefault,
64
+ },
65
+ Timeout: confopt.Duration(-time.Second),
66
+ },
67
+ wantErrContains: "timeout cannot be negative",
68
+ },
69
}
70
71
for name, tc := range tests {
72
t.Run(name, func(t *testing.T) {
73
s := &store{
74
Config: tc.cfg,
55
- provider: &provider{
56
- apiClient: &http.Client{},
57
- imdsClient: &http.Client{},
58
- },
75
}
76
77
err := s.init(context.Background())
@@ -68,6 +84,8 @@ func TestStoreInit(t *testing.T) {
84
require.NoError(t, err)
85
require.NotNil(t, s.published)
86
assert.NotNil(t, s.published.tokenProvider)
87
+ assert.Equal(t, tc.wantTimeout, s.runtime.apiClient.Timeout)
88
+ assert.Equal(t, tc.wantTimeout, s.runtime.imdsClient.Timeout)
89
})
90
}
91
}
@@ -100,8 +118,12 @@ func TestStoreAuthTimeout(t *testing.T) {
118
for name, tc := range tests {
119
t.Run(name, func(t *testing.T) {
120
s := &store{
103
- Config: Config{Mode: tc.mode},
104
- provider: &provider{
121
+ Config: Config{
122
+ AzureADAuthConfig: cloudauth.AzureADAuthConfig{
123
+ Mode: tc.mode,
124
+ },
125
+ },
126
+ runtime: &runtime{
127
apiClient: &http.Client{Timeout: tc.apiTimeout},
128
imdsClient: &http.Client{Timeout: tc.imdsTimeout},
129
},
@@ -221,8 +243,12 @@ func TestDefaultCredentialTransportRouting(t *testing.T) {
243
var noProxyCalls int
244
245
s := &store{
224
- Config: Config{Mode: cloudauth.AzureADAuthModeDefault},
225
- provider: &provider{
246
+ Config: Config{
247
+ AzureADAuthConfig: cloudauth.AzureADAuthConfig{
248
+ Mode: cloudauth.AzureADAuthModeDefault,
249
+ },
250
+ },
251
+ runtime: &runtime{
252
apiClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
253
defaultCalls++
254
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Header: make(http.Header)}, nil
@@ -248,3 +274,27 @@ func TestDefaultCredentialTransportRouting(t *testing.T) {
274
})
275
}
276
}
277
+
278
+func TestInitBuildsStoreScopedRuntime(t *testing.T) {
279
+ creator := New()
280
+
281
+ first, ok := creator.Create().(*store)
282
+ require.True(t, ok)
283
+ second, ok := creator.Create().(*store)
284
+ require.True(t, ok)
285
+
286
+ assert.Equal(t, defaultTimeout, first.Config.Timeout)
287
+ first.AzureADAuthConfig.Mode = cloudauth.AzureADAuthModeDefault
288
+ second.AzureADAuthConfig.Mode = cloudauth.AzureADAuthModeDefault
289
+
290
+ require.NoError(t, first.init(context.Background()))
291
+ require.NoError(t, second.init(context.Background()))
292
+
293
+ require.NotNil(t, first.runtime)
294
+ require.NotNil(t, second.runtime)
295
+ assert.NotSame(t, first.runtime, second.runtime)
296
+ assert.NotSame(t, first.runtime.apiClient, second.runtime.apiClient)
297
+ assert.NotSame(t, first.runtime.imdsClient, second.runtime.imdsClient)
298
+ assert.Equal(t, defaultTimeout.Duration(), first.runtime.apiClient.Timeout)
299
+ assert.Equal(t, defaultTimeout.Duration(), first.runtime.imdsClient.Timeout)
300
+}
src/go/plugin/agent/secrets/secretstore/backends/azure/metadata.yaml
+4
@@ -78,6 +78,10 @@ setup:
78
description: 'Optional client ID of a user-assigned managed identity when `mode` is `managed_identity`. Leave it empty for the system-assigned identity.'
79
default_value: ''
80
required: false
81
+ - name: 'timeout'
82
+ description: 'Timeout in seconds for HTTP requests made by this secretstore backend.'
83
+ default_value: 3
84
+ required: false
85
examples:
86
folding:
87
title: 'Example configuration'
src/go/plugin/agent/secrets/secretstore/backends/azure/provider.go
+17
-15
@@ -9,8 +9,8 @@ import (
9
"regexp"
10
"time"
11
12
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
13
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
13
- "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
15
)
16
@@ -20,42 +20,44 @@ var (
20
reAzureSafeName = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
21
)
22
23
-type Config = cloudauth.AzureADAuthConfig
23
+var defaultTimeout = confopt.Duration(3 * time.Second)
24
25
-type provider struct {
25
+type Config struct {
26
+ cloudauth.AzureADAuthConfig `yaml:",inline" json:",inline"`
27
+ Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`
28
+}
29
+
30
+type runtime struct {
31
apiClient *http.Client
32
imdsClient *http.Client
33
}
34
35
type store struct {
36
Config `yaml:",inline" json:""`
32
- provider *provider
37
+ runtime *runtime
38
published *publishedStore
39
}
40
41
type publishedStore struct {
37
- provider *provider
42
+ runtime *runtime
43
tokenProvider *cloudauth.TokenProvider
44
}
45
46
func New() secretstore.Creator {
42
- p := &provider{
43
- apiClient: httpx.APIClient(10 * time.Second),
44
- imdsClient: httpx.NoProxyClient(2 * time.Second),
45
- }
46
-
47
return secretstore.Creator{
48
Kind: secretstore.KindAzureKV,
49
DisplayName: "Azure Key Vault",
50
Schema: configSchema,
51
- Create: p.create,
51
+ Create: func() secretstore.Store {
52
+ return &store{
53
+ Config: Config{
54
+ Timeout: defaultTimeout,
55
+ },
56
+ }
57
+ },
58
}
59
}
60
55
-func (p *provider) create() secretstore.Store {
56
- return &store{provider: p}
57
-}
58
-
61
func (s *store) Configuration() any { return &s.Config }
62
63
func (s *store) Init(ctx context.Context) error { return s.init(ctx) }
src/go/plugin/agent/secrets/secretstore/backends/azure/resolve.go
+1
-1
@@ -42,7 +42,7 @@ func (s *publishedStore) resolve(ctx context.Context, req secretstore.ResolveReq
42
}
43
httpReq.Header.Set("Authorization", "Bearer "+token)
44
45
- resp, err := s.provider.apiClient.Do(httpReq)
45
+ resp, err := s.runtime.apiClient.Do(httpReq)
46
if err != nil {
47
return "", fmt.Errorf("resolving secret '%s': store '%s': request failed: %w", req.Original, req.StoreKey, err)
48
}
src/go/plugin/agent/secrets/secretstore/backends/azure/resolve_test.go
+2
-2
@@ -90,7 +90,7 @@ func TestPublishedStoreResolve(t *testing.T) {
90
require.NoError(t, err)
91
92
s := &publishedStore{
93
- provider: &provider{
93
+ runtime: &runtime{
94
apiClient: &http.Client{Transport: tc.transport},
95
},
96
tokenProvider: tokenProvider,
@@ -131,7 +131,7 @@ func TestPublishedStoreResolve_LogsDetailedResolution(t *testing.T) {
131
require.NoError(t, err)
132
133
s := &publishedStore{
134
- provider: &provider{
134
+ runtime: &runtime{
135
apiClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
136
return &http.Response{
137
StatusCode: http.StatusOK,
src/go/plugin/agent/secrets/secretstore/backends/gcp/config_schema.json
+7
@@ -13,6 +13,13 @@
13
"service_account_file"
14
],
15
"default": "metadata"
16
+ },
17
+ "timeout": {
18
+ "title": "Timeout",
19
+ "description": "Timeout in seconds for HTTP requests made by this secretstore backend.",
20
+ "type": "number",
21
+ "minimum": 0,
22
+ "default": 3
23
}
24
},
25
"required": [
src/go/plugin/agent/secrets/secretstore/backends/gcp/init.go
+13
-1
@@ -8,10 +8,22 @@ import (
8
"strings"
9
10
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
11
+ "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
12
)
13
14
func (s *store) init(_ context.Context) error {
14
- published := &publishedStore{provider: s.provider}
15
+ switch {
16
+ case s.Config.Timeout.Duration() < 0:
17
+ return fmt.Errorf("timeout cannot be negative")
18
+ case s.Config.Timeout.Duration() == 0:
19
+ s.Config.Timeout = defaultTimeout
20
+ }
21
+ s.runtime = &runtime{
22
+ apiClient: httpx.APIClient(s.Config.Timeout.Duration()),
23
+ metadataClient: httpx.NoProxyClient(s.Config.Timeout.Duration()),
24
+ }
25
+
26
+ published := &publishedStore{runtime: s.runtime}
27
28
switch strings.TrimSpace(s.Config.Mode) {
29
case "metadata":
src/go/plugin/agent/secrets/secretstore/backends/gcp/init_test.go
new
+80
@@ -0,0 +1,80 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package gcp
4
+
5
+import (
6
+ "context"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
11
+ "github.com/stretchr/testify/assert"
12
+ "github.com/stretchr/testify/require"
13
+)
14
+
15
+func TestStoreInitTimeout(t *testing.T) {
16
+ tests := map[string]struct {
17
+ timeout confopt.Duration
18
+ wantTimeout time.Duration
19
+ wantErrContains string
20
+ }{
21
+ "default timeout": {
22
+ wantTimeout: defaultTimeout.Duration(),
23
+ },
24
+ "configured timeout": {
25
+ timeout: confopt.Duration(7 * time.Second),
26
+ wantTimeout: 7 * time.Second,
27
+ },
28
+ "negative timeout": {
29
+ timeout: confopt.Duration(-time.Second),
30
+ wantErrContains: "timeout cannot be negative",
31
+ },
32
+ }
33
+
34
+ for name, tc := range tests {
35
+ t.Run(name, func(t *testing.T) {
36
+ s := &store{
37
+ Config: Config{
38
+ Mode: "metadata",
39
+ Timeout: tc.timeout,
40
+ },
41
+ }
42
+
43
+ err := s.init(context.Background())
44
+ if tc.wantErrContains != "" {
45
+ require.Error(t, err)
46
+ assert.ErrorContains(t, err, tc.wantErrContains)
47
+ return
48
+ }
49
+
50
+ require.NoError(t, err)
51
+ assert.Equal(t, tc.wantTimeout, s.runtime.apiClient.Timeout)
52
+ assert.Equal(t, tc.wantTimeout, s.runtime.metadataClient.Timeout)
53
+ assert.Equal(t, confopt.Duration(tc.wantTimeout), s.Config.Timeout)
54
+ })
55
+ }
56
+}
57
+
58
+func TestInitBuildsStoreScopedRuntime(t *testing.T) {
59
+ creator := New()
60
+
61
+ first, ok := creator.Create().(*store)
62
+ require.True(t, ok)
63
+ second, ok := creator.Create().(*store)
64
+ require.True(t, ok)
65
+
66
+ assert.Equal(t, defaultTimeout, first.Config.Timeout)
67
+ first.Mode = "metadata"
68
+ second.Mode = "metadata"
69
+
70
+ require.NoError(t, first.init(context.Background()))
71
+ require.NoError(t, second.init(context.Background()))
72
+
73
+ require.NotNil(t, first.runtime)
74
+ require.NotNil(t, second.runtime)
75
+ assert.NotSame(t, first.runtime, second.runtime)
76
+ assert.NotSame(t, first.runtime.apiClient, second.runtime.apiClient)
77
+ assert.NotSame(t, first.runtime.metadataClient, second.runtime.metadataClient)
78
+ assert.Equal(t, defaultTimeout.Duration(), first.runtime.apiClient.Timeout)
79
+ assert.Equal(t, defaultTimeout.Duration(), first.runtime.metadataClient.Timeout)
80
+}
src/go/plugin/agent/secrets/secretstore/backends/gcp/metadata.yaml
+4
@@ -64,6 +64,10 @@ setup:
64
description: 'Absolute path to a service account JSON file. Required when `mode` is `service_account_file`. The file contains a private key and should be readable only by the `netdata` user or another tightly scoped owner.'
65
default_value: ''
66
required: true
67
+ - name: 'timeout'
68
+ description: 'Timeout in seconds for HTTP requests made by this secretstore backend.'
69
+ default_value: 3
70
+ required: false
71
examples:
72
folding:
73
title: 'Example configuration'
src/go/plugin/agent/secrets/secretstore/backends/gcp/provider.go
+14
-17
@@ -9,8 +9,8 @@ import (
9
"regexp"
10
"time"
11
12
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
13
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
13
- "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
14
)
15
16
var (
@@ -20,53 +20,50 @@ var (
20
reGCPSafeName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
21
)
22
23
+var defaultTimeout = confopt.Duration(3 * time.Second)
24
+
25
type Config struct {
26
Mode string `json:"mode" yaml:"mode"`
27
ModeServiceAccountFile *ModeServiceAccountFileConfig `json:"mode_service_account_file,omitempty" yaml:"mode_service_account_file,omitempty"`
28
+ Timeout confopt.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
29
}
30
31
type ModeServiceAccountFileConfig struct {
32
Path string `json:"path" yaml:"path"`
33
}
34
32
-type provider struct {
35
+type runtime struct {
36
apiClient *http.Client
37
metadataClient *http.Client
35
- secretEndpoint string
36
- now func() time.Time
38
}
39
40
type store struct {
41
Config `yaml:",inline" json:""`
41
- provider *provider
42
+ runtime *runtime
43
published *publishedStore
44
}
45
46
type publishedStore struct {
46
- provider *provider
47
+ runtime *runtime
48
mode string
49
serviceAccountFilePath string
50
}
51
52
func New() secretstore.Creator {
52
- p := &provider{
53
- apiClient: httpx.APIClient(10 * time.Second),
54
- metadataClient: httpx.NoProxyClient(2 * time.Second),
55
- now: time.Now,
56
- }
57
-
53
return secretstore.Creator{
54
Kind: secretstore.KindGCPSM,
55
DisplayName: "Google Secret Manager",
56
Schema: configSchema,
62
- Create: p.create,
57
+ Create: func() secretstore.Store {
58
+ return &store{
59
+ Config: Config{
60
+ Timeout: defaultTimeout,
61
+ },
62
+ }
63
+ },
64
}
65
}
66
66
-func (p *provider) create() secretstore.Store {
67
- return &store{provider: p}
68
-}
69
-
67
func (s *store) Configuration() any { return &s.Config }
68
69
func (s *store) Init(ctx context.Context) error { return s.init(ctx) }
src/go/plugin/agent/secrets/secretstore/backends/gcp/resolve.go
+6
-9
@@ -18,6 +18,7 @@ import (
18
"net/url"
19
"os"
20
"strings"
21
+ "time"
22
23
"github.com/netdata/netdata/go/plugins/logger"
24
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
@@ -48,16 +49,12 @@ func (s *publishedStore) resolve(ctx context.Context, req secretstore.ResolveReq
49
return "", fmt.Errorf("resolving secret '%s': store '%s': %w", req.Original, req.StoreKey, err)
50
}
51
51
- baseURL := s.provider.secretEndpoint
52
- if baseURL == "" {
53
- baseURL = "https://secretmanager.googleapis.com"
54
- }
55
- httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/projects/%s/secrets/%s/versions/%s:access", baseURL, project, secretName, version), nil)
52
+ httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://secretmanager.googleapis.com/v1/projects/%s/secrets/%s/versions/%s:access", project, secretName, version), nil)
53
if err != nil {
54
return "", fmt.Errorf("resolving secret '%s': store '%s': creating request: %w", req.Original, req.StoreKey, err)
55
}
56
httpReq.Header.Set("Authorization", "Bearer "+token)
60
- resp, err := s.provider.apiClient.Do(httpReq)
57
+ resp, err := s.runtime.apiClient.Do(httpReq)
58
if err != nil {
59
return "", fmt.Errorf("resolving secret '%s': store '%s': request failed: %w", req.Original, req.StoreKey, err)
60
}
@@ -124,7 +121,7 @@ func (s *publishedStore) metadataToken(ctx context.Context) (string, error) {
121
return "", fmt.Errorf("creating metadata token request: %w", err)
122
}
123
req.Header.Set("Metadata-Flavor", "Google")
127
- resp, err := s.provider.metadataClient.Do(req)
124
+ resp, err := s.runtime.metadataClient.Do(req)
125
if err != nil {
126
return "", fmt.Errorf("metadata token request failed: %w", err)
127
}
@@ -164,7 +161,7 @@ func (s *publishedStore) serviceAccountToken(ctx context.Context, credFile strin
161
if sa.ClientEmail == "" || sa.PrivateKey == "" || sa.TokenURI == "" {
162
return "", fmt.Errorf("service account JSON missing required fields (client_email, private_key, token_uri)")
163
}
167
- now := s.provider.now().Unix()
164
+ now := time.Now().Unix()
165
signedJWT, err := createSignedJWT(sa.ClientEmail, sa.TokenURI, sa.PrivateKey, now)
166
if err != nil {
167
return "", err
@@ -178,7 +175,7 @@ func (s *publishedStore) serviceAccountToken(ctx context.Context, credFile strin
175
return "", fmt.Errorf("creating token exchange request: %w", err)
176
}
177
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
181
- resp, err := s.provider.apiClient.Do(httpReq)
178
+ resp, err := s.runtime.apiClient.Do(httpReq)
179
if err != nil {
180
return "", fmt.Errorf("token exchange request failed: %w", err)
181
}
src/go/plugin/agent/secrets/secretstore/backends/gcp/resolve_test.go
+1
-1
@@ -24,7 +24,7 @@ func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
24
25
func TestPublishedStoreResolve_LogsDetailedResolution(t *testing.T) {
26
s := &publishedStore{
27
- provider: &provider{
27
+ runtime: &runtime{
28
apiClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
29
assert.Equal(t, "/v1/projects/my-project/secrets/my-secret/versions/latest:access", req.URL.Path)
30
return &http.Response{
src/go/plugin/agent/secrets/secretstore/backends/vault/config_schema.json
+7
@@ -29,6 +29,13 @@
29
"description": "Disable TLS certificate verification for Vault requests.",
30
"type": "boolean",
31
"default": false
32
+ },
33
+ "timeout": {
34
+ "title": "Timeout",
35
+ "description": "Timeout in seconds for HTTP requests made by this secretstore backend.",
36
+ "type": "number",
37
+ "minimum": 0,
38
+ "default": 3
39
}
40
},
41
"required": [
src/go/plugin/agent/secrets/secretstore/backends/vault/init.go
+12
-2
@@ -8,12 +8,22 @@ import (
8
"strings"
9
10
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
11
+ "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
12
)
13
14
func (s *store) init(_ context.Context) error {
14
- published := &publishedStore{
15
- provider: s.provider,
15
+ switch {
16
+ case s.Config.Timeout.Duration() < 0:
17
+ return fmt.Errorf("timeout cannot be negative")
18
+ case s.Config.Timeout.Duration() == 0:
19
+ s.Config.Timeout = defaultTimeout
20
}
21
+ s.runtime = &runtime{
22
+ httpClient: httpx.VaultClient(s.Config.Timeout.Duration()),
23
+ httpClientInsecure: httpx.VaultInsecureClient(s.Config.Timeout.Duration()),
24
+ }
25
+
26
+ published := &publishedStore{runtime: s.runtime}
27
28
switch strings.TrimSpace(s.Config.Mode) {
29
case "token":
src/go/plugin/agent/secrets/secretstore/backends/vault/init_test.go
new
+88
@@ -0,0 +1,88 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package vault
4
+
5
+import (
6
+ "context"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
11
+ "github.com/stretchr/testify/assert"
12
+ "github.com/stretchr/testify/require"
13
+)
14
+
15
+func TestStoreInitTimeout(t *testing.T) {
16
+ tests := map[string]struct {
17
+ timeout confopt.Duration
18
+ wantTimeout time.Duration
19
+ wantErrContains string
20
+ }{
21
+ "default timeout": {
22
+ wantTimeout: defaultTimeout.Duration(),
23
+ },
24
+ "configured timeout": {
25
+ timeout: confopt.Duration(7 * time.Second),
26
+ wantTimeout: 7 * time.Second,
27
+ },
28
+ "negative timeout": {
29
+ timeout: confopt.Duration(-time.Second),
30
+ wantErrContains: "timeout cannot be negative",
31
+ },
32
+ }
33
+
34
+ for name, tc := range tests {
35
+ t.Run(name, func(t *testing.T) {
36
+ s := &store{
37
+ Config: Config{
38
+ Mode: "token",
39
+ ModeToken: &ModeTokenConfig{
40
+ Token: "vault-token",
41
+ },
42
+ Addr: "https://vault.example",
43
+ Timeout: tc.timeout,
44
+ },
45
+ }
46
+
47
+ err := s.init(context.Background())
48
+ if tc.wantErrContains != "" {
49
+ require.Error(t, err)
50
+ assert.ErrorContains(t, err, tc.wantErrContains)
51
+ return
52
+ }
53
+
54
+ require.NoError(t, err)
55
+ assert.Equal(t, tc.wantTimeout, s.runtime.httpClient.Timeout)
56
+ assert.Equal(t, tc.wantTimeout, s.runtime.httpClientInsecure.Timeout)
57
+ assert.Equal(t, confopt.Duration(tc.wantTimeout), s.Config.Timeout)
58
+ })
59
+ }
60
+}
61
+
62
+func TestInitBuildsStoreScopedRuntime(t *testing.T) {
63
+ creator := New()
64
+
65
+ first, ok := creator.Create().(*store)
66
+ require.True(t, ok)
67
+ second, ok := creator.Create().(*store)
68
+ require.True(t, ok)
69
+
70
+ assert.Equal(t, defaultTimeout, first.Config.Timeout)
71
+ first.Mode = "token"
72
+ first.ModeToken = &ModeTokenConfig{Token: "vault-token"}
73
+ first.Addr = "https://vault.example"
74
+ second.Mode = "token"
75
+ second.ModeToken = &ModeTokenConfig{Token: "vault-token"}
76
+ second.Addr = "https://vault.example"
77
+
78
+ require.NoError(t, first.init(context.Background()))
79
+ require.NoError(t, second.init(context.Background()))
80
+
81
+ require.NotNil(t, first.runtime)
82
+ require.NotNil(t, second.runtime)
83
+ assert.NotSame(t, first.runtime, second.runtime)
84
+ assert.NotSame(t, first.runtime.httpClient, second.runtime.httpClient)
85
+ assert.NotSame(t, first.runtime.httpClientInsecure, second.runtime.httpClientInsecure)
86
+ assert.Equal(t, defaultTimeout.Duration(), first.runtime.httpClient.Timeout)
87
+ assert.Equal(t, defaultTimeout.Duration(), first.runtime.httpClientInsecure.Timeout)
88
+}
src/go/plugin/agent/secrets/secretstore/backends/vault/metadata.yaml
+4
@@ -72,6 +72,10 @@ setup:
72
required: false
73
detailed_description: |
74
This is insecure. Use it only as a temporary workaround or in a non-production environment.
75
+ - name: 'timeout'
76
+ description: 'Timeout in seconds for HTTP requests made by this secretstore backend.'
77
+ default_value: 3
78
+ required: false
79
- name: 'mode_token.token'
80
group: 'Token'
81
description: 'Vault token value. Required when `mode` is `token`.'
src/go/plugin/agent/secrets/secretstore/backends/vault/provider.go
+14
-14
@@ -8,13 +8,15 @@ import (
8
"net/http"
9
"time"
10
11
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
12
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
12
- "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
13
)
14
15
//go:embed config_schema.json
16
var configSchema string
17
18
+var defaultTimeout = confopt.Duration(3 * time.Second)
19
+
20
type Config struct {
21
Mode string `json:"mode" yaml:"mode"`
22
ModeToken *ModeTokenConfig `json:"mode_token,omitempty" yaml:"mode_token,omitempty"`
@@ -22,6 +24,7 @@ type Config struct {
24
Addr string `json:"addr" yaml:"addr"`
25
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
26
TLSSkipVerify bool `json:"tls_skip_verify,omitempty" yaml:"tls_skip_verify,omitempty"`
27
+ Timeout confopt.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
28
}
29
30
type ModeTokenConfig struct {
@@ -32,19 +35,19 @@ type ModeTokenFileConfig struct {
35
Path string `json:"path" yaml:"path"`
36
}
37
35
-type provider struct {
38
+type runtime struct {
39
httpClient *http.Client
40
httpClientInsecure *http.Client
41
}
42
43
type store struct {
44
Config `yaml:",inline" json:""`
42
- provider *provider
45
+ runtime *runtime
46
published *publishedStore
47
}
48
49
type publishedStore struct {
47
- provider *provider
50
+ runtime *runtime
51
mode string
52
tokenValue string
53
tokenFilePath string
@@ -54,23 +57,20 @@ type publishedStore struct {
57
}
58
59
func New() secretstore.Creator {
57
- p := &provider{
58
- httpClient: httpx.VaultClient(10 * time.Second),
59
- httpClientInsecure: httpx.VaultInsecureClient(10 * time.Second),
60
- }
61
-
60
return secretstore.Creator{
61
Kind: secretstore.KindVault,
62
DisplayName: "Vault",
63
Schema: configSchema,
66
- Create: p.create,
64
+ Create: func() secretstore.Store {
65
+ return &store{
66
+ Config: Config{
67
+ Timeout: defaultTimeout,
68
+ },
69
+ }
70
+ },
71
}
72
}
73
70
-func (p *provider) create() secretstore.Store {
71
- return &store{provider: p}
72
-}
73
-
74
func (s *store) Configuration() any { return &s.Config }
75
76
func (s *store) Init(ctx context.Context) error { return s.init(ctx) }
src/go/plugin/agent/secrets/secretstore/backends/vault/resolve.go
+2
-2
@@ -51,9 +51,9 @@ func (s *publishedStore) resolve(ctx context.Context, req secretstore.ResolveReq
51
httpReq.Header.Set("X-Vault-Namespace", ns)
52
}
53
54
- client := s.provider.httpClient
54
+ client := s.runtime.httpClient
55
if s.skipVerify() {
56
- client = s.provider.httpClientInsecure
56
+ client = s.runtime.httpClientInsecure
57
}
58
59
resp, err := client.Do(httpReq)
src/go/plugin/agent/secrets/secretstore/backends/vault/resolve_test.go
+1
-1
@@ -81,7 +81,7 @@ func TestParseResponse(t *testing.T) {
81
82
func TestPublishedStoreResolve_LogsDetailedResolution(t *testing.T) {
83
s := &publishedStore{
84
- provider: &provider{
84
+ runtime: &runtime{
85
httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
86
return &http.Response{
87
StatusCode: http.StatusOK,