diff --git a/.chloggen/azuremonitor-additional-authentication.yaml b/.chloggen/azuremonitor-additional-authentication.yaml new file mode 100644 index 000000000000..2b64ddedab4c --- /dev/null +++ b/.chloggen/azuremonitor-additional-authentication.yaml @@ -0,0 +1,20 @@ +# Use this changelog template to create an entry for release notes. +# If your change doesn't affect end users, such as a test fix or a tooling change, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: receiver/azuremonitorreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add support for Managed Identity and Default Credential auth + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [31268, 33584] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: \ No newline at end of file diff --git a/receiver/azuremonitorreceiver/README.md b/receiver/azuremonitorreceiver/README.md index e484bf0cc81b..8c01eabd1b3f 100644 --- a/receiver/azuremonitorreceiver/README.md +++ b/receiver/azuremonitorreceiver/README.md @@ -22,7 +22,7 @@ The following settings are required: The following settings are optional: -- `auth` (default = service_principal): Specifies the used authentication method. Supported values are `service_principal`, `workload_identity`. +- `auth` (default = service_principal): Specifies the used authentication method. Supported values are `service_principal`, `workload_identity`, `managed_identity`, `default_credentials`. - `resource_groups` (default = none): Filter metrics for specific resource groups, not setting a value will scrape metrics for all resources in the subscription. - `services` (default = none): Filter metrics for specific services, not setting a value will scrape metrics for all services integrated with Azure Monitor. - `cache_resources` (default = 86400): List of resources will be cached for the provided amount of time in seconds. @@ -43,9 +43,13 @@ Authenticating using workload identities requires following additional settings: - `client_id` - `federate_token_file` +Authenticating using managed identities has the following optional settings: + +- `client_id` + ### Example Configurations -Using Service Principal for authentication: +Using [Service Principal](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication?tabs=bash#service-principal-with-a-secret) for authentication: ```yaml receivers: @@ -65,7 +69,7 @@ receivers: initial_delay: 1s ``` -Using Azure Workload Identity for authentication: +Using [Azure Workload Identity](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication?tabs=bash#option-2-use-workload-identity) for authentication: ```yaml receivers: @@ -77,6 +81,26 @@ receivers: federated_token_file: "${env:AZURE_FEDERATED_TOKEN_FILE}" ``` +Using [Managed Identity](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication?tabs=bash#option-3-use-a-managed-identity) for authentication: + +```yaml +receivers: + azuremonitor: + subscription_id: "${subscription_id}" + auth: "managed_identity" + client_id: "${env:AZURE_CLIENT_ID}" +``` + +Using [Environment Variables](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication?tabs=bash#option-1-define-environment-variables) for authentication: + +```yaml +receivers: + azuremonitor: + subscription_id: "${subscription_id}" + auth: "default_credentials" +``` + + ## Metrics Details about the metrics scraped by this receiver can be found in [Supported metrics with Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/metrics-supported). This receiver adds the prefix "azure_" to all scraped metrics. diff --git a/receiver/azuremonitorreceiver/config.go b/receiver/azuremonitorreceiver/config.go index 743fa94f5bb7..7078813512a8 100644 --- a/receiver/azuremonitorreceiver/config.go +++ b/receiver/azuremonitorreceiver/config.go @@ -247,8 +247,10 @@ type Config struct { } const ( - servicePrincipal = "service_principal" - workloadIdentity = "workload_identity" + defaultCredentials = "default_credentials" + servicePrincipal = "service_principal" + workloadIdentity = "workload_identity" + managedIdentity = "managed_identity" ) // Validate validates the configuration by checking for missing or invalid fields @@ -282,8 +284,11 @@ func (c Config) Validate() (err error) { if c.FederatedTokenFile == "" { err = multierr.Append(err, errMissingFedTokenFile) } + + case managedIdentity: + case defaultCredentials: default: - return fmt.Errorf("authentication %v is not supported. supported authentications include [%v,%v]", c.Authentication, servicePrincipal, workloadIdentity) + return fmt.Errorf("authentication %v is not supported. supported authentications include [%v,%v,%v,%v]", c.Authentication, servicePrincipal, workloadIdentity, managedIdentity, defaultCredentials) } if c.Cloud != azureCloud && c.Cloud != azureGovernmentCloud { diff --git a/receiver/azuremonitorreceiver/scraper.go b/receiver/azuremonitorreceiver/scraper.go index 7850ffca381a..c20ea33afaa4 100644 --- a/receiver/azuremonitorreceiver/scraper.go +++ b/receiver/azuremonitorreceiver/scraper.go @@ -83,8 +83,10 @@ func newScraper(conf *Config, settings receiver.Settings) *azureScraper { cfg: conf, settings: settings.TelemetrySettings, mb: metadata.NewMetricsBuilder(conf.MetricsBuilderConfig, settings), + azDefaultCredentialsFunc: azidentity.NewDefaultAzureCredential, azIDCredentialsFunc: azidentity.NewClientSecretCredential, azIDWorkloadFunc: azidentity.NewWorkloadIdentityCredential, + azManagedIdentityFunc: azidentity.NewManagedIdentityCredential, armClientFunc: armresources.NewClient, armMonitorDefinitionsClientFunc: armmonitor.NewMetricDefinitionsClient, armMonitorMetricsClientFunc: armmonitor.NewMetricsClient, @@ -104,8 +106,10 @@ type azureScraper struct { resources map[string]*azureResource resourcesUpdated time.Time mb *metadata.MetricsBuilder + azDefaultCredentialsFunc func(options *azidentity.DefaultAzureCredentialOptions) (*azidentity.DefaultAzureCredential, error) azIDCredentialsFunc func(string, string, string, *azidentity.ClientSecretCredentialOptions) (*azidentity.ClientSecretCredential, error) azIDWorkloadFunc func(options *azidentity.WorkloadIdentityCredentialOptions) (*azidentity.WorkloadIdentityCredential, error) + azManagedIdentityFunc func(options *azidentity.ManagedIdentityCredentialOptions) (*azidentity.ManagedIdentityCredential, error) armClientOptions *arm.ClientOptions armClientFunc func(string, azcore.TokenCredential, *arm.ClientOptions) (*armresources.Client, error) armMonitorDefinitionsClientFunc func(string, azcore.TokenCredential, *arm.ClientOptions) (*armmonitor.MetricDefinitionsClient, error) @@ -134,18 +138,18 @@ func (s *azureScraper) getArmClientOptions() *arm.ClientOptions { return &options } -func (s *azureScraper) getArmClient() armClient { - client, _ := s.armClientFunc(s.cfg.SubscriptionID, s.cred, s.armClientOptions) - return client +func (s *azureScraper) getArmClient() (armClient, error) { + client, err := s.armClientFunc(s.cfg.SubscriptionID, s.cred, s.armClientOptions) + return client, err } type metricsDefinitionsClientInterface interface { NewListPager(resourceURI string, options *armmonitor.MetricDefinitionsClientListOptions) *runtime.Pager[armmonitor.MetricDefinitionsClientListResponse] } -func (s *azureScraper) getMetricsDefinitionsClient() metricsDefinitionsClientInterface { - client, _ := s.armMonitorDefinitionsClientFunc(s.cfg.SubscriptionID, s.cred, s.armClientOptions) - return client +func (s *azureScraper) getMetricsDefinitionsClient() (metricsDefinitionsClientInterface, error) { + client, err := s.armMonitorDefinitionsClientFunc(s.cfg.SubscriptionID, s.cred, s.armClientOptions) + return client, err } type metricsValuesClient interface { @@ -154,9 +158,9 @@ type metricsValuesClient interface { ) } -func (s *azureScraper) GetMetricsValuesClient() metricsValuesClient { - client, _ := s.armMonitorMetricsClientFunc(s.cfg.SubscriptionID, s.cred, s.armClientOptions) - return client +func (s *azureScraper) GetMetricsValuesClient() (metricsValuesClient, error) { + client, err := s.armMonitorMetricsClientFunc(s.cfg.SubscriptionID, s.cred, s.armClientOptions) + return client, err } func (s *azureScraper) start(_ context.Context, _ component.Host) (err error) { @@ -165,9 +169,18 @@ func (s *azureScraper) start(_ context.Context, _ component.Host) (err error) { } s.armClientOptions = s.getArmClientOptions() - s.clientResources = s.getArmClient() - s.clientMetricsDefinitions = s.getMetricsDefinitionsClient() - s.clientMetricsValues = s.GetMetricsValuesClient() + s.clientResources, err = s.getArmClient() + if err != nil { + return err + } + s.clientMetricsDefinitions, err = s.getMetricsDefinitionsClient() + if err != nil { + return err + } + s.clientMetricsValues, err = s.GetMetricsValuesClient() + if err != nil { + return err + } s.resources = map[string]*azureResource{} @@ -176,6 +189,10 @@ func (s *azureScraper) start(_ context.Context, _ component.Host) (err error) { func (s *azureScraper) loadCredentials() (err error) { switch s.cfg.Authentication { + case defaultCredentials: + if s.cred, err = s.azDefaultCredentialsFunc(nil); err != nil { + return err + } case servicePrincipal: if s.cred, err = s.azIDCredentialsFunc(s.cfg.TenantID, s.cfg.ClientID, s.cfg.ClientSecret, nil); err != nil { return err @@ -184,6 +201,16 @@ func (s *azureScraper) loadCredentials() (err error) { if s.cred, err = s.azIDWorkloadFunc(nil); err != nil { return err } + case managedIdentity: + var options *azidentity.ManagedIdentityCredentialOptions + if s.cfg.ClientID != "" { + options = &azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ClientID(s.cfg.ClientID), + } + } + if s.cred, err = s.azManagedIdentityFunc(options); err != nil { + return err + } default: return fmt.Errorf("unknown authentication %v", s.cfg.Authentication) } diff --git a/receiver/azuremonitorreceiver/scraper_test.go b/receiver/azuremonitorreceiver/scraper_test.go index 375c3ca3c25a..68e643884b6b 100644 --- a/receiver/azuremonitorreceiver/scraper_test.go +++ b/receiver/azuremonitorreceiver/scraper_test.go @@ -41,6 +41,14 @@ func azIDWorkloadFuncMock(*azidentity.WorkloadIdentityCredentialOptions) (*azide return &azidentity.WorkloadIdentityCredential{}, nil } +func azManagedIdentityFuncMock(*azidentity.ManagedIdentityCredentialOptions) (*azidentity.ManagedIdentityCredential, error) { + return &azidentity.ManagedIdentityCredential{}, nil +} + +func azDefaultCredentialsFuncMock(*azidentity.DefaultAzureCredentialOptions) (*azidentity.DefaultAzureCredential, error) { + return &azidentity.DefaultAzureCredential{}, nil +} + func armClientFuncMock(string, azcore.TokenCredential, *arm.ClientOptions) (*armresources.Client, error) { return &armresources.Client{}, nil } @@ -137,6 +145,62 @@ func TestAzureScraperStart(t *testing.T) { require.IsType(t, &azidentity.WorkloadIdentityCredential{}, s.cred) }, }, + { + name: "managed_identity", + testFunc: func(t *testing.T) { + customCfg := &Config{ + ControllerConfig: cfg.ControllerConfig, + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + CacheResources: 24 * 60 * 60, + CacheResourcesDefinitions: 24 * 60 * 60, + MaximumNumberOfMetricsInACall: 20, + Services: monitorServices, + Authentication: managedIdentity, + } + s := &azureScraper{ + cfg: customCfg, + azIDCredentialsFunc: azIDCredentialsFuncMock, + azManagedIdentityFunc: azManagedIdentityFuncMock, + armClientFunc: armClientFuncMock, + armMonitorDefinitionsClientFunc: armMonitorDefinitionsClientFuncMock, + armMonitorMetricsClientFunc: armMonitorMetricsClientFuncMock, + } + + if err := s.start(context.Background(), componenttest.NewNopHost()); err != nil { + t.Errorf("azureScraper.start() error = %v", err) + } + require.NotNil(t, s.cred) + require.IsType(t, &azidentity.ManagedIdentityCredential{}, s.cred) + }, + }, + { + name: "default_credentials", + testFunc: func(t *testing.T) { + customCfg := &Config{ + ControllerConfig: cfg.ControllerConfig, + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + CacheResources: 24 * 60 * 60, + CacheResourcesDefinitions: 24 * 60 * 60, + MaximumNumberOfMetricsInACall: 20, + Services: monitorServices, + Authentication: defaultCredentials, + } + s := &azureScraper{ + cfg: customCfg, + azIDCredentialsFunc: azIDCredentialsFuncMock, + azDefaultCredentialsFunc: azDefaultCredentialsFuncMock, + armClientFunc: armClientFuncMock, + armMonitorDefinitionsClientFunc: armMonitorDefinitionsClientFuncMock, + armMonitorMetricsClientFunc: armMonitorMetricsClientFuncMock, + } + + if err := s.start(context.Background(), componenttest.NewNopHost()); err != nil { + t.Errorf("azureScraper.start() error = %v", err) + } + require.NotNil(t, s.cred) + require.IsType(t, &azidentity.DefaultAzureCredential{}, s.cred) + }, + }, } for _, tt := range tests { t.Run(tt.name, tt.testFunc)