refactor(go/cloudauth): adopt shared Azure auth schema for collectors and secretstore (#21995)
Ilya Mashchenko committed
Mar 20, 2026 at 23:08 UTC
0650102c70096fef699bef8131c0cb1466432833
26 files changed
+1143
-391
src/go/plugin/agent/secrets/secretstore/backends/azure/config_schema.json
+19
-10
@@ -9,10 +9,11 @@
9
"description": "Azure authentication mode.",
10
"type": "string",
11
"enum": [
12
- "client",
13
- "managed_identity"
12
+ "service_principal",
13
+ "managed_identity",
14
+ "default"
15
],
15
- "default": "client"
16
+ "default": "default"
17
}
18
},
19
"required": [
@@ -24,11 +25,11 @@
25
{
26
"properties": {
27
"mode": {
27
- "const": "client"
28
+ "const": "service_principal"
29
},
29
- "mode_client": {
30
- "title": "Client",
31
- "description": "Client credentials used when mode is `client`.",
30
+ "mode_service_principal": {
31
+ "title": "Service Principal",
32
+ "description": "Service principal credentials used when mode is `service_principal`.",
33
"type": "object",
34
"properties": {
35
"tenant_id": {
@@ -56,7 +57,7 @@
57
}
58
},
59
"required": [
59
- "mode_client"
60
+ "mode_service_principal"
61
]
62
},
63
{
@@ -77,6 +78,13 @@
78
}
79
}
80
}
81
+ },
82
+ {
83
+ "properties": {
84
+ "mode": {
85
+ "const": "default"
86
+ }
87
+ }
88
}
89
]
90
}
@@ -90,9 +98,10 @@
98
"ui:widget": "radio",
99
"ui:options": {
100
"inline": true
93
- }
101
+ },
102
+ "ui:help": "Choose how Netdata gets Azure credentials.\n\n- `service_principal`: Use an Azure app / service principal. Requires `tenant_id`, `client_id`, and `client_secret` in `mode_service_principal`.\n- `managed_identity`: Use the managed identity attached to the Azure resource running Netdata. Set `mode_managed_identity.client_id` only for a user-assigned identity.\n- `default`: Use the Azure SDK `DefaultAzureCredential` chain. This automatically tries available Azure credential sources, such as environment-based credentials, managed identity, and local developer credentials.\n\nUse `service_principal` for explicit app credentials. Use `managed_identity` when Netdata runs on an Azure resource with an attached identity. Use `default` when you want Azure SDK auto-discovery or local development convenience."
103
},
95
- "mode_client": {
104
+ "mode_service_principal": {
105
"client_secret": {
106
"ui:widget": "password"
107
}
src/go/plugin/agent/secrets/secretstore/backends/azure/init.go
+147
-38
@@ -5,57 +5,166 @@ package azure
5
import (
6
"context"
7
"fmt"
8
- "strings"
8
+ "net"
9
+ "net/http"
10
+ "time"
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/plugin/agent/secrets/secretstore"
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
16
)
17
18
+const azureKeyVaultScope = "https://vault.azure.net/.default"
19
+
20
func (s *store) init(_ context.Context) error {
14
- published := &publishedStore{provider: s.provider}
21
+ if err := s.Config.ValidateWithPath(""); err != nil {
22
+ return err
23
+ }
24
+
25
+ cred, err := s.Config.NewCredentialWithOptions(s.credentialOptions())
26
+ if err != nil {
27
+ return fmt.Errorf("creating azure credential for kind %q: %w", secretstore.KindAzureKV, err)
28
+ }
29
+ cred = credentialWithTimeout{
30
+ cred: cred,
31
+ timeout: s.authTimeout(),
32
+ }
33
+
34
+ tokenProvider, err := cloudauth.NewTokenProvider(
35
+ cred,
36
+ []string{azureKeyVaultScope},
37
+ cloudauth.DefaultTokenRefreshMargin,
38
+ )
39
+ if err != nil {
40
+ return fmt.Errorf("creating azure token provider for kind %q: %w", secretstore.KindAzureKV, err)
41
+ }
42
16
- switch strings.TrimSpace(s.Config.Mode) {
17
- case "client":
18
- if s.Config.ModeClient == nil {
19
- return fmt.Errorf("mode_client is required when mode is 'client'")
43
+ s.published = &publishedStore{
44
+ provider: s.provider,
45
+ tokenProvider: tokenProvider,
46
+ }
47
+ return nil
48
+}
49
+
50
+func (s *store) authTimeout() time.Duration {
51
+ switch s.Config.NormalizedMode() {
52
+ case cloudauth.AzureADAuthModeServicePrincipal:
53
+ if s.provider.apiClient != nil {
54
+ return s.provider.apiClient.Timeout
55
}
21
- tenantID := strings.TrimSpace(s.Config.ModeClient.TenantID)
22
- if tenantID == "" {
23
- return fmt.Errorf("mode_client.tenant_id is required")
56
+ case cloudauth.AzureADAuthModeManagedIdentity:
57
+ if s.provider.imdsClient != nil {
58
+ return s.provider.imdsClient.Timeout
59
}
25
- clientID := strings.TrimSpace(s.Config.ModeClient.ClientID)
26
- if clientID == "" {
27
- return fmt.Errorf("mode_client.client_id is required")
60
+ case cloudauth.AzureADAuthModeDefault:
61
+ if s.provider.apiClient != nil && s.provider.apiClient.Timeout > 0 {
62
+ return s.provider.apiClient.Timeout
63
}
29
- clientSecret := strings.TrimSpace(s.Config.ModeClient.ClientSecret)
30
- if clientSecret == "" {
31
- return fmt.Errorf("mode_client.client_secret is required")
64
+ if s.provider.imdsClient != nil {
65
+ return s.provider.imdsClient.Timeout
66
}
33
- s.Config.Mode = "client"
34
- s.Config.ModeClient.TenantID = tenantID
35
- s.Config.ModeClient.ClientID = clientID
36
- s.Config.ModeClient.ClientSecret = clientSecret
37
- s.Config.ModeManagedIdentity = nil
38
- published.mode = s.Config.Mode
39
- published.clientTenantID = tenantID
40
- published.clientID = clientID
41
- published.clientSecret = clientSecret
42
- case "managed_identity":
43
- s.Config.Mode = "managed_identity"
44
- s.Config.ModeClient = nil
45
- published.mode = s.Config.Mode
46
- if s.Config.ModeManagedIdentity != nil {
47
- clientID := strings.TrimSpace(s.Config.ModeManagedIdentity.ClientID)
48
- if clientID != "" {
49
- s.Config.ModeManagedIdentity.ClientID = clientID
50
- published.managedIdentityClientID = clientID
51
- } else {
52
- s.Config.ModeManagedIdentity = nil
53
- }
67
+ }
68
+
69
+ return 0
70
+}
71
+
72
+func (s *store) credentialOptions() *cloudauth.AzureADCredentialOptions {
73
+ opts := &cloudauth.AzureADCredentialOptions{}
74
+
75
+ switch s.Config.NormalizedMode() {
76
+ case cloudauth.AzureADAuthModeServicePrincipal:
77
+ if s.provider.apiClient != nil && s.provider.apiClient.Transport != nil {
78
+ opts.ClientOptions.Transport = transportAdapter{s.provider.apiClient.Transport}
79
}
80
+ case cloudauth.AzureADAuthModeManagedIdentity:
81
+ if s.provider.imdsClient != nil && s.provider.imdsClient.Transport != nil {
82
+ opts.ClientOptions.Transport = transportAdapter{s.provider.imdsClient.Transport}
83
+ }
84
+ case cloudauth.AzureADAuthModeDefault:
85
+ opts.ClientOptions.Transport = routingTransportAdapter{
86
+ defaultRoundTripper: roundTripperForClient(s.provider.apiClient),
87
+ noProxyRoundTripper: roundTripperForClient(s.provider.imdsClient),
88
+ }
89
+ }
90
+
91
+ return opts
92
+}
93
+
94
+type transportAdapter struct {
95
+ roundTripper httpRoundTripper
96
+}
97
+
98
+type httpRoundTripper interface {
99
+ RoundTrip(*http.Request) (*http.Response, error)
100
+}
101
+
102
+func (t transportAdapter) Do(req *http.Request) (*http.Response, error) {
103
+ return t.roundTripper.RoundTrip(req)
104
+}
105
+
106
+type routingTransportAdapter struct {
107
+ defaultRoundTripper httpRoundTripper
108
+ noProxyRoundTripper httpRoundTripper
109
+}
110
+
111
+func (t routingTransportAdapter) Do(req *http.Request) (*http.Response, error) {
112
+ switch {
113
+ case shouldUseNoProxyTransport(req) && t.noProxyRoundTripper != nil:
114
+ return t.noProxyRoundTripper.RoundTrip(req)
115
+ case t.defaultRoundTripper != nil:
116
+ return t.defaultRoundTripper.RoundTrip(req)
117
+ case t.noProxyRoundTripper != nil:
118
+ return t.noProxyRoundTripper.RoundTrip(req)
119
default:
56
- return fmt.Errorf("mode '%s' is invalid for kind '%s'", s.Config.Mode, secretstore.KindAzureKV)
120
+ return http.DefaultTransport.RoundTrip(req)
121
}
122
+}
123
59
- s.published = published
124
+func roundTripperForClient(client *http.Client) httpRoundTripper {
125
+ if client != nil && client.Transport != nil {
126
+ return client.Transport
127
+ }
128
+ if rt, ok := http.DefaultTransport.(httpRoundTripper); ok {
129
+ return rt
130
+ }
131
return nil
132
}
133
+
134
+// Managed identity endpoints are local or link-local and must bypass proxies.
135
+func shouldUseNoProxyTransport(req *http.Request) bool {
136
+ if req == nil || req.URL == nil {
137
+ return false
138
+ }
139
+
140
+ host := req.URL.Hostname()
141
+ if host == "" {
142
+ return false
143
+ }
144
+ if host == "localhost" {
145
+ return true
146
+ }
147
+
148
+ ip := net.ParseIP(host)
149
+ if ip == nil {
150
+ return false
151
+ }
152
+
153
+ return ip.IsLoopback() || ip.IsLinkLocalUnicast()
154
+}
155
+
156
+type credentialWithTimeout struct {
157
+ cred azcore.TokenCredential
158
+ timeout time.Duration
159
+}
160
+
161
+func (c credentialWithTimeout) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
162
+ if c.timeout <= 0 {
163
+ return c.cred.GetToken(ctx, opts)
164
+ }
165
+
166
+ ctx, cancel := context.WithTimeout(ctx, c.timeout)
167
+ defer cancel()
168
+
169
+ return c.cred.GetToken(ctx, opts)
170
+}
src/go/plugin/agent/secrets/secretstore/backends/azure/init_test.go
new
+250
@@ -0,0 +1,250 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package azure
4
+
5
+import (
6
+ "context"
7
+ "errors"
8
+ "net/http"
9
+ "testing"
10
+ "time"
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/plugin/go.d/pkg/cloudauth"
15
+ "github.com/stretchr/testify/assert"
16
+ "github.com/stretchr/testify/require"
17
+)
18
+
19
+func TestStoreInit(t *testing.T) {
20
+ tests := map[string]struct {
21
+ cfg Config
22
+ wantErrContains string
23
+ }{
24
+ "service principal": {
25
+ cfg: Config{
26
+ Mode: cloudauth.AzureADAuthModeServicePrincipal,
27
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
28
+ TenantID: "tenant-id",
29
+ ClientID: "client-id",
30
+ ClientSecret: "client-secret",
31
+ },
32
+ },
33
+ },
34
+ "default": {
35
+ cfg: Config{
36
+ Mode: cloudauth.AzureADAuthModeDefault,
37
+ },
38
+ },
39
+ "service principal validation": {
40
+ cfg: Config{
41
+ Mode: cloudauth.AzureADAuthModeServicePrincipal,
42
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
43
+ TenantID: "tenant-id",
44
+ ClientID: "client-id",
45
+ },
46
+ },
47
+ wantErrContains: "mode_service_principal.client_secret is required",
48
+ },
49
+ }
50
+
51
+ for name, tc := range tests {
52
+ t.Run(name, func(t *testing.T) {
53
+ s := &store{
54
+ Config: tc.cfg,
55
+ provider: &provider{
56
+ apiClient: &http.Client{},
57
+ imdsClient: &http.Client{},
58
+ },
59
+ }
60
+
61
+ err := s.init(context.Background())
62
+ if tc.wantErrContains != "" {
63
+ require.Error(t, err)
64
+ assert.ErrorContains(t, err, tc.wantErrContains)
65
+ return
66
+ }
67
+
68
+ require.NoError(t, err)
69
+ require.NotNil(t, s.published)
70
+ assert.NotNil(t, s.published.tokenProvider)
71
+ })
72
+ }
73
+}
74
+
75
+func TestStoreAuthTimeout(t *testing.T) {
76
+ tests := map[string]struct {
77
+ mode string
78
+ apiTimeout time.Duration
79
+ imdsTimeout time.Duration
80
+ want time.Duration
81
+ }{
82
+ "service principal uses api client timeout": {
83
+ mode: cloudauth.AzureADAuthModeServicePrincipal,
84
+ apiTimeout: 10 * time.Second,
85
+ want: 10 * time.Second,
86
+ },
87
+ "managed identity uses imds timeout": {
88
+ mode: cloudauth.AzureADAuthModeManagedIdentity,
89
+ imdsTimeout: 2 * time.Second,
90
+ want: 2 * time.Second,
91
+ },
92
+ "default prefers api client timeout": {
93
+ mode: cloudauth.AzureADAuthModeDefault,
94
+ apiTimeout: 10 * time.Second,
95
+ imdsTimeout: 2 * time.Second,
96
+ want: 10 * time.Second,
97
+ },
98
+ }
99
+
100
+ for name, tc := range tests {
101
+ t.Run(name, func(t *testing.T) {
102
+ s := &store{
103
+ Config: Config{Mode: tc.mode},
104
+ provider: &provider{
105
+ apiClient: &http.Client{Timeout: tc.apiTimeout},
106
+ imdsClient: &http.Client{Timeout: tc.imdsTimeout},
107
+ },
108
+ }
109
+
110
+ assert.Equal(t, tc.want, s.authTimeout())
111
+ })
112
+ }
113
+}
114
+
115
+func TestCredentialWithTimeout(t *testing.T) {
116
+ errMissingDeadline := errors.New("missing deadline")
117
+
118
+ tests := map[string]struct {
119
+ timeout time.Duration
120
+ buildTokenFn func(t *testing.T) (func(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error), func(t *testing.T))
121
+ wantErr bool
122
+ wantErrIs error
123
+ wantToken string
124
+ }{
125
+ "zero timeout passes through": {
126
+ timeout: 0,
127
+ buildTokenFn: func(*testing.T) (func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error), func(t *testing.T)) {
128
+ return func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) {
129
+ return azcore.AccessToken{Token: "ok"}, nil
130
+ }, nil
131
+ },
132
+ wantToken: "ok",
133
+ },
134
+ "timeout cancels token request": {
135
+ timeout: 20 * time.Millisecond,
136
+ buildTokenFn: func(*testing.T) (func(ctx context.Context, _ policy.TokenRequestOptions) (azcore.AccessToken, error), func(t *testing.T)) {
137
+ var sawDeadline bool
138
+
139
+ return func(ctx context.Context, _ policy.TokenRequestOptions) (azcore.AccessToken, error) {
140
+ if _, ok := ctx.Deadline(); !ok {
141
+ return azcore.AccessToken{}, errMissingDeadline
142
+ }
143
+
144
+ sawDeadline = true
145
+ <-ctx.Done()
146
+ return azcore.AccessToken{}, ctx.Err()
147
+ }, func(t *testing.T) {
148
+ assert.True(t, sawDeadline)
149
+ }
150
+ },
151
+ wantErr: true,
152
+ wantErrIs: context.DeadlineExceeded,
153
+ },
154
+ }
155
+
156
+ for name, tc := range tests {
157
+ t.Run(name, func(t *testing.T) {
158
+ getToken, assertPostRun := tc.buildTokenFn(t)
159
+
160
+ cred := credentialWithTimeout{
161
+ cred: fakeTokenCredential{getToken: getToken},
162
+ timeout: tc.timeout,
163
+ }
164
+
165
+ type result struct {
166
+ token azcore.AccessToken
167
+ err error
168
+ }
169
+
170
+ results := make(chan result, 1)
171
+ go func() {
172
+ token, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: []string{azureKeyVaultScope}})
173
+ results <- result{token: token, err: err}
174
+ }()
175
+
176
+ var res result
177
+ select {
178
+ case res = <-results:
179
+ case <-time.After(2 * time.Second):
180
+ t.Fatal("GetToken did not return before the outer test timeout")
181
+ }
182
+
183
+ if tc.wantErr {
184
+ require.Error(t, res.err)
185
+ assert.ErrorIs(t, res.err, tc.wantErrIs)
186
+ if assertPostRun != nil {
187
+ assertPostRun(t)
188
+ }
189
+ return
190
+ }
191
+
192
+ require.NoError(t, res.err)
193
+ assert.Equal(t, tc.wantToken, res.token.Token)
194
+ })
195
+ }
196
+}
197
+
198
+func TestDefaultCredentialTransportRouting(t *testing.T) {
199
+ tests := map[string]struct {
200
+ url string
201
+ wantDefaultCalls int
202
+ wantNoProxyCalls int
203
+ }{
204
+ "aad host uses default transport": {
205
+ url: "https://login.microsoftonline.com/tenant/oauth2/v2.0/token",
206
+ wantDefaultCalls: 1,
207
+ },
208
+ "imds uses no proxy transport": {
209
+ url: "http://169.254.169.254/metadata/identity/oauth2/token",
210
+ wantNoProxyCalls: 1,
211
+ },
212
+ "localhost managed identity endpoint uses no proxy transport": {
213
+ url: "http://localhost/msi/token",
214
+ wantNoProxyCalls: 1,
215
+ },
216
+ }
217
+
218
+ for name, tc := range tests {
219
+ t.Run(name, func(t *testing.T) {
220
+ var defaultCalls int
221
+ var noProxyCalls int
222
+
223
+ s := &store{
224
+ Config: Config{Mode: cloudauth.AzureADAuthModeDefault},
225
+ provider: &provider{
226
+ apiClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
227
+ defaultCalls++
228
+ return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Header: make(http.Header)}, nil
229
+ })},
230
+ imdsClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
231
+ noProxyCalls++
232
+ return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Header: make(http.Header)}, nil
233
+ })},
234
+ },
235
+ }
236
+
237
+ transport, ok := s.credentialOptions().ClientOptions.Transport.(routingTransportAdapter)
238
+ require.True(t, ok)
239
+
240
+ req, err := http.NewRequest(http.MethodGet, tc.url, nil)
241
+ require.NoError(t, err)
242
+
243
+ _, err = transport.Do(req)
244
+ require.NoError(t, err)
245
+
246
+ assert.Equal(t, tc.wantDefaultCalls, defaultCalls)
247
+ assert.Equal(t, tc.wantNoProxyCalls, noProxyCalls)
248
+ })
249
+ }
250
+}
src/go/plugin/agent/secrets/secretstore/backends/azure/provider.go
+6
-24
@@ -11,6 +11,7 @@ import (
11
12
"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
17
var (
@@ -19,26 +20,11 @@ var (
20
reAzureSafeName = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
21
)
22
22
-type Config struct {
23
- Mode string `json:"mode" yaml:"mode"`
24
- ModeClient *ModeClientConfig `json:"mode_client,omitempty" yaml:"mode_client,omitempty"`
25
- ModeManagedIdentity *ModeManagedIdentityConfig `json:"mode_managed_identity,omitempty" yaml:"mode_managed_identity,omitempty"`
26
-}
27
-
28
-type ModeClientConfig struct {
29
- TenantID string `json:"tenant_id" yaml:"tenant_id"`
30
- ClientID string `json:"client_id" yaml:"client_id"`
31
- ClientSecret string `json:"client_secret" yaml:"client_secret"`
32
-}
33
-
34
-type ModeManagedIdentityConfig struct {
35
- ClientID string `json:"client_id,omitempty" yaml:"client_id,omitempty"`
36
-}
23
+type Config = cloudauth.AzureADAuthConfig
24
25
type provider struct {
39
- apiClient *http.Client
40
- imdsClient *http.Client
41
- loginEndpointURL string
26
+ apiClient *http.Client
27
+ imdsClient *http.Client
28
}
29
30
type store struct {
@@ -48,12 +34,8 @@ type store struct {
34
}
35
36
type publishedStore struct {
51
- provider *provider
52
- mode string
53
- clientTenantID string
54
- clientID string
55
- clientSecret string
56
- managedIdentityClientID string
37
+ provider *provider
38
+ tokenProvider *cloudauth.TokenProvider
39
}
40
41
func New() secretstore.Creator {
src/go/plugin/agent/secrets/secretstore/backends/azure/resolve.go
+5
-92
@@ -8,7 +8,6 @@ import (
8
"fmt"
9
"io"
10
"net/http"
11
- "net/url"
11
"strings"
12
13
"github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
@@ -74,99 +73,13 @@ func splitOperand(operand string) (string, string, bool) {
73
}
74
75
func (s *publishedStore) accessToken(ctx context.Context) (string, error) {
77
- switch s.mode {
78
- case "client":
79
- if s.clientTenantID == "" {
80
- return "", fmt.Errorf("mode_client.tenant_id is required")
81
- }
82
- if s.clientID == "" {
83
- return "", fmt.Errorf("mode_client.client_id is required")
84
- }
85
- if s.clientSecret == "" {
86
- return "", fmt.Errorf("mode_client.client_secret is required")
87
- }
88
- return s.clientCredentialsToken(ctx, s.clientTenantID, s.clientID, s.clientSecret)
89
- case "managed_identity":
90
- var clientID string
91
- if s.managedIdentityClientID != "" {
92
- clientID = s.managedIdentityClientID
93
- }
94
- return s.managedIdentityToken(ctx, clientID)
95
- default:
96
- return "", fmt.Errorf("mode '%s' is invalid for azure-kv", s.mode)
76
+ if s.tokenProvider == nil {
77
+ return "", fmt.Errorf("azure token provider is not initialized")
78
}
98
-}
99
-
100
-func (s *publishedStore) clientCredentialsToken(ctx context.Context, tenantID, clientID, clientSecret string) (string, error) {
101
- tokenURL := s.provider.loginEndpointURL
102
- if tokenURL == "" {
103
- tokenURL = fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID)
104
- }
105
- form := url.Values{
106
- "client_id": {clientID},
107
- "client_secret": {clientSecret},
108
- "scope": {"https://vault.azure.net/.default"},
109
- "grant_type": {"client_credentials"},
110
- }
111
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
112
- if err != nil {
113
- return "", fmt.Errorf("creating client credentials token request: %w", err)
114
- }
115
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
116
- resp, err := s.provider.apiClient.Do(req)
117
- if err != nil {
118
- return "", fmt.Errorf("client credentials token request failed: %w", err)
119
- }
120
- defer resp.Body.Close()
121
- body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
122
- if err != nil {
123
- return "", fmt.Errorf("reading client credentials token response: %w", err)
124
- }
125
- if resp.StatusCode != http.StatusOK {
126
- return "", fmt.Errorf("client credentials token request returned HTTP %d: %s", resp.StatusCode, httpx.TruncateBody(body))
127
- }
128
- var result struct {
129
- AccessToken string `json:"access_token"`
130
- }
131
- if err := json.Unmarshal(body, &result); err != nil {
132
- return "", fmt.Errorf("parsing client credentials token response: %w", err)
133
- }
134
- if result.AccessToken == "" {
135
- return "", fmt.Errorf("client credentials token response missing access_token")
136
- }
137
- return result.AccessToken, nil
138
-}
79
140
-func (s *publishedStore) managedIdentityToken(ctx context.Context, clientID string) (string, error) {
141
- reqURL := "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net"
142
- if clientID != "" {
143
- reqURL += "&client_id=" + url.QueryEscape(clientID)
144
- }
145
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
80
+ token, _, err := s.tokenProvider.Token(ctx)
81
if err != nil {
147
- return "", fmt.Errorf("creating managed identity token request: %w", err)
148
- }
149
- req.Header.Set("Metadata", "true")
150
- resp, err := s.provider.imdsClient.Do(req)
151
- if err != nil {
152
- return "", fmt.Errorf("managed identity token request failed: %w", err)
153
- }
154
- defer resp.Body.Close()
155
- body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
156
- if err != nil {
157
- return "", fmt.Errorf("reading managed identity token response: %w", err)
158
- }
159
- if resp.StatusCode != http.StatusOK {
160
- return "", fmt.Errorf("managed identity token request returned HTTP %d: %s", resp.StatusCode, httpx.TruncateBody(body))
161
- }
162
- var result struct {
163
- AccessToken string `json:"access_token"`
164
- }
165
- if err := json.Unmarshal(body, &result); err != nil {
166
- return "", fmt.Errorf("parsing managed identity token response: %w", err)
167
- }
168
- if result.AccessToken == "" {
169
- return "", fmt.Errorf("managed identity token response missing access_token")
82
+ return "", fmt.Errorf("acquiring Azure Key Vault access token: %w", err)
83
}
171
- return result.AccessToken, nil
84
+ return token, nil
85
}
src/go/plugin/agent/secrets/secretstore/backends/azure/resolve_test.go
new
+114
@@ -0,0 +1,114 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package azure
4
+
5
+import (
6
+ "context"
7
+ "errors"
8
+ "io"
9
+ "net/http"
10
+ "strings"
11
+ "testing"
12
+ "time"
13
+
14
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
15
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
16
+ "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
18
+ "github.com/stretchr/testify/assert"
19
+ "github.com/stretchr/testify/require"
20
+)
21
+
22
+type fakeTokenCredential struct {
23
+ getToken func(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error)
24
+}
25
+
26
+func (f fakeTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
27
+ return f.getToken(ctx, opts)
28
+}
29
+
30
+type roundTripFunc func(*http.Request) (*http.Response, error)
31
+
32
+func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
33
+ return f(req)
34
+}
35
+
36
+func TestPublishedStoreResolve(t *testing.T) {
37
+ errTokenFailure := errors.New("token failure")
38
+
39
+ tests := map[string]struct {
40
+ operand string
41
+ transport roundTripFunc
42
+ getToken func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error)
43
+ wantValue string
44
+ wantErrContains string
45
+ wantErrIs error
46
+ }{
47
+ "key vault secret": {
48
+ operand: "my-vault/my-secret",
49
+ transport: func(req *http.Request) (*http.Response, error) {
50
+ assert.Equal(t, "https", req.URL.Scheme)
51
+ assert.Equal(t, "my-vault.vault.azure.net", req.URL.Host)
52
+ assert.Equal(t, "/secrets/my-secret", req.URL.Path)
53
+ assert.Equal(t, "Bearer test-token", req.Header.Get("Authorization"))
54
+
55
+ return &http.Response{
56
+ StatusCode: http.StatusOK,
57
+ Body: io.NopCloser(strings.NewReader(`{"value":"secret-value"}`)),
58
+ Header: make(http.Header),
59
+ }, nil
60
+ },
61
+ getToken: func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) {
62
+ return azcore.AccessToken{
63
+ Token: "test-token",
64
+ ExpiresOn: time.Now().Add(30 * time.Minute),
65
+ }, nil
66
+ },
67
+ wantValue: "secret-value",
68
+ },
69
+ "token acquisition failure": {
70
+ operand: "my-vault/my-secret",
71
+ getToken: func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) {
72
+ return azcore.AccessToken{}, errTokenFailure
73
+ },
74
+ wantErrContains: "acquiring Azure Key Vault access token",
75
+ wantErrIs: errTokenFailure,
76
+ },
77
+ }
78
+
79
+ for name, tc := range tests {
80
+ t.Run(name, func(t *testing.T) {
81
+ tokenProvider, err := cloudauth.NewTokenProvider(
82
+ fakeTokenCredential{
83
+ getToken: tc.getToken,
84
+ },
85
+ []string{azureKeyVaultScope},
86
+ time.Minute,
87
+ )
88
+ require.NoError(t, err)
89
+
90
+ s := &publishedStore{
91
+ provider: &provider{
92
+ apiClient: &http.Client{Transport: tc.transport},
93
+ },
94
+ tokenProvider: tokenProvider,
95
+ }
96
+
97
+ value, err := s.Resolve(context.Background(), secretstore.ResolveRequest{
98
+ StoreKey: "azure-kv:azure_prod",
99
+ Operand: tc.operand,
100
+ Original: "${store:azure-kv:azure_prod:my-vault/my-secret}",
101
+ })
102
+ if tc.wantErrContains != "" {
103
+ require.Error(t, err)
104
+ assert.ErrorContains(t, err, tc.wantErrContains)
105
+ assert.ErrorIs(t, err, tc.wantErrIs)
106
+ assert.Empty(t, value)
107
+ return
108
+ }
109
+
110
+ require.NoError(t, err)
111
+ assert.Equal(t, tc.wantValue, value)
112
+ })
113
+ }
114
+}
src/go/plugin/agent/secrets/secretstore/provider_parity_test.go
+16
-5
@@ -49,23 +49,34 @@ func TestProviderSchemaAndValidationParity(t *testing.T) {
49
},
50
invalid: map[string]any{
51
"name": "azure_prod",
52
- "mode": "client",
53
- "mode_client": map[string]any{
52
+ "mode": "service_principal",
53
+ "mode_service_principal": map[string]any{
54
"client_id": "client-id",
55
},
56
},
57
- wantErrContains: "mode_client.tenant_id is required",
57
+ wantErrContains: "mode_service_principal.tenant_id is required",
58
assertSchemaShape: func(t *testing.T, schema map[string]any) {
59
jsonSchema := schema["jsonSchema"].(map[string]any)
60
uiSchema := schema["uiSchema"].(map[string]any)
61
assert.Contains(t, jsonSchema["required"], "mode")
62
deps := jsonSchema["dependencies"].(map[string]any)
63
assert.Contains(t, deps, "mode")
64
- modeClient := uiSchema["mode_client"].(map[string]any)
65
- clientSecret := modeClient["client_secret"].(map[string]any)
64
+ modeServicePrincipal := uiSchema["mode_service_principal"].(map[string]any)
65
+ clientSecret := modeServicePrincipal["client_secret"].(map[string]any)
66
assert.Equal(t, "password", clientSecret["ui:widget"])
67
},
68
},
69
+ "azure default requires mode": {
70
+ kind: secretstore.KindAzureKV,
71
+ valid: map[string]any{
72
+ "name": "azure_default",
73
+ "mode": "default",
74
+ },
75
+ invalid: map[string]any{
76
+ "name": "azure_default",
77
+ },
78
+ wantErrContains: "mode is required",
79
+ },
80
"gcp": {
81
kind: secretstore.KindGCPSM,
82
valid: map[string]any{
src/go/plugin/go.d/collector/mssql/config_schema.json
+83
-25
@@ -63,29 +63,81 @@
63
"default"
64
],
65
"default": "default"
66
- },
67
- "tenant_id": {
68
- "title": "Tenant ID",
69
- "description": "Azure tenant ID. Required for service_principal mode.",
70
- "type": "string"
71
- },
72
- "client_id": {
73
- "title": "Client ID",
74
- "description": "Service principal client ID for service_principal mode.",
75
- "type": "string"
76
- },
77
- "client_secret": {
78
- "title": "Client Secret",
79
- "description": "Service principal client secret for service_principal mode.",
80
- "type": "string",
81
- "sensitive": true
82
- },
83
- "managed_identity_client_id": {
84
- "title": "Managed Identity Client ID",
85
- "description": "Optional client ID of a user-assigned managed identity.",
86
- "type": "string"
66
}
88
- }
67
+ },
68
+ "dependencies": {
69
+ "mode": {
70
+ "oneOf": [
71
+ {
72
+ "properties": {
73
+ "mode": {
74
+ "const": "service_principal"
75
+ },
76
+ "mode_service_principal": {
77
+ "title": "Service Principal",
78
+ "description": "Service principal settings used when mode is `service_principal`.",
79
+ "type": "object",
80
+ "properties": {
81
+ "tenant_id": {
82
+ "title": "Tenant ID",
83
+ "description": "Azure tenant ID.",
84
+ "type": "string"
85
+ },
86
+ "client_id": {
87
+ "title": "Client ID",
88
+ "description": "Service principal client ID.",
89
+ "type": "string"
90
+ },
91
+ "client_secret": {
92
+ "title": "Client Secret",
93
+ "description": "Service principal client secret.",
94
+ "type": "string",
95
+ "sensitive": true
96
+ }
97
+ },
98
+ "required": [
99
+ "tenant_id",
100
+ "client_id",
101
+ "client_secret"
102
+ ]
103
+ }
104
+ },
105
+ "required": [
106
+ "mode_service_principal"
107
+ ]
108
+ },
109
+ {
110
+ "properties": {
111
+ "mode": {
112
+ "const": "managed_identity"
113
+ },
114
+ "mode_managed_identity": {
115
+ "title": "Managed Identity",
116
+ "description": "Managed identity settings used when mode is `managed_identity`.",
117
+ "type": "object",
118
+ "properties": {
119
+ "client_id": {
120
+ "title": "Client ID",
121
+ "description": "Optional client ID of a user-assigned managed identity.",
122
+ "type": "string"
123
+ }
124
+ }
125
+ }
126
+ }
127
+ },
128
+ {
129
+ "properties": {
130
+ "mode": {
131
+ "const": "default"
132
+ }
133
+ }
134
+ }
135
+ ]
136
+ }
137
+ },
138
+ "required": [
139
+ "mode"
140
+ ]
141
}
142
},
143
"required": [
@@ -237,10 +289,16 @@
289
},
290
"azure_ad": {
291
"mode": {
240
- "ui:help": "Use `service_principal`, `managed_identity`, or `default` credential chain."
292
+ "ui:widget": "radio",
293
+ "ui:options": {
294
+ "inline": true
295
+ },
296
+ "ui:help": "Choose how Netdata gets Azure credentials.\n\n- `service_principal`: Use an Azure app / service principal. Requires `tenant_id`, `client_id`, and `client_secret` in `mode_service_principal`.\n- `managed_identity`: Use the managed identity attached to the Azure resource running Netdata. Set `mode_managed_identity.client_id` only for a user-assigned identity.\n- `default`: Use the Azure SDK `DefaultAzureCredential` chain. This automatically tries available Azure credential sources, such as environment-based credentials, managed identity, and local developer credentials.\n\nUse `service_principal` for explicit app credentials. Use `managed_identity` when Netdata runs on an Azure resource with an attached identity. Use `default` when you want Azure SDK auto-discovery or local development convenience."
297
},
242
- "client_secret": {
243
- "ui:widget": "password"
298
+ "mode_service_principal": {
299
+ "client_secret": {
300
+ "ui:widget": "password"
301
+ }
302
}
303
}
304
},
src/go/plugin/go.d/collector/mssql/integrations/microsoft_sql_server.md
+9
-11
@@ -717,11 +717,11 @@ The following options can be defined globally: update_every, autodetection_retry
717
| | autodetection_retry | Autodetection retry interval (seconds). Set 0 to disable. | 0 | no |
718
| **Target** | dsn | SQL Server DSN (Data Source Name). See [DSN syntax](https://github.com/microsoft/go-mssqldb#connection-parameters-and-dsn). When `cloud_auth.provider` is `azure_ad`, use URL format with `sqlserver://` scheme. | sqlserver://localhost:1433 | yes |
719
| **Cloud Auth** | cloud_auth.provider | Cloud auth provider (`none` or `azure_ad`). | none | no |
720
-| **Cloud Auth/Azure** | cloud_auth.azure_ad.mode | Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). | default | no |
721
-| | cloud_auth.azure_ad.tenant_id | Azure tenant ID. Required for `service_principal` mode. | | no |
722
-| | cloud_auth.azure_ad.client_id | Azure client ID. Required for `service_principal`; optional for user-assigned managed identity. | | no |
723
-| | cloud_auth.azure_ad.client_secret | Azure client secret for `service_principal` mode. | | no |
724
-| | cloud_auth.azure_ad.managed_identity_client_id | Optional client ID of a user-assigned managed identity (`managed_identity` mode). | | no |
720
+| **Cloud Auth/Azure** | cloud_auth.azure_ad.mode | Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). Required when `cloud_auth.provider` is `azure_ad`. | | yes |
721
+| | cloud_auth.azure_ad.mode_service_principal.tenant_id | Azure tenant ID. Required for `service_principal` mode. | | no |
722
+| | cloud_auth.azure_ad.mode_service_principal.client_id | Azure client ID. Required for `service_principal` mode. | | no |
723
+| | cloud_auth.azure_ad.mode_service_principal.client_secret | Azure client secret for `service_principal` mode. | | no |
724
+| | cloud_auth.azure_ad.mode_managed_identity.client_id | Optional client ID of a user-assigned managed identity (`managed_identity` mode). | | no |
725
| **Target** | timeout | Query timeout (seconds). | 5 | no |
726
| **Functions** | functions.top_queries.disabled | Disable the [top-queries](#top-queries) function. | no | no |
727
| | functions.top_queries.timeout | Query timeout for top-queries function (seconds). Uses collector timeout if not set. | | no |
@@ -847,9 +847,10 @@ jobs:
847
provider: azure_ad
848
azure_ad:
849
mode: service_principal
850
- tenant_id: "00000000-0000-0000-0000-000000000000"
851
- client_id: "11111111-1111-1111-1111-111111111111"
852
- client_secret: "super-secret-value"
850
+ mode_service_principal:
851
+ tenant_id: "00000000-0000-0000-0000-000000000000"
852
+ client_id: "11111111-1111-1111-1111-111111111111"
853
+ client_secret: "super-secret-value"
854
855
```
856
</details>
@@ -1003,6 +1004,3 @@ Ensure SQL Server is configured for mixed mode authentication if using SQL login
1004
1005
The monitoring user needs VIEW SERVER STATE permission.
1006
Grant it with: `GRANT VIEW SERVER STATE TO netdata_user;`
1006
-
1007
-
1008
-
src/go/plugin/go.d/collector/mssql/metadata.yaml
+11
-11
@@ -155,26 +155,25 @@ modules:
155
required: false
156
group: Cloud Auth
157
- name: cloud_auth.azure_ad.mode
158
- description: Azure AD credential mode (`service_principal`, `managed_identity`, or `default`).
159
- default_value: default
160
- required: false
158
+ description: Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). Required when `cloud_auth.provider` is `azure_ad`.
159
+ required: true
160
group: Cloud Auth/Azure
162
- - name: cloud_auth.azure_ad.tenant_id
161
+ - name: cloud_auth.azure_ad.mode_service_principal.tenant_id
162
description: Azure tenant ID. Required for `service_principal` mode.
163
default_value: ""
164
required: false
165
group: Cloud Auth/Azure
167
- - name: cloud_auth.azure_ad.client_id
168
- description: Azure client ID. Required for `service_principal`; optional for user-assigned managed identity.
166
+ - name: cloud_auth.azure_ad.mode_service_principal.client_id
167
+ description: Azure client ID. Required for `service_principal` mode.
168
default_value: ""
169
required: false
170
group: Cloud Auth/Azure
172
- - name: cloud_auth.azure_ad.client_secret
171
+ - name: cloud_auth.azure_ad.mode_service_principal.client_secret
172
description: Azure client secret for `service_principal` mode.
173
default_value: ""
174
required: false
175
group: Cloud Auth/Azure
177
- - name: cloud_auth.azure_ad.managed_identity_client_id
176
+ - name: cloud_auth.azure_ad.mode_managed_identity.client_id
177
description: Optional client ID of a user-assigned managed identity (`managed_identity` mode).
178
default_value: ""
179
required: false
@@ -289,9 +288,10 @@ modules:
288
provider: azure_ad
289
azure_ad:
290
mode: service_principal
292
- tenant_id: "00000000-0000-0000-0000-000000000000"
293
- client_id: "11111111-1111-1111-1111-111111111111"
294
- client_secret: "super-secret-value"
291
+ mode_service_principal:
292
+ tenant_id: "00000000-0000-0000-0000-000000000000"
293
+ client_id: "11111111-1111-1111-1111-111111111111"
294
+ client_secret: "super-secret-value"
295
- name: Azure SQL with managed identity
296
description: Use managed identity authentication (system-assigned by default).
297
config: |
src/go/plugin/go.d/collector/mssql/mssql_test.go
+5
-3
@@ -32,9 +32,11 @@ func TestCollector_Init_InvalidAzureADConfig(t *testing.T) {
32
c := New()
33
c.CloudAuth.Provider = cloudauth.ProviderAzureAD
34
c.CloudAuth.AzureAD = &cloudauth.AzureADAuthConfig{
35
- Mode: cloudauth.AzureADAuthModeServicePrincipal,
36
- ClientID: "client-id",
37
- TenantID: "tenant-id",
35
+ Mode: cloudauth.AzureADAuthModeServicePrincipal,
36
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
37
+ ClientID: "client-id",
38
+ TenantID: "tenant-id",
39
+ },
40
}
41
// Missing client_secret.
42
src/go/plugin/go.d/collector/postgres/collector_test.go
+12
-8
@@ -121,10 +121,12 @@ func TestCollector_Init(t *testing.T) {
121
CloudAuth: cloudauth.Config{
122
Provider: cloudauth.ProviderAzureAD,
123
AzureAD: &cloudauth.AzureADAuthConfig{
124
- Mode: "service_principal",
125
- TenantID: "tenant-id",
126
- ClientID: "client-id",
127
- // Missing client_secret.
124
+ Mode: "service_principal",
125
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
126
+ TenantID: "tenant-id",
127
+ ClientID: "client-id",
128
+ // Missing client_secret.
129
+ },
130
},
131
},
132
},
@@ -150,10 +152,12 @@ func TestCollector_Init_AzureADInitializesTokenProvider(t *testing.T) {
152
c.CloudAuth = cloudauth.Config{
153
Provider: cloudauth.ProviderAzureAD,
154
AzureAD: &cloudauth.AzureADAuthConfig{
153
- Mode: cloudauth.AzureADAuthModeServicePrincipal,
154
- TenantID: "tenant-id",
155
- ClientID: "client-id",
156
- ClientSecret: "client-secret",
155
+ Mode: cloudauth.AzureADAuthModeServicePrincipal,
156
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
157
+ TenantID: "tenant-id",
158
+ ClientID: "client-id",
159
+ ClientSecret: "client-secret",
160
+ },
161
},
162
}
163
src/go/plugin/go.d/collector/postgres/config_schema.json
+83
-25
@@ -70,29 +70,81 @@
70
"default"
71
],
72
"default": "default"
73
- },
74
- "tenant_id": {
75
- "title": "Tenant ID",
76
- "description": "Azure tenant ID. Required for service_principal mode.",
77
- "type": "string"
78
- },
79
- "client_id": {
80
- "title": "Client ID",
81
- "description": "Service principal client ID for service_principal mode.",
82
- "type": "string"
83
- },
84
- "client_secret": {
85
- "title": "Client Secret",
86
- "description": "Service principal client secret for service_principal mode.",
87
- "type": "string",
88
- "sensitive": true
89
- },
90
- "managed_identity_client_id": {
91
- "title": "Managed Identity Client ID",
92
- "description": "Optional client ID of a user-assigned managed identity.",
93
- "type": "string"
73
}
95
- }
74
+ },
75
+ "dependencies": {
76
+ "mode": {
77
+ "oneOf": [
78
+ {
79
+ "properties": {
80
+ "mode": {
81
+ "const": "service_principal"
82
+ },
83
+ "mode_service_principal": {
84
+ "title": "Service Principal",
85
+ "description": "Service principal settings used when mode is `service_principal`.",
86
+ "type": "object",
87
+ "properties": {
88
+ "tenant_id": {
89
+ "title": "Tenant ID",
90
+ "description": "Azure tenant ID.",
91
+ "type": "string"
92
+ },
93
+ "client_id": {
94
+ "title": "Client ID",
95
+ "description": "Service principal client ID.",
96
+ "type": "string"
97
+ },
98
+ "client_secret": {
99
+ "title": "Client Secret",
100
+ "description": "Service principal client secret.",
101
+ "type": "string",
102
+ "sensitive": true
103
+ }
104
+ },
105
+ "required": [
106
+ "tenant_id",
107
+ "client_id",
108
+ "client_secret"
109
+ ]
110
+ }
111
+ },
112
+ "required": [
113
+ "mode_service_principal"
114
+ ]
115
+ },
116
+ {
117
+ "properties": {
118
+ "mode": {
119
+ "const": "managed_identity"
120
+ },
121
+ "mode_managed_identity": {
122
+ "title": "Managed Identity",
123
+ "description": "Managed identity settings used when mode is `managed_identity`.",
124
+ "type": "object",
125
+ "properties": {
126
+ "client_id": {
127
+ "title": "Client ID",
128
+ "description": "Optional client ID of a user-assigned managed identity.",
129
+ "type": "string"
130
+ }
131
+ }
132
+ }
133
+ }
134
+ },
135
+ {
136
+ "properties": {
137
+ "mode": {
138
+ "const": "default"
139
+ }
140
+ }
141
+ }
142
+ ]
143
+ }
144
+ },
145
+ "required": [
146
+ "mode"
147
+ ]
148
}
149
},
150
"required": [
@@ -281,10 +333,16 @@
333
},
334
"azure_ad": {
335
"mode": {
284
- "ui:help": "Use `service_principal`, `managed_identity`, or `default` credential chain."
336
+ "ui:widget": "radio",
337
+ "ui:options": {
338
+ "inline": true
339
+ },
340
+ "ui:help": "Choose how Netdata gets Azure credentials.\n\n- `service_principal`: Use an Azure app / service principal. Requires `tenant_id`, `client_id`, and `client_secret` in `mode_service_principal`.\n- `managed_identity`: Use the managed identity attached to the Azure resource running Netdata. Set `mode_managed_identity.client_id` only for a user-assigned identity.\n- `default`: Use the Azure SDK `DefaultAzureCredential` chain. This automatically tries available Azure credential sources, such as environment-based credentials, managed identity, and local developer credentials.\n\nUse `service_principal` for explicit app credentials. Use `managed_identity` when Netdata runs on an Azure resource with an attached identity. Use `default` when you want Azure SDK auto-discovery or local development convenience."
341
},
286
- "client_secret": {
287
- "ui:widget": "password"
342
+ "mode_service_principal": {
343
+ "client_secret": {
344
+ "ui:widget": "password"
345
+ }
346
}
347
}
348
},
src/go/plugin/go.d/collector/postgres/integrations/postgresql.md
+9
-10
@@ -530,11 +530,11 @@ The following options can be defined globally: update_every, autodetection_retry
530
| | autodetection_retry | Autodetection retry interval (seconds). Set 0 to disable. | 0 | no |
531
| **Target** | dsn | Postgres connection string (DSN). See [DSN syntax](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). | postgres://postgres:postgres@127.0.0.1:5432/postgres | yes |
532
| **Cloud Auth** | cloud_auth.provider | Cloud auth provider (`none` or `azure_ad`). | none | no |
533
-| **Cloud Auth/Azure** | cloud_auth.azure_ad.mode | Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). | default | no |
534
-| | cloud_auth.azure_ad.tenant_id | Azure tenant ID. Required for `service_principal` mode. | | no |
535
-| | cloud_auth.azure_ad.client_id | Azure client ID. Required for `service_principal`; optional for user-assigned managed identity. | | no |
536
-| | cloud_auth.azure_ad.client_secret | Azure client secret for `service_principal` mode. | | no |
537
-| | cloud_auth.azure_ad.managed_identity_client_id | Optional client ID of a user-assigned managed identity (`managed_identity` mode). | | no |
533
+| **Cloud Auth/Azure** | cloud_auth.azure_ad.mode | Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). Required when `cloud_auth.provider` is `azure_ad`. | | yes |
534
+| | cloud_auth.azure_ad.mode_service_principal.tenant_id | Azure tenant ID. Required for `service_principal` mode. | | no |
535
+| | cloud_auth.azure_ad.mode_service_principal.client_id | Azure client ID. Required for `service_principal` mode. | | no |
536
+| | cloud_auth.azure_ad.mode_service_principal.client_secret | Azure client secret for `service_principal` mode. | | no |
537
+| | cloud_auth.azure_ad.mode_managed_identity.client_id | Optional client ID of a user-assigned managed identity (`managed_identity` mode). | | no |
538
| **Target** | timeout | Query timeout (seconds). | 2 | no |
539
| **Filters** | collect_databases_matching | Database selector. Controls which databases are included. Uses [simple patterns](https://github.com/netdata/netdata/tree/master/src/go/pkg/matcher#simple-patterns-matcher). | | no |
540
| **Limits** | max_db_tables | Maximum number of tables per database to collect metrics for (0 = no limit). | 50 | no |
@@ -637,9 +637,10 @@ jobs:
637
provider: azure_ad
638
azure_ad:
639
mode: service_principal
640
- tenant_id: "00000000-0000-0000-0000-000000000000"
641
- client_id: "11111111-1111-1111-1111-111111111111"
642
- client_secret: "super-secret-value"
640
+ mode_service_principal:
641
+ tenant_id: "00000000-0000-0000-0000-000000000000"
642
+ client_id: "11111111-1111-1111-1111-111111111111"
643
+ client_secret: "super-secret-value"
644
645
```
646
</details>
@@ -750,5 +751,3 @@ If your Netdata runs in a Docker container named "netdata" (replace if different
751
```bash
752
docker logs netdata 2>&1 | grep postgres
753
```
753
-
754
-
src/go/plugin/go.d/collector/postgres/metadata.yaml
+11
-11
@@ -103,26 +103,25 @@ modules:
103
required: false
104
group: Cloud Auth
105
- name: cloud_auth.azure_ad.mode
106
- description: Azure AD credential mode (`service_principal`, `managed_identity`, or `default`).
107
- default_value: default
108
- required: false
106
+ description: Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). Required when `cloud_auth.provider` is `azure_ad`.
107
+ required: true
108
group: Cloud Auth/Azure
110
- - name: cloud_auth.azure_ad.tenant_id
109
+ - name: cloud_auth.azure_ad.mode_service_principal.tenant_id
110
description: Azure tenant ID. Required for `service_principal` mode.
111
default_value: ""
112
required: false
113
group: Cloud Auth/Azure
115
- - name: cloud_auth.azure_ad.client_id
116
- description: Azure client ID. Required for `service_principal`; optional for user-assigned managed identity.
114
+ - name: cloud_auth.azure_ad.mode_service_principal.client_id
115
+ description: Azure client ID. Required for `service_principal` mode.
116
default_value: ""
117
required: false
118
group: Cloud Auth/Azure
120
- - name: cloud_auth.azure_ad.client_secret
119
+ - name: cloud_auth.azure_ad.mode_service_principal.client_secret
120
description: Azure client secret for `service_principal` mode.
121
default_value: ""
122
required: false
123
group: Cloud Auth/Azure
125
- - name: cloud_auth.azure_ad.managed_identity_client_id
124
+ - name: cloud_auth.azure_ad.mode_managed_identity.client_id
125
description: Optional client ID of a user-assigned managed identity (`managed_identity` mode).
126
default_value: ""
127
required: false
@@ -206,9 +205,10 @@ modules:
205
provider: azure_ad
206
azure_ad:
207
mode: service_principal
209
- tenant_id: "00000000-0000-0000-0000-000000000000"
210
- client_id: "11111111-1111-1111-1111-111111111111"
211
- client_secret: "super-secret-value"
208
+ mode_service_principal:
209
+ tenant_id: "00000000-0000-0000-0000-000000000000"
210
+ client_id: "11111111-1111-1111-1111-111111111111"
211
+ client_secret: "super-secret-value"
212
- name: Azure Database for PostgreSQL with managed identity
213
description: Use managed identity authentication (system-assigned by default).
214
config: |
src/go/plugin/go.d/collector/sql/collector_test.go
+5
-3
@@ -164,9 +164,11 @@ func TestCollector_Init_ConfigValidation(t *testing.T) {
164
c.Functions = []ConfigFunction{{ID: "test", Query: "SELECT 1"}}
165
c.CloudAuth.Provider = "azure_ad"
166
c.CloudAuth.AzureAD = &cloudauth.AzureADAuthConfig{
167
- Mode: "service_principal",
168
- TenantID: "tenant",
169
- ClientID: "client",
167
+ Mode: "service_principal",
168
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
169
+ TenantID: "tenant",
170
+ ClientID: "client",
171
+ },
172
}
173
},
174
wantFail: true,
src/go/plugin/go.d/collector/sql/config_schema.json
+83
-25
@@ -89,29 +89,81 @@
89
"default"
90
],
91
"default": "default"
92
- },
93
- "tenant_id": {
94
- "title": "Tenant ID",
95
- "description": "Azure tenant ID. Required for service_principal mode.",
96
- "type": "string"
97
- },
98
- "client_id": {
99
- "title": "Client ID",
100
- "description": "Service principal client ID for service_principal mode.",
101
- "type": "string"
102
- },
103
- "client_secret": {
104
- "title": "Client Secret",
105
- "description": "Service principal client secret for service_principal mode.",
106
- "type": "string",
107
- "sensitive": true
108
- },
109
- "managed_identity_client_id": {
110
- "title": "Managed Identity Client ID",
111
- "description": "Optional client ID of a user-assigned managed identity.",
112
- "type": "string"
92
}
114
- }
93
+ },
94
+ "dependencies": {
95
+ "mode": {
96
+ "oneOf": [
97
+ {
98
+ "properties": {
99
+ "mode": {
100
+ "const": "service_principal"
101
+ },
102
+ "mode_service_principal": {
103
+ "title": "Service Principal",
104
+ "description": "Service principal settings used when mode is `service_principal`.",
105
+ "type": "object",
106
+ "properties": {
107
+ "tenant_id": {
108
+ "title": "Tenant ID",
109
+ "description": "Azure tenant ID.",
110
+ "type": "string"
111
+ },
112
+ "client_id": {
113
+ "title": "Client ID",
114
+ "description": "Service principal client ID.",
115
+ "type": "string"
116
+ },
117
+ "client_secret": {
118
+ "title": "Client Secret",
119
+ "description": "Service principal client secret.",
120
+ "type": "string",
121
+ "sensitive": true
122
+ }
123
+ },
124
+ "required": [
125
+ "tenant_id",
126
+ "client_id",
127
+ "client_secret"
128
+ ]
129
+ }
130
+ },
131
+ "required": [
132
+ "mode_service_principal"
133
+ ]
134
+ },
135
+ {
136
+ "properties": {
137
+ "mode": {
138
+ "const": "managed_identity"
139
+ },
140
+ "mode_managed_identity": {
141
+ "title": "Managed Identity",
142
+ "description": "Managed identity settings used when mode is `managed_identity`.",
143
+ "type": "object",
144
+ "properties": {
145
+ "client_id": {
146
+ "title": "Client ID",
147
+ "description": "Optional client ID of a user-assigned managed identity.",
148
+ "type": "string"
149
+ }
150
+ }
151
+ }
152
+ }
153
+ },
154
+ {
155
+ "properties": {
156
+ "mode": {
157
+ "const": "default"
158
+ }
159
+ }
160
+ }
161
+ ]
162
+ }
163
+ },
164
+ "required": [
165
+ "mode"
166
+ ]
167
}
168
},
169
"required": [
@@ -554,10 +606,16 @@
606
},
607
"azure_ad": {
608
"mode": {
557
- "ui:help": "Use `service_principal`, `managed_identity`, or `default` credential chain."
609
+ "ui:widget": "radio",
610
+ "ui:options": {
611
+ "inline": true
612
+ },
613
+ "ui:help": "Choose how Netdata gets Azure credentials.\n\n- `service_principal`: Use an Azure app / service principal. Requires `tenant_id`, `client_id`, and `client_secret` in `mode_service_principal`.\n- `managed_identity`: Use the managed identity attached to the Azure resource running Netdata. Set `mode_managed_identity.client_id` only for a user-assigned identity.\n- `default`: Use the Azure SDK `DefaultAzureCredential` chain. This automatically tries available Azure credential sources, such as environment-based credentials, managed identity, and local developer credentials.\n\nUse `service_principal` for explicit app credentials. Use `managed_identity` when Netdata runs on an Azure resource with an attached identity. Use `default` when you want Azure SDK auto-discovery or local development convenience."
614
},
559
- "client_secret": {
560
- "ui:widget": "password"
615
+ "mode_service_principal": {
616
+ "client_secret": {
617
+ "ui:widget": "password"
618
+ }
619
}
620
}
621
},
src/go/plugin/go.d/collector/sql/integrations/sql_databases_generic.md
+15
-14
@@ -201,10 +201,12 @@ cloud_auth: # OPTIONAL. Cloud auth for pgx/sqls
201
provider: <none|azure_ad> # OPTIONAL. Default: none.
202
azure_ad:
203
mode: <service_principal|managed_identity|default>
204
- tenant_id: "<tenant-id>" # REQUIRED for service_principal
205
- client_id: "<client-id>" # REQUIRED for service_principal
206
- client_secret: "<client-secret>" # REQUIRED for service_principal
207
- managed_identity_client_id: "<client-id>" # Optional for user-assigned MI
204
+ mode_service_principal: # REQUIRED for service_principal
205
+ tenant_id: "<tenant-id>"
206
+ client_id: "<client-id>"
207
+ client_secret: "<client-secret>"
208
+ mode_managed_identity: # OPTIONAL for managed_identity
209
+ client_id: "<client-id>" # Optional for user-assigned MI
210
211
# Optional static labels applied to all charts
212
static_labels:
@@ -312,11 +314,11 @@ functions:
314
| **Target** | driver | SQL driver to use. Supported values: `mysql`, `pgx`, `oracle`, `sqlserver`, `azuresql`. | mysql | yes |
315
| | dsn | Database connection string (DSN). The format depends on the selected driver ( [MySQL](https://github.com/go-sql-driver/mysql#dsn-data-source-name), [PostgreSQL](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS), [MS SQL Server](https://github.com/microsoft/go-mssqldb#connection-parameters-and-dsn)). | | yes |
316
| **Cloud Auth** | cloud_auth.provider | Cloud auth provider (`none` or `azure_ad`). Supported for `pgx`, `sqlserver`, and `azuresql`. | none | no |
315
-| **Cloud Auth/Azure** | cloud_auth.azure_ad.mode | Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). | default | no |
316
-| | cloud_auth.azure_ad.tenant_id | Azure tenant ID. Required for `service_principal` mode. | | no |
317
-| | cloud_auth.azure_ad.client_id | Azure client ID. Required for `service_principal`; optional for user-assigned managed identity. | | no |
318
-| | cloud_auth.azure_ad.client_secret | Azure client secret for `service_principal` mode. | | no |
319
-| | cloud_auth.azure_ad.managed_identity_client_id | Optional client ID of a user-assigned managed identity (`managed_identity` mode). | | no |
317
+| **Cloud Auth/Azure** | cloud_auth.azure_ad.mode | Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). Required when `cloud_auth.provider` is `azure_ad`. | | yes |
318
+| | cloud_auth.azure_ad.mode_service_principal.tenant_id | Azure tenant ID. Required for `service_principal` mode. | | no |
319
+| | cloud_auth.azure_ad.mode_service_principal.client_id | Azure client ID. Required for `service_principal` mode. | | no |
320
+| | cloud_auth.azure_ad.mode_service_principal.client_secret | Azure client secret for `service_principal` mode. | | no |
321
+| | cloud_auth.azure_ad.mode_managed_identity.client_id | Optional client ID of a user-assigned managed identity (`managed_identity` mode). | | no |
322
| **Connection** | timeout | Query and connection check timeout (seconds). | 5 | no |
323
| **Labels** | static_labels | A map of static labels added to every chart created by this job. Useful for tagging charts with environment, region, or role. | {} | no |
324
| **Queries & Metrics** | queries | A list of reusable queries. Metric blocks can reference these via `query_ref` to avoid repeating SQL. See [Configuration Structure](#configuration) for details. | [] | no |
@@ -392,9 +394,10 @@ jobs:
394
provider: azure_ad
395
azure_ad:
396
mode: service_principal
395
- tenant_id: "00000000-0000-0000-0000-000000000000"
396
- client_id: "11111111-1111-1111-1111-111111111111"
397
- client_secret: "super-secret-value"
397
+ mode_service_principal:
398
+ tenant_id: "00000000-0000-0000-0000-000000000000"
399
+ client_id: "11111111-1111-1111-1111-111111111111"
400
+ client_secret: "super-secret-value"
401
metrics:
402
- id: user_connections
403
mode: columns
@@ -894,5 +897,3 @@ If your Netdata runs in a Docker container named "netdata" (replace if different
897
```bash
898
docker logs netdata 2>&1 | grep sql
899
```
897
-
898
-
src/go/plugin/go.d/collector/sql/metadata.yaml
+17
-15
@@ -144,10 +144,12 @@ modules:
144
provider: <none|azure_ad> # OPTIONAL. Default: none.
145
azure_ad:
146
mode: <service_principal|managed_identity|default>
147
- tenant_id: "<tenant-id>" # REQUIRED for service_principal
148
- client_id: "<client-id>" # REQUIRED for service_principal
149
- client_secret: "<client-secret>" # REQUIRED for service_principal
150
- managed_identity_client_id: "<client-id>" # Optional for user-assigned MI
147
+ mode_service_principal: # REQUIRED for service_principal
148
+ tenant_id: "<tenant-id>"
149
+ client_id: "<client-id>"
150
+ client_secret: "<client-secret>"
151
+ mode_managed_identity: # OPTIONAL for managed_identity
152
+ client_id: "<client-id>" # Optional for user-assigned MI
153
154
# Optional static labels applied to all charts
155
static_labels:
@@ -278,26 +280,25 @@ modules:
280
required: false
281
group: Cloud Auth
282
- name: cloud_auth.azure_ad.mode
281
- description: Azure AD credential mode (`service_principal`, `managed_identity`, or `default`).
282
- default_value: default
283
- required: false
283
+ description: Azure AD credential mode (`service_principal`, `managed_identity`, or `default`). Required when `cloud_auth.provider` is `azure_ad`.
284
+ required: true
285
group: Cloud Auth/Azure
285
- - name: cloud_auth.azure_ad.tenant_id
286
+ - name: cloud_auth.azure_ad.mode_service_principal.tenant_id
287
description: Azure tenant ID. Required for `service_principal` mode.
288
default_value: ""
289
required: false
290
group: Cloud Auth/Azure
290
- - name: cloud_auth.azure_ad.client_id
291
- description: Azure client ID. Required for `service_principal`; optional for user-assigned managed identity.
291
+ - name: cloud_auth.azure_ad.mode_service_principal.client_id
292
+ description: Azure client ID. Required for `service_principal` mode.
293
default_value: ""
294
required: false
295
group: Cloud Auth/Azure
295
- - name: cloud_auth.azure_ad.client_secret
296
+ - name: cloud_auth.azure_ad.mode_service_principal.client_secret
297
description: Azure client secret for `service_principal` mode.
298
default_value: ""
299
required: false
300
group: Cloud Auth/Azure
300
- - name: cloud_auth.azure_ad.managed_identity_client_id
301
+ - name: cloud_auth.azure_ad.mode_managed_identity.client_id
302
description: Optional client ID of a user-assigned managed identity (`managed_identity` mode).
303
default_value: ""
304
required: false
@@ -416,9 +417,10 @@ modules:
417
provider: azure_ad
418
azure_ad:
419
mode: service_principal
419
- tenant_id: "00000000-0000-0000-0000-000000000000"
420
- client_id: "11111111-1111-1111-1111-111111111111"
421
- client_secret: "super-secret-value"
420
+ mode_service_principal:
421
+ tenant_id: "00000000-0000-0000-0000-000000000000"
422
+ client_id: "11111111-1111-1111-1111-111111111111"
423
+ client_secret: "super-secret-value"
424
metrics:
425
- id: user_connections
426
mode: columns
src/go/plugin/go.d/config/go.d/ss/azure-kv.conf
+2
-2
@@ -3,8 +3,8 @@
3
4
#jobs:
5
# - name: azure_prod
6
-# mode: client
7
-# mode_client:
6
+# mode: service_principal
7
+# mode_service_principal:
8
# tenant_id: 00000000-0000-0000-0000-000000000000
9
# client_id: 00000000-0000-0000-0000-000000000000
10
# client_secret: your-client-secret
src/go/plugin/go.d/pkg/cloudauth/azuread_auth_config.go
+100
-31
@@ -15,43 +15,68 @@ const (
15
AzureADAuthModeServicePrincipal = "service_principal"
16
AzureADAuthModeManagedIdentity = "managed_identity"
17
AzureADAuthModeDefault = "default"
18
+
19
+ azureADAuthConfigPath = "cloud_auth.azure_ad"
20
)
21
22
+type AzureADModeServicePrincipalConfig struct {
23
+ TenantID string `yaml:"tenant_id,omitempty" json:"tenant_id,omitempty"`
24
+ ClientID string `yaml:"client_id,omitempty" json:"client_id,omitempty"`
25
+ ClientSecret string `yaml:"client_secret,omitempty" json:"client_secret,omitempty"`
26
+}
27
+
28
+type AzureADModeManagedIdentityConfig struct {
29
+ ClientID string `yaml:"client_id,omitempty" json:"client_id,omitempty"`
30
+}
31
+
32
type AzureADAuthConfig struct {
21
- Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
22
- TenantID string `yaml:"tenant_id,omitempty" json:"tenant_id,omitempty"`
23
- ClientID string `yaml:"client_id,omitempty" json:"client_id,omitempty"`
24
- ClientSecret string `yaml:"client_secret,omitempty" json:"client_secret,omitempty"`
25
- ManagedIdentityClientID string `yaml:"managed_identity_client_id,omitempty" json:"managed_identity_client_id,omitempty"`
33
+ Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
34
+ ModeServicePrincipal *AzureADModeServicePrincipalConfig `yaml:"mode_service_principal,omitempty" json:"mode_service_principal,omitempty"`
35
+ ModeManagedIdentity *AzureADModeManagedIdentityConfig `yaml:"mode_managed_identity,omitempty" json:"mode_managed_identity,omitempty"`
36
+}
37
+
38
+type AzureADCredentialOptions struct {
39
+ ClientOptions azcore.ClientOptions
40
}
41
42
func (c AzureADAuthConfig) NormalizedMode() string {
29
- mode := strings.TrimSpace(c.Mode)
30
- if mode == "" {
31
- return AzureADAuthModeDefault
32
- }
33
- return strings.ToLower(mode)
43
+ return strings.ToLower(strings.TrimSpace(c.Mode))
44
}
45
46
func (c AzureADAuthConfig) Validate() error {
37
- switch c.NormalizedMode() {
47
+ return c.ValidateWithPath(azureADAuthConfigPath)
48
+}
49
+
50
+func (c AzureADAuthConfig) ValidateWithPath(path string) error {
51
+ modeField := fieldPath(path, "mode")
52
+ mode := c.NormalizedMode()
53
+
54
+ if mode == "" {
55
+ return errors.New(modeField + " is required")
56
+ }
57
+
58
+ switch mode {
59
case AzureADAuthModeServicePrincipal:
60
var errs []error
40
- if strings.TrimSpace(c.TenantID) == "" {
41
- errs = append(errs, errors.New("cloud_auth.azure_ad.tenant_id is required for service_principal mode"))
61
+ if c.ModeServicePrincipal == nil {
62
+ return fmt.Errorf("%s is required when %s is %q", fieldPath(path, "mode_service_principal"), modeField, AzureADAuthModeServicePrincipal)
63
+ }
64
+
65
+ if strings.TrimSpace(c.ModeServicePrincipal.TenantID) == "" {
66
+ errs = append(errs, errors.New(fieldPath(path, "mode_service_principal.tenant_id")+" is required"))
67
}
43
- if strings.TrimSpace(c.ClientID) == "" {
44
- errs = append(errs, errors.New("cloud_auth.azure_ad.client_id is required for service_principal mode"))
68
+ if strings.TrimSpace(c.ModeServicePrincipal.ClientID) == "" {
69
+ errs = append(errs, errors.New(fieldPath(path, "mode_service_principal.client_id")+" is required"))
70
}
46
- if strings.TrimSpace(c.ClientSecret) == "" {
47
- errs = append(errs, errors.New("cloud_auth.azure_ad.client_secret is required for service_principal mode"))
71
+ if strings.TrimSpace(c.ModeServicePrincipal.ClientSecret) == "" {
72
+ errs = append(errs, errors.New(fieldPath(path, "mode_service_principal.client_secret")+" is required"))
73
}
74
return errors.Join(errs...)
75
case AzureADAuthModeManagedIdentity, AzureADAuthModeDefault:
76
return nil
77
default:
53
- return fmt.Errorf("cloud_auth.azure_ad.mode %q is invalid: expected one of %q, %q, %q",
54
- c.Mode, AzureADAuthModeServicePrincipal, AzureADAuthModeManagedIdentity, AzureADAuthModeDefault)
78
+ return fmt.Errorf("%s %q is invalid: expected one of %q, %q, %q",
79
+ modeField, c.Mode, AzureADAuthModeServicePrincipal, AzureADAuthModeManagedIdentity, AzureADAuthModeDefault)
80
}
81
}
82
@@ -60,25 +85,69 @@ func (c AzureADAuthConfig) NewCredential() (azcore.TokenCredential, error) {
85
return nil, err
86
}
87
88
+ return c.newCredential(nil)
89
+}
90
+
91
+func (c AzureADAuthConfig) NewCredentialWithOptions(opts *AzureADCredentialOptions) (azcore.TokenCredential, error) {
92
+ if err := c.Validate(); err != nil {
93
+ return nil, err
94
+ }
95
+
96
+ return c.newCredential(opts)
97
+}
98
+
99
+func (c AzureADAuthConfig) newCredential(opts *AzureADCredentialOptions) (azcore.TokenCredential, error) {
100
switch c.NormalizedMode() {
101
case AzureADAuthModeServicePrincipal:
102
+ cfg := c.servicePrincipalConfig()
103
+ credOpts := &azidentity.ClientSecretCredentialOptions{}
104
+ if opts != nil {
105
+ credOpts.ClientOptions = opts.ClientOptions
106
+ }
107
return azidentity.NewClientSecretCredential(
66
- strings.TrimSpace(c.TenantID),
67
- strings.TrimSpace(c.ClientID),
68
- strings.TrimSpace(c.ClientSecret),
69
- nil,
108
+ strings.TrimSpace(cfg.TenantID),
109
+ strings.TrimSpace(cfg.ClientID),
110
+ strings.TrimSpace(cfg.ClientSecret),
111
+ credOpts,
112
)
113
case AzureADAuthModeManagedIdentity:
72
- if strings.TrimSpace(c.ManagedIdentityClientID) != "" {
73
- opts := &azidentity.ManagedIdentityCredentialOptions{
74
- ID: azidentity.ClientID(strings.TrimSpace(c.ManagedIdentityClientID)),
75
- }
76
- return azidentity.NewManagedIdentityCredential(opts)
114
+ cfg := c.managedIdentityConfig()
115
+ credOpts := &azidentity.ManagedIdentityCredentialOptions{}
116
+ if opts != nil {
117
+ credOpts.ClientOptions = opts.ClientOptions
118
}
78
- return azidentity.NewManagedIdentityCredential(nil)
119
+ if strings.TrimSpace(cfg.ClientID) != "" {
120
+ credOpts.ID = azidentity.ClientID(strings.TrimSpace(cfg.ClientID))
121
+ }
122
+ return azidentity.NewManagedIdentityCredential(credOpts)
123
case AzureADAuthModeDefault:
80
- return azidentity.NewDefaultAzureCredential(nil)
124
+ credOpts := &azidentity.DefaultAzureCredentialOptions{}
125
+ if opts != nil {
126
+ credOpts.ClientOptions = opts.ClientOptions
127
+ }
128
+ return azidentity.NewDefaultAzureCredential(credOpts)
129
default:
82
- return nil, fmt.Errorf("cloud_auth.azure_ad.mode %q is invalid", c.Mode)
130
+ return nil, fmt.Errorf("%s %q is invalid", fieldPath(azureADAuthConfigPath, "mode"), c.Mode)
131
+ }
132
+}
133
+
134
+func (c AzureADAuthConfig) servicePrincipalConfig() AzureADModeServicePrincipalConfig {
135
+ if c.ModeServicePrincipal == nil {
136
+ return AzureADModeServicePrincipalConfig{}
137
+ }
138
+ return *c.ModeServicePrincipal
139
+}
140
+
141
+func (c AzureADAuthConfig) managedIdentityConfig() AzureADModeManagedIdentityConfig {
142
+ if c.ModeManagedIdentity == nil {
143
+ return AzureADModeManagedIdentityConfig{}
144
+ }
145
+ return *c.ModeManagedIdentity
146
+}
147
+
148
+func fieldPath(path, field string) string {
149
+ if path == "" {
150
+ return field
151
}
152
+ return path + "." + field
153
}
src/go/plugin/go.d/pkg/cloudauth/azuread_auth_config_test.go
+99
-9
@@ -5,6 +5,7 @@ package cloudauth
5
import (
6
"testing"
7
8
+ "github.com/stretchr/testify/assert"
9
"github.com/stretchr/testify/require"
10
)
11
@@ -16,25 +17,30 @@ func TestAzureADAuthConfigValidate(t *testing.T) {
17
"default mode": {
18
cfg: AzureADAuthConfig{Mode: AzureADAuthModeDefault},
19
},
19
- "empty mode defaults to default": {
20
- cfg: AzureADAuthConfig{},
20
+ "empty mode": {
21
+ cfg: AzureADAuthConfig{},
22
+ wantErr: true,
23
},
24
"managed identity mode": {
25
cfg: AzureADAuthConfig{Mode: AzureADAuthModeManagedIdentity},
26
},
27
"service principal mode": {
28
cfg: AzureADAuthConfig{
27
- Mode: AzureADAuthModeServicePrincipal,
28
- TenantID: "tenant",
29
- ClientID: "client",
30
- ClientSecret: "secret",
29
+ Mode: AzureADAuthModeServicePrincipal,
30
+ ModeServicePrincipal: &AzureADModeServicePrincipalConfig{
31
+ TenantID: "tenant",
32
+ ClientID: "client",
33
+ ClientSecret: "secret",
34
+ },
35
},
36
},
37
"service principal missing secret": {
38
cfg: AzureADAuthConfig{
35
- Mode: AzureADAuthModeServicePrincipal,
36
- TenantID: "tenant",
37
- ClientID: "client",
39
+ Mode: AzureADAuthModeServicePrincipal,
40
+ ModeServicePrincipal: &AzureADModeServicePrincipalConfig{
41
+ TenantID: "tenant",
42
+ ClientID: "client",
43
+ },
44
},
45
wantErr: true,
46
},
@@ -55,3 +61,87 @@ func TestAzureADAuthConfigValidate(t *testing.T) {
61
})
62
}
63
}
64
+
65
+func TestAzureADAuthConfigValidateWithPath(t *testing.T) {
66
+ tests := map[string]struct {
67
+ cfg AzureADAuthConfig
68
+ validatePath string
69
+ wantErrString string
70
+ }{
71
+ "cloud auth path": {
72
+ cfg: AzureADAuthConfig{
73
+ Mode: AzureADAuthModeServicePrincipal,
74
+ ModeServicePrincipal: &AzureADModeServicePrincipalConfig{
75
+ TenantID: "tenant",
76
+ ClientID: "client",
77
+ },
78
+ },
79
+ validatePath: azureADAuthConfigPath,
80
+ wantErrString: "cloud_auth.azure_ad.mode_service_principal.client_secret is required",
81
+ },
82
+ "root path": {
83
+ cfg: AzureADAuthConfig{
84
+ Mode: AzureADAuthModeServicePrincipal,
85
+ ModeServicePrincipal: &AzureADModeServicePrincipalConfig{
86
+ TenantID: "tenant",
87
+ ClientID: "client",
88
+ },
89
+ },
90
+ validatePath: "",
91
+ wantErrString: "mode_service_principal.client_secret is required",
92
+ },
93
+ "missing mode": {
94
+ cfg: AzureADAuthConfig{},
95
+ validatePath: azureADAuthConfigPath,
96
+ wantErrString: "cloud_auth.azure_ad.mode is required",
97
+ },
98
+ }
99
+
100
+ for name, tc := range tests {
101
+ t.Run(name, func(t *testing.T) {
102
+ err := tc.cfg.ValidateWithPath(tc.validatePath)
103
+ require.Error(t, err)
104
+ assert.ErrorContains(t, err, tc.wantErrString)
105
+ })
106
+ }
107
+}
108
+
109
+func TestAzureADAuthConfigNewCredentialWithOptions(t *testing.T) {
110
+ tests := map[string]struct {
111
+ cfg AzureADAuthConfig
112
+ wantErr bool
113
+ wantErrString string
114
+ }{
115
+ "valid default": {
116
+ cfg: AzureADAuthConfig{
117
+ Mode: AzureADAuthModeDefault,
118
+ },
119
+ },
120
+ "invalid service principal": {
121
+ cfg: AzureADAuthConfig{
122
+ Mode: AzureADAuthModeServicePrincipal,
123
+ ModeServicePrincipal: &AzureADModeServicePrincipalConfig{
124
+ TenantID: "tenant",
125
+ ClientID: "client",
126
+ },
127
+ },
128
+ wantErr: true,
129
+ wantErrString: "cloud_auth.azure_ad.mode_service_principal.client_secret is required",
130
+ },
131
+ }
132
+
133
+ for name, tc := range tests {
134
+ t.Run(name, func(t *testing.T) {
135
+ cred, err := tc.cfg.NewCredentialWithOptions(nil)
136
+ if tc.wantErr {
137
+ require.Error(t, err)
138
+ assert.Nil(t, cred)
139
+ assert.ErrorContains(t, err, tc.wantErrString)
140
+ return
141
+ }
142
+
143
+ require.NoError(t, err)
144
+ require.NotNil(t, cred)
145
+ })
146
+ }
147
+}
src/go/plugin/go.d/pkg/cloudauth/config.go
+4
-1
@@ -31,7 +31,10 @@ func (c Config) Validate() error {
31
case ProviderNone:
32
return nil
33
case ProviderAzureAD:
34
- return c.azureADConfig().Validate()
34
+ if c.AzureAD == nil {
35
+ return errors.New("cloud_auth.azure_ad is required")
36
+ }
37
+ return c.AzureAD.Validate()
38
default:
39
return fmt.Errorf("cloud_auth.provider %q is invalid: expected one of %q, %q",
40
c.Provider, ProviderNone, ProviderAzureAD)
src/go/plugin/go.d/pkg/cloudauth/config_test.go
+18
-8
@@ -29,7 +29,7 @@ func TestConfigValidate(t *testing.T) {
29
cfg: Config{
30
Provider: ProviderNone,
31
AzureAD: &AzureADAuthConfig{
32
- Mode: "service_principal",
32
+ Mode: AzureADAuthModeServicePrincipal,
33
},
34
},
35
},
@@ -37,20 +37,30 @@ func TestConfigValidate(t *testing.T) {
37
cfg: Config{
38
Provider: ProviderAzureAD,
39
AzureAD: &AzureADAuthConfig{
40
- Mode: AzureADAuthModeServicePrincipal,
41
- TenantID: "tenant",
42
- ClientID: "client",
43
- ClientSecret: "secret",
40
+ Mode: AzureADAuthModeServicePrincipal,
41
+ ModeServicePrincipal: &AzureADModeServicePrincipalConfig{
42
+ TenantID: "tenant",
43
+ ClientID: "client",
44
+ ClientSecret: "secret",
45
+ },
46
},
47
},
48
},
49
+ "provider azure_ad missing block": {
50
+ cfg: Config{
51
+ Provider: ProviderAzureAD,
52
+ },
53
+ wantErr: true,
54
+ },
55
"provider azure_ad invalid": {
56
cfg: Config{
57
Provider: ProviderAzureAD,
58
AzureAD: &AzureADAuthConfig{
51
- Mode: AzureADAuthModeServicePrincipal,
52
- TenantID: "tenant",
53
- ClientID: "client",
59
+ Mode: AzureADAuthModeServicePrincipal,
60
+ ModeServicePrincipal: &AzureADModeServicePrincipalConfig{
61
+ TenantID: "tenant",
62
+ ClientID: "client",
63
+ },
64
},
65
},
66
wantErr: true,
src/go/plugin/go.d/pkg/cloudauth/sqladapter/mssql.go
+10
-4
@@ -57,18 +57,24 @@ func BuildMSSQLAzureADDSN(baseDSN string, cfg cloudauth.Config) (string, error)
57
58
switch aadCfg.NormalizedMode() {
59
case cloudauth.AzureADAuthModeServicePrincipal:
60
+ sp := aadCfg.ModeServicePrincipal
61
+ if sp == nil {
62
+ return "", fmt.Errorf("unsupported cloud_auth.azure_ad.mode %q", aadCfg.Mode)
63
+ }
64
q.Set("fedauth", mssqlFedAuthServicePrincipal)
61
- clientID := strings.TrimSpace(aadCfg.ClientID)
62
- clientSecret := strings.TrimSpace(aadCfg.ClientSecret)
65
+ clientID := strings.TrimSpace(sp.ClientID)
66
+ clientSecret := strings.TrimSpace(sp.ClientSecret)
67
userID := clientID
64
- if tenantID := strings.TrimSpace(aadCfg.TenantID); tenantID != "" {
68
+ if tenantID := strings.TrimSpace(sp.TenantID); tenantID != "" {
69
userID = userID + "@" + tenantID
70
}
71
u.User = url.UserPassword(userID, clientSecret)
72
case cloudauth.AzureADAuthModeManagedIdentity:
73
+ mi := aadCfg.ModeManagedIdentity
74
q.Set("fedauth", mssqlFedAuthManagedIdentity)
75
u.User = nil
71
- if id := strings.TrimSpace(aadCfg.ManagedIdentityClientID); id != "" {
76
+ if mi != nil && strings.TrimSpace(mi.ClientID) != "" {
77
+ id := strings.TrimSpace(mi.ClientID)
78
q.Set("user id", id)
79
}
80
case cloudauth.AzureADAuthModeDefault:
src/go/plugin/go.d/pkg/cloudauth/sqladapter/mssql_test.go
+10
-6
@@ -39,10 +39,12 @@ func TestBuildMSSQLAzureADDSN(t *testing.T) {
39
cfg: cloudauth.Config{
40
Provider: cloudauth.ProviderAzureAD,
41
AzureAD: &cloudauth.AzureADAuthConfig{
42
- Mode: cloudauth.AzureADAuthModeServicePrincipal,
43
- TenantID: "tenant",
44
- ClientID: "client",
45
- ClientSecret: "secret",
42
+ Mode: cloudauth.AzureADAuthModeServicePrincipal,
43
+ ModeServicePrincipal: &cloudauth.AzureADModeServicePrincipalConfig{
44
+ TenantID: "tenant",
45
+ ClientID: "client",
46
+ ClientSecret: "secret",
47
+ },
48
},
49
},
50
parseDSN: true,
@@ -59,8 +61,10 @@ func TestBuildMSSQLAzureADDSN(t *testing.T) {
61
cfg: cloudauth.Config{
62
Provider: cloudauth.ProviderAzureAD,
63
AzureAD: &cloudauth.AzureADAuthConfig{
62
- Mode: cloudauth.AzureADAuthModeManagedIdentity,
63
- ManagedIdentityClientID: "mi-client-id",
64
+ Mode: cloudauth.AzureADAuthModeManagedIdentity,
65
+ ModeManagedIdentity: &cloudauth.AzureADModeManagedIdentityConfig{
66
+ ClientID: "mi-client-id",
67
+ },
68
},
69
},
70
parseDSN: true,