diff --git a/tools/cmdutils/auth.go b/tools/cmdutils/auth.go index 101ee228..78747b60 100644 --- a/tools/cmdutils/auth.go +++ b/tools/cmdutils/auth.go @@ -4,6 +4,7 @@ import ( "os" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" ) @@ -14,9 +15,19 @@ import ( // workload identity token used in the GitHub action as it currently // does not refresh: https://github.com/Azure/azure-cli/issues/28708 func GetAzureTokenCredentials() (azcore.TokenCredential, error) { + return GetAzureTokenCredentialsForCloud(cloud.AzurePublic) +} + +// GetAzureTokenCredentialsForCloud returns an Azure TokenCredential configured for +// a specific Azure cloud. The cloud config determines the AAD authority host used +// when requesting tokens. +func GetAzureTokenCredentialsForCloud(cloudConfig cloud.Configuration) (azcore.TokenCredential, error) { if _, ok := os.LookupEnv("GITHUB_ACTIONS"); ok { return azidentity.NewAzureCLICredential(nil) } - return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{RequireAzureTokenCredentials: true}) + return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ + ClientOptions: azcore.ClientOptions{Cloud: cloudConfig}, + RequireAzureTokenCredentials: true, + }) } diff --git a/tools/grafanactl/README.md b/tools/grafanactl/README.md index 66dd78f9..1de704d2 100644 --- a/tools/grafanactl/README.md +++ b/tools/grafanactl/README.md @@ -42,6 +42,16 @@ All commands require these basic parameters: - `--output` - Output format: `table` (default) or `json` - `-v, --verbosity` - Set logging verbosity level (0-10) +For sovereign clouds (e.g. Fairfax ), pass the ARM endpoint and AAD +authority directly. Both flags must be set together; if neither is provided, +the public Azure cloud is used. Each flag accepts either a hostname or a full +`https://` URL — bare hostnames are normalized to URL form automatically. + +- `--arm-endpoint` - Azure Resource Manager endpoint (e.g. + `management.usgovcloudapi.net` for Fairfax). +- `--aad-authority` - Microsoft Entra ID authority (e.g. + `login.microsoftonline.us` for Fairfax). + ### List Commands #### List Datasources diff --git a/tools/grafanactl/cmd/base/options.go b/tools/grafanactl/cmd/base/options.go index 0983d026..520e4fbc 100644 --- a/tools/grafanactl/cmd/base/options.go +++ b/tools/grafanactl/cmd/base/options.go @@ -16,11 +16,14 @@ package base import ( "fmt" + "net/url" "strings" "github.com/spf13/cobra" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" ) // BaseOptions represents common options used across multiple commands. @@ -31,6 +34,15 @@ type BaseOptions struct { GrafanaResourceID string OutputFormat string DryRun bool + ARMEndpoint string + AADAuthority string +} + +// CompletedBaseOptions represents base options that have been validated and resolved +type CompletedBaseOptions struct { + // CloudConfig is the resolved Azure SDK cloud configuration derived from the + // ARMEndpoint and AADAuthority flags (or the public cloud defaults when not set) + CloudConfig cloud.Configuration } // DefaultBaseOptions returns a new BaseOptions with default values @@ -49,22 +61,24 @@ func BindBaseOptions(opts *BaseOptions, cmd *cobra.Command) error { flags.StringVar(&opts.GrafanaResourceID, "grafana-resource-id", opts.GrafanaResourceID, "Azure Managed Grafana instance resource ID") flags.StringVar(&opts.OutputFormat, "output", opts.OutputFormat, "Output format: table or json") flags.BoolVar(&opts.DryRun, "dry-run", opts.DryRun, "Print actions without executing them") + flags.StringVar(&opts.ARMEndpoint, "arm-endpoint", opts.ARMEndpoint, "Azure Resource Manager endpoint for the target cloud. Defaults to the public cloud when unset") + flags.StringVar(&opts.AADAuthority, "aad-authority", opts.AADAuthority, "Microsoft Entra ID (AAD) authority for the target cloud. Defaults to the public cloud when unset") return nil } // ValidateBaseOptions performs validation on the base options -func ValidateBaseOptions(opts *BaseOptions) error { +func ValidateBaseOptions(opts *BaseOptions) (*CompletedBaseOptions, error) { // Validate required fields if opts.GrafanaResourceID == "" { if opts.SubscriptionID == "" || opts.ResourceGroup == "" || opts.GrafanaName == "" { - return fmt.Errorf("subscription ID, resource group, and grafana name are required if grafana resource ID is not provided") + return nil, fmt.Errorf("subscription ID, resource group, and grafana name are required if grafana resource ID is not provided") } } else { resourceID, err := ValidateAzureResourceID(opts.GrafanaResourceID, "Microsoft.Dashboard/grafana") if err != nil { - return fmt.Errorf("failed to validate grafana resource ID: %w", err) + return nil, fmt.Errorf("failed to validate grafana resource ID: %w", err) } opts.SubscriptionID = resourceID.SubscriptionID opts.ResourceGroup = resourceID.ResourceGroupName @@ -73,10 +87,84 @@ func ValidateBaseOptions(opts *BaseOptions) error { // Validate output format if opts.OutputFormat != "table" && opts.OutputFormat != "json" { - return fmt.Errorf("output format must be 'table' or 'json', got: %s", opts.OutputFormat) + return nil, fmt.Errorf("output format must be 'table' or 'json', got: %s", opts.OutputFormat) } - return nil + cloudConfig, err := resolveCloudConfig(opts.ARMEndpoint, opts.AADAuthority) + if err != nil { + return nil, err + } + + return &CompletedBaseOptions{CloudConfig: cloudConfig}, nil +} + +// resolveCloudConfig builds a cloud.Configuration from the provided ARM endpoint +// and AAD authority. When both inputs are empty, the public cloud configuration is +// returned. When both are set, a custom configuration is built. Each input may be +// a hostname or a full URL (see normalizeEndpoint). +func resolveCloudConfig(armEndpoint, aadAuthority string) (cloud.Configuration, error) { + if armEndpoint == "" && aadAuthority == "" { + return cloud.AzurePublic, nil + } + if armEndpoint == "" || aadAuthority == "" { + return cloud.Configuration{}, fmt.Errorf("--arm-endpoint and --aad-authority must both be set, or both unset") + } + + normalizedARM, err := normalizeEndpoint(armEndpoint) + if err != nil { + return cloud.Configuration{}, fmt.Errorf("invalid --arm-endpoint %q: %w", armEndpoint, err) + } + + normalizedAAD, err := normalizeEndpoint(aadAuthority) + if err != nil { + return cloud.Configuration{}, fmt.Errorf("invalid --aad-authority %q: %w", aadAuthority, err) + } + + return cloud.Configuration{ + ActiveDirectoryAuthorityHost: normalizedAAD, + Services: map[cloud.ServiceName]cloud.ServiceConfiguration{ + cloud.ResourceManager: { + Endpoint: normalizedARM, + Audience: normalizedARM, + }, + }, + }, nil +} + +// normalizeEndpoint accepts either a hostname or a full URL and returns a +// URL-form value with the https scheme. EV2 central config typically stores +// endpoint DNS values as hostnames, so we prepend the scheme when it is missing +// to make the input forgiving for callers. +func normalizeEndpoint(endpoint string) (string, error) { + endpoint = strings.TrimSpace(endpoint) + if endpoint == "" { + return "", fmt.Errorf("endpoint cannot be empty") + } + + if !strings.HasPrefix(endpoint, "https://") { + endpoint = "https://" + endpoint + } + + parsed, err := url.Parse(endpoint) + if err != nil { + return "", fmt.Errorf("not a valid URL: %w", err) + } + if parsed.Scheme != "https" { + return "", fmt.Errorf("endpoint must use https scheme, got %q", parsed.Scheme) + } + if parsed.Host == "" { + return "", fmt.Errorf("endpoint host cannot be empty") + } + + return endpoint, nil +} + +// ARMClientOptions returns an *arm.ClientOptions configured with the resolved +// cloud configuration, suitable for passing to Azure SDK ARM client constructors +func (c *CompletedBaseOptions) ARMClientOptions() *azcorearm.ClientOptions { + return &azcorearm.ClientOptions{ + ClientOptions: azcore.ClientOptions{Cloud: c.CloudConfig}, + } } // ValidateAzureResourceID validates an Azure resource ID and ensures it's an Azure Managed Grafana resource diff --git a/tools/grafanactl/cmd/base/options_test.go b/tools/grafanactl/cmd/base/options_test.go new file mode 100644 index 00000000..6e257325 --- /dev/null +++ b/tools/grafanactl/cmd/base/options_test.go @@ -0,0 +1,176 @@ +// Copyright 2025 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package base + +import ( + "strings" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" +) + +func TestNormalizeEndpoint(t *testing.T) { + for _, tc := range []struct { + name string + input string + want string + wantErrSub string + }{ + { + name: "hostname is prefixed with https", + input: "management.usgovcloudapi.net", + want: "https://management.usgovcloudapi.net", + }, + { + name: "full https URL is returned unchanged", + input: "https://management.usgovcloudapi.net", + want: "https://management.usgovcloudapi.net", + }, + { + name: "https URL with trailing slash is preserved", + input: "https://login.microsoftonline.us/", + want: "https://login.microsoftonline.us/", + }, + { + name: "leading and trailing whitespace is trimmed", + input: " management.azure.com ", + want: "https://management.azure.com", + }, + { + name: "empty string returns an error", + input: "", + wantErrSub: "endpoint cannot be empty", + }, + { + name: "whitespace-only string returns an error", + input: " ", + wantErrSub: "endpoint cannot be empty", + }, + { + name: "missing host is rejected", + input: "https://", + wantErrSub: "host cannot be empty", + }, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := normalizeEndpoint(tc.input) + if tc.wantErrSub != "" { + if err == nil { + t.Fatalf("expected error containing %q, got nil (result %q)", tc.wantErrSub, got) + } + if !strings.Contains(err.Error(), tc.wantErrSub) { + t.Fatalf("expected error containing %q, got %q", tc.wantErrSub, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.want { + t.Fatalf("got %q, want %q", got, tc.want) + } + }) + } +} + +func TestResolveCloudConfig(t *testing.T) { + for _, tc := range []struct { + name string + armEndpoint string + aadAuthority string + wantPublic bool + wantARM string + wantAAD string + wantErrSub string + }{ + { + name: "both empty defaults to public cloud", + wantPublic: true, + }, + { + name: "only ARM endpoint set returns an error", + armEndpoint: "management.usgovcloudapi.net", + aadAuthority: "", + wantErrSub: "must both be set", + }, + { + name: "only AAD authority set returns an error", + armEndpoint: "", + aadAuthority: "login.microsoftonline.us", + wantErrSub: "must both be set", + }, + { + name: "both set as hostnames builds Gov cloud config", + armEndpoint: "management.usgovcloudapi.net", + aadAuthority: "login.microsoftonline.us", + wantARM: "https://management.usgovcloudapi.net", + wantAAD: "https://login.microsoftonline.us", + }, + { + name: "both set as full URLs builds Gov cloud config", + armEndpoint: "https://management.usgovcloudapi.net", + aadAuthority: "https://login.microsoftonline.us/", + wantARM: "https://management.usgovcloudapi.net", + wantAAD: "https://login.microsoftonline.us/", + }, + { + name: "empty ARM endpoint after trimming surfaces a wrapped error", + armEndpoint: " ", + aadAuthority: "login.microsoftonline.us", + wantErrSub: "invalid --arm-endpoint", + }, + { + name: "empty AAD authority after trimming surfaces a wrapped error", + armEndpoint: "management.usgovcloudapi.net", + aadAuthority: " ", + wantErrSub: "invalid --aad-authority", + }, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := resolveCloudConfig(tc.armEndpoint, tc.aadAuthority) + if tc.wantErrSub != "" { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErrSub) + } + if !strings.Contains(err.Error(), tc.wantErrSub) { + t.Fatalf("expected error containing %q, got %q", tc.wantErrSub, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tc.wantPublic { + if got.ActiveDirectoryAuthorityHost != cloud.AzurePublic.ActiveDirectoryAuthorityHost { + t.Fatalf("expected public cloud authority, got %q", got.ActiveDirectoryAuthorityHost) + } + return + } + if got.ActiveDirectoryAuthorityHost != tc.wantAAD { + t.Fatalf("AAD authority: got %q, want %q", got.ActiveDirectoryAuthorityHost, tc.wantAAD) + } + armSvc, ok := got.Services[cloud.ResourceManager] + if !ok { + t.Fatal("expected ResourceManager service in cloud configuration") + } + if armSvc.Endpoint != tc.wantARM { + t.Fatalf("ARM endpoint: got %q, want %q", armSvc.Endpoint, tc.wantARM) + } + if armSvc.Audience != tc.wantARM { + t.Fatalf("ARM audience: got %q, want %q", armSvc.Audience, tc.wantARM) + } + }) + } +} diff --git a/tools/grafanactl/cmd/clean/options.go b/tools/grafanactl/cmd/clean/options.go index e14c7f7f..bf54be73 100644 --- a/tools/grafanactl/cmd/clean/options.go +++ b/tools/grafanactl/cmd/clean/options.go @@ -34,6 +34,7 @@ type RawCleanDatasourcesOptions struct { // validatedCleanOptions is a private struct that enforces the options validation pattern. type validatedCleanDatasourcesOptions struct { *RawCleanDatasourcesOptions + *base.CompletedBaseOptions } // ValidatedCleanOptions represents clean configuration that has passed validation. @@ -69,25 +70,29 @@ func BindCleanDatasourcesOptions(opts *RawCleanDatasourcesOptions, cmd *cobra.Co // Validate performs validation on the raw options func (o *RawCleanDatasourcesOptions) Validate(ctx context.Context) (*ValidatedCleanDatasourcesOptions, error) { - if err := base.ValidateBaseOptions(o.BaseOptions); err != nil { + completedBase, err := base.ValidateBaseOptions(o.BaseOptions) + if err != nil { return nil, err } return &ValidatedCleanDatasourcesOptions{ validatedCleanDatasourcesOptions: &validatedCleanDatasourcesOptions{ RawCleanDatasourcesOptions: o, + CompletedBaseOptions: completedBase, }, }, nil } // Complete performs final initialization to create fully usable clean options. func (o *ValidatedCleanDatasourcesOptions) Complete(ctx context.Context) (*CompletedCleanDatasourcesOptions, error) { - cred, err := cmdutils.GetAzureTokenCredentials() + cred, err := cmdutils.GetAzureTokenCredentialsForCloud(o.CloudConfig) if err != nil { return nil, fmt.Errorf("failed to obtain Azure credentials: %w", err) } - managedGrafanaClient, err := azure.NewManagedGrafanaClient(o.SubscriptionID, cred) + clientOpts := o.ARMClientOptions() + + managedGrafanaClient, err := azure.NewManagedGrafanaClient(o.SubscriptionID, cred, clientOpts) if err != nil { return nil, fmt.Errorf("failed to create managed Grafana client: %w", err) } @@ -97,7 +102,7 @@ func (o *ValidatedCleanDatasourcesOptions) Complete(ctx context.Context) (*Compl return nil, fmt.Errorf("failed to create Grafana client: %w", err) } - monitorWorkspaceClient, err := azure.NewMonitorWorkspaceClient(o.SubscriptionID, cred) + monitorWorkspaceClient, err := azure.NewMonitorWorkspaceClient(o.SubscriptionID, cred, clientOpts) if err != nil { return nil, fmt.Errorf("failed to create managed Prometheus client: %w", err) } diff --git a/tools/grafanactl/cmd/list/options.go b/tools/grafanactl/cmd/list/options.go index 43adeba9..674606c5 100644 --- a/tools/grafanactl/cmd/list/options.go +++ b/tools/grafanactl/cmd/list/options.go @@ -34,6 +34,7 @@ type RawListDataSourcesOptions struct { // validatedListDataSourcesOptions is a private struct that enforces the options validation pattern. type validatedListDataSourcesOptions struct { *RawListDataSourcesOptions + *base.CompletedBaseOptions } // ValidatedListDataSourcesOptions represents list-datasources configuration that has passed validation. @@ -63,24 +64,29 @@ func BindListDataSourcesOptions(opts *RawListDataSourcesOptions, cmd *cobra.Comm // Validate performs validation on the raw options func (o *RawListDataSourcesOptions) Validate(ctx context.Context) (*ValidatedListDataSourcesOptions, error) { - if err := base.ValidateBaseOptions(o.BaseOptions); err != nil { + completedBase, err := base.ValidateBaseOptions(o.BaseOptions) + if err != nil { return nil, err } return &ValidatedListDataSourcesOptions{ validatedListDataSourcesOptions: &validatedListDataSourcesOptions{ RawListDataSourcesOptions: o, + CompletedBaseOptions: completedBase, }, }, nil } // Complete performs final initialization to create fully usable list-datasources options. func (o *ValidatedListDataSourcesOptions) Complete(ctx context.Context) (*CompletedListDataSourcesOptions, error) { - cred, err := cmdutils.GetAzureTokenCredentials() + cred, err := cmdutils.GetAzureTokenCredentialsForCloud(o.CloudConfig) if err != nil { return nil, fmt.Errorf("failed to obtain Azure credentials: %w", err) } - managedGrafanaClient, err := azure.NewManagedGrafanaClient(o.SubscriptionID, cred) + + clientOpts := o.ARMClientOptions() + + managedGrafanaClient, err := azure.NewManagedGrafanaClient(o.SubscriptionID, cred, clientOpts) if err != nil { return nil, fmt.Errorf("failed to create Managed Grafana client: %w", err) } diff --git a/tools/grafanactl/cmd/modify/options.go b/tools/grafanactl/cmd/modify/options.go index 79a987b9..28cbd3d5 100644 --- a/tools/grafanactl/cmd/modify/options.go +++ b/tools/grafanactl/cmd/modify/options.go @@ -35,6 +35,7 @@ type RawAddDatasourceOptions struct { // validatedAddDatasourceOptions is a private struct that enforces the options validation pattern. type validatedAddDatasourceOptions struct { *RawAddDatasourceOptions + *base.CompletedBaseOptions } // ValidatedAddDatasourceOptions represents add datasource configuration that has passed validation. @@ -75,7 +76,8 @@ func BindAddDatasourceOptions(opts *RawAddDatasourceOptions, cmd *cobra.Command) // Validate performs validation on the raw options func (o *RawAddDatasourceOptions) Validate(ctx context.Context) (*ValidatedAddDatasourceOptions, error) { - if err := base.ValidateBaseOptions(o.BaseOptions); err != nil { + completedBase, err := base.ValidateBaseOptions(o.BaseOptions) + if err != nil { return nil, err } @@ -86,23 +88,26 @@ func (o *RawAddDatasourceOptions) Validate(ctx context.Context) (*ValidatedAddDa TagKey: o.TagKey, TagValue: o.TagValue, }, + CompletedBaseOptions: completedBase, }, }, nil } // Complete performs final initialization to create fully usable add datasource options. func (o *ValidatedAddDatasourceOptions) Complete(ctx context.Context) (*CompletedAddDatasourceOptions, error) { - cred, err := cmdutils.GetAzureTokenCredentials() + cred, err := cmdutils.GetAzureTokenCredentialsForCloud(o.CloudConfig) if err != nil { return nil, fmt.Errorf("failed to obtain Azure credentials: %w", err) } - managedGrafanaClient, err := azure.NewManagedGrafanaClient(o.SubscriptionID, cred) + clientOpts := o.ARMClientOptions() + + managedGrafanaClient, err := azure.NewManagedGrafanaClient(o.SubscriptionID, cred, clientOpts) if err != nil { return nil, fmt.Errorf("failed to create managed Grafana client: %w", err) } - monitorWorkspaceClient, err := azure.NewMonitorWorkspaceClient(o.SubscriptionID, cred) + monitorWorkspaceClient, err := azure.NewMonitorWorkspaceClient(o.SubscriptionID, cred, clientOpts) if err != nil { return nil, fmt.Errorf("failed to create monitor workspace client: %w", err) } diff --git a/tools/grafanactl/cmd/sync/options.go b/tools/grafanactl/cmd/sync/options.go index e2594eea..bcf64d62 100644 --- a/tools/grafanactl/cmd/sync/options.go +++ b/tools/grafanactl/cmd/sync/options.go @@ -39,6 +39,7 @@ type RawSyncDashboardsOptions struct { // validatedSyncDashboardsOptions is a private struct that enforces the options validation pattern. type validatedSyncDashboardsOptions struct { *RawSyncDashboardsOptions + *base.CompletedBaseOptions } // ValidatedSyncDashboardsOptions represents sync configuration that has passed validation. @@ -78,7 +79,8 @@ func BindSyncDashboardsOptions(opts *RawSyncDashboardsOptions, cmd *cobra.Comman // Validate performs validation on the raw options func (o *RawSyncDashboardsOptions) Validate(ctx context.Context) (*ValidatedSyncDashboardsOptions, error) { - if err := base.ValidateBaseOptions(o.BaseOptions); err != nil { + completedBase, err := base.ValidateBaseOptions(o.BaseOptions) + if err != nil { return nil, err } @@ -96,6 +98,7 @@ func (o *RawSyncDashboardsOptions) Validate(ctx context.Context) (*ValidatedSync return &ValidatedSyncDashboardsOptions{ validatedSyncDashboardsOptions: &validatedSyncDashboardsOptions{ RawSyncDashboardsOptions: o, + CompletedBaseOptions: completedBase, }, }, nil } @@ -107,12 +110,14 @@ func (o *ValidatedSyncDashboardsOptions) Complete(ctx context.Context) (*Complet return nil, fmt.Errorf("failed to load config: %w", err) } - cred, err := cmdutils.GetAzureTokenCredentials() + cred, err := cmdutils.GetAzureTokenCredentialsForCloud(o.CloudConfig) if err != nil { return nil, fmt.Errorf("failed to obtain Azure credentials: %w", err) } - managedGrafanaClient, err := azure.NewManagedGrafanaClient(o.SubscriptionID, cred) + clientOpts := o.ARMClientOptions() + + managedGrafanaClient, err := azure.NewManagedGrafanaClient(o.SubscriptionID, cred, clientOpts) if err != nil { return nil, fmt.Errorf("failed to create managed Grafana client: %w", err) } diff --git a/tools/grafanactl/internal/azure/grafana.go b/tools/grafanactl/internal/azure/grafana.go index e4af3490..0b9797d4 100644 --- a/tools/grafanactl/internal/azure/grafana.go +++ b/tools/grafanactl/internal/azure/grafana.go @@ -23,6 +23,7 @@ import ( "github.com/go-logr/logr" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dashboard/armdashboard/v2" ) @@ -32,9 +33,11 @@ type ManagedGrafanaClient struct { client *armdashboard.GrafanaClient } -// NewManagedGrafanaClient creates a new ManagedGrafanaClient with the provided credentials -func NewManagedGrafanaClient(subscriptionID string, cred azcore.TokenCredential) (*ManagedGrafanaClient, error) { - grafanaClient, err := armdashboard.NewGrafanaClient(subscriptionID, cred, nil) +// NewManagedGrafanaClient creates a new ManagedGrafanaClient with the provided credentials. +// The clientOptions parameter allows callers to specify cloud-specific configuration +// (e.g. cloud.AzureGovernment for Fairfax). Pass nil to use the default (public cloud). +func NewManagedGrafanaClient(subscriptionID string, cred azcore.TokenCredential, clientOptions *arm.ClientOptions) (*ManagedGrafanaClient, error) { + grafanaClient, err := armdashboard.NewGrafanaClient(subscriptionID, cred, clientOptions) if err != nil { return nil, fmt.Errorf("failed to create Azure Monitor Workspaces client: %w", err) } diff --git a/tools/grafanactl/internal/azure/monitor.go b/tools/grafanactl/internal/azure/monitor.go index bad056df..276dd85e 100644 --- a/tools/grafanactl/internal/azure/monitor.go +++ b/tools/grafanactl/internal/azure/monitor.go @@ -19,6 +19,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor" ) @@ -35,9 +36,11 @@ type MonitorWorkspaceClient struct { subscriptionID string } -// NewPrometheusClient creates a new PrometheusClient with the provided credentials -func NewMonitorWorkspaceClient(subscriptionID string, cred azcore.TokenCredential) (*MonitorWorkspaceClient, error) { - client, err := armmonitor.NewAzureMonitorWorkspacesClient(subscriptionID, cred, nil) +// NewMonitorWorkspaceClient creates a new MonitorWorkspaceClient with the provided credentials. +// The clientOptions parameter allows callers to specify cloud-specific configuration +// (e.g. cloud.AzureGovernment for Fairfax). Pass nil to use the default (public cloud). +func NewMonitorWorkspaceClient(subscriptionID string, cred azcore.TokenCredential, clientOptions *arm.ClientOptions) (*MonitorWorkspaceClient, error) { + client, err := armmonitor.NewAzureMonitorWorkspacesClient(subscriptionID, cred, clientOptions) if err != nil { return nil, fmt.Errorf("failed to create Azure Monitor Workspaces client: %w", err) }