azure

package module
v0.0.0-...-16b5795 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: OSL-3.0 Imports: 30 Imported by: 0

Documentation

Overview

Package azure provides Azure cloud provider implementation

Package azure provides Azure recommendations client

Package azure provides the org-wide (multi-subscription) recommendations fan-out client.

Package azure provides service client factory functions

Index

Constants

This section is empty.

Variables

View Source
var ErrSubscriptionNotAccessible = errors.New("subscription is not accessible to the authenticated principal")

ErrSubscriptionNotAccessible is the cause recorded for a subscription named by RecommendationParams.AccountFilter that is not among the subscriptions visible to the authenticated principal.

It is a distinct sentinel (rather than a formatted string) so a caller can tell "the principal cannot see this subscription" -- a durable access or configuration problem -- apart from a transient per-subscription ARM failure that a retry might clear.

Functions

func NewCacheClient

func NewCacheClient(cred azcore.TokenCredential, subscriptionID, region string) provider.ServiceClient

NewCacheClient creates a new Azure Cache for Redis client.

func NewComputeClient

func NewComputeClient(cred azcore.TokenCredential, subscriptionID, region string) provider.ServiceClient

NewComputeClient creates a new Azure Compute (VM) client.

func NewCosmosDBClient

func NewCosmosDBClient(cred azcore.TokenCredential, subscriptionID, region string) provider.ServiceClient

NewCosmosDBClient creates a new Azure Cosmos DB client.

func NewDatabaseClient

func NewDatabaseClient(cred azcore.TokenCredential, subscriptionID, region string) provider.ServiceClient

NewDatabaseClient creates a new Azure SQL Database client.

func NewManagedRedisClient

func NewManagedRedisClient(cred azcore.TokenCredential, subscriptionID, region string) provider.ServiceClient

NewManagedRedisClient creates a new Azure Managed Redis client (ServiceMemoryDB). Azure Cache for Redis is the Azure equivalent of AWS MemoryDB for Redis.

func NewRecommendationsClient

func NewRecommendationsClient(cred azcore.TokenCredential, subscriptionID string) (provider.RecommendationsClient, error)

NewRecommendationsClient creates a new Azure recommendations client.

Returns an error when subscriptionID is empty — the adapter's downstream converters use it as the Recommendation.Account field, and a silently empty Account would mis-route recommendations in account-scoped caches, UI filters and billing reports. Callers that want the bare struct should use NewRecommendationsClientAdapter directly.

func NewSavingsPlansClient

func NewSavingsPlansClient(cred azcore.TokenCredential, subscriptionID, region string) provider.ServiceClient

NewSavingsPlansClient creates a new Azure Savings Plans client.

func NewSearchClient

func NewSearchClient(cred azcore.TokenCredential, subscriptionID, region string) provider.ServiceClient

NewSearchClient creates a new Azure Cognitive Search client.

func NewSynapseClient

func NewSynapseClient(cred azcore.TokenCredential, subscriptionID, region string) provider.ServiceClient

NewSynapseClient creates a new Azure Synapse Analytics client.

Types

type AdvisorPager

type AdvisorPager interface {
	More() bool
	NextPage(ctx context.Context) (armadvisor.RecommendationsClientListResponse, error)
}

AdvisorPager is the page iterator collectAdvisorRecommendations walks (enables mocking).

type CredentialProvider

type CredentialProvider interface {
	NewDefaultAzureCredential() (azcore.TokenCredential, error)
}

CredentialProvider interface for credential creation (enables mocking).

type LocationsPager

type LocationsPager interface {
	More() bool
	NextPage(ctx context.Context) (armsubscriptions.ClientListLocationsResponse, error)
}

LocationsPager interface for locations pagination (enables mocking).

type MultiSubscriptionRecommendationsClient

type MultiSubscriptionRecommendationsClient struct {
	// contains filtered or unexported fields
}

MultiSubscriptionRecommendationsClient fans recommendation collection out across the Azure subscriptions accessible to the authenticated principal -- every one of them by default, or the subset named by RecommendationParams.AccountFilter (see selectSubscriptions).

Azure has no organization-wide equivalent of AWS Cost Explorer's AccountScope=Linked: the Consumption Reservation Recommendations and Advisor APIs are subscription-scoped. Achieving AWS-parity org-wide coverage therefore requires calling the per-subscription RecommendationsClientAdapter once per subscription and aggregating the results client-side, which is what this type does.

func NewMultiSubscriptionRecommendationsClient

func NewMultiSubscriptionRecommendationsClient(cred azcore.TokenCredential, accounts []common.Account) (*MultiSubscriptionRecommendationsClient, error)

NewMultiSubscriptionRecommendationsClient builds a fan-out client covering every account in accounts. Returns an error when accounts is empty (there is nothing to fan out to) or when building the per-subscription client fails for any account -- fail loud rather than silently dropping a subscription that should have been covered.

func (*MultiSubscriptionRecommendationsClient) GetAllRecommendations

GetAllRecommendations retrieves recommendations for every supported service across every subscription.

func (*MultiSubscriptionRecommendationsClient) GetRecommendations

GetRecommendations fans params out concurrently (errgroup) to the subscriptions selected by selectSubscriptions -- every accessible subscription unless params.AccountFilter narrows it -- and merges the results.

Error isolation mirrors RecommendationsClientAdapter.GetRecommendations: each per-subscription goroutine captures its own error and returns nil to the group, so one subscription failing (e.g. the principal lost Reader access mid-run, or a subscription-specific throttle) never cancels sibling subscriptions. The semaphore that bounds aggregate concurrent ARM calls is acquired inside each per-subscription client's own GetRecommendations (around the outbound API calls, not around this fan-out), so no additional semaphore is needed at this layer.

After g.Wait(), ctx.Err() is checked explicitly: g.Wait() only reports errors returned to the group, and every goroutine here returns nil, so a parent-context cancellation would otherwise go unnoticed.

If every subscription fails, GetRecommendations returns a wrapped error instead of a silently empty, nil-error result -- the same all-attempted-failed guard used by mergeServiceResults, ported here so a total credential/throttle failure isn't indistinguishable from "no savings available across the whole tenant".

If SOME subscriptions fail, it returns the successful subscriptions' recommendations together with a *PartialSubscriptionFailureError. Callers that want the partial data must inspect the error with errors.As; a caller that treats any non-nil error as fatal gets a loud failure rather than a silently incomplete sweep. Either way the incompleteness is visible in the return values, not just in a log line.

The same signal covers params.AccountFilter entries that name no accessible subscription: they are reported as ErrSubscriptionNotAccessible failures rather than silently dropped from the sweep, so a partially-satisfied filter never returns a nil error (see selectSubscriptions).

func (*MultiSubscriptionRecommendationsClient) GetRecommendationsForService

func (m *MultiSubscriptionRecommendationsClient) GetRecommendationsForService(ctx context.Context, service common.ServiceType) ([]common.Recommendation, error)

GetRecommendationsForService retrieves recommendations for a single service across every subscription.

type PartialSubscriptionFailureError

type PartialSubscriptionFailureError struct {
	// Attempted is how many subscriptions the sweep was supposed to cover:
	// the ones actually queried plus any named by AccountFilter that the
	// principal cannot see (those are never queried, but they were asked
	// for, so leaving them out of the denominator would under-report the
	// gap this error exists to surface).
	Attempted int
	// Succeeded is how many returned a result. Always < Attempted and > 0:
	// an all-failed sweep is a plain error, not a partial one.
	Succeeded int
	// Failed carries every subscription that errored, with its cause.
	Failed []SubscriptionFailure
}

PartialSubscriptionFailureError reports that an org-wide fan-out completed with some subscriptions queried successfully and others not.

It is returned ALONGSIDE the successful subscriptions' recommendations, so a caller can keep the partial data and still know the sweep was incomplete. Callers that want the data must inspect the error:

recs, err := client.GetAllRecommendations(ctx)
var partial *azure.PartialSubscriptionFailureError
if errors.As(err, &partial) {
    // recs holds partial.Succeeded subscriptions' recommendations;
    // partial.Failed says which subscriptions are missing and why.
} else if err != nil {
    return err
}

This exists because the alternative -- returning the partial results with a nil error -- makes "these subscriptions have no savings available" indistinguishable from "these subscriptions were never successfully queried". On a collection path whose output is persisted and rendered as a savings opportunity, that reads as a shrinking opportunity rather than a failed sweep. A log line is not a programmatic signal; this is.

Failing the whole sweep on one transient subscription error would be worse than a partial result, which is why the successful data is still returned.

func AsPartialSubscriptionFailure

func AsPartialSubscriptionFailure(err error) *PartialSubscriptionFailureError

AsPartialSubscriptionFailure reports whether err is (or wraps) the org-wide fan-out's partial-failure signal, returning it when so and nil otherwise.

Provided so callers do not each hand-roll the errors.As dance, and -- more importantly -- so the "a partial sweep must not be treated as a total failure" rule is expressed the same way everywhere. A caller that skips this check and falls into a plain `if err != nil` discards the recommendations that WERE collected, turning one flaky subscription into a total collection outage, which is worse than the silent under-collection this error exists to prevent.

func (*PartialSubscriptionFailureError) Error

func (*PartialSubscriptionFailureError) FailedSubscriptionIDs

func (e *PartialSubscriptionFailureError) FailedSubscriptionIDs() []string

FailedSubscriptionIDs lists the subscriptions that could not be queried, for log lines and operator-facing messages.

func (*PartialSubscriptionFailureError) Unwrap

func (e *PartialSubscriptionFailureError) Unwrap() []error

Unwrap exposes the per-subscription causes so errors.Is/errors.As can match against any of them (e.g. checking whether a throttling error is in play).

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider implements the provider.Provider interface for Azure.

func NewAzureProvider

func NewAzureProvider(config *provider.ProviderConfig) (*Provider, error)

NewAzureProvider creates a new Azure provider instance.

Subscription resolution order:

  1. config.AzureSubscriptionID (typed field, preferred)
  2. config.Profile (deprecated overload — kept for backwards compatibility)

Credential resolution: if config.AzureTokenCredential is a non-nil azcore.TokenCredential, it is installed directly so all downstream clients use those credentials. Otherwise, GetCredentials lazily falls back to DefaultAzureCredential.

func (*Provider) DisplayName

func (p *Provider) DisplayName() string

DisplayName returns the human-readable provider name.

func (*Provider) GetAccounts

func (p *Provider) GetAccounts(ctx context.Context) ([]common.Account, error)

GetAccounts returns all accessible Azure subscriptions.

IsDefault is set to true for the subscription that matches (in priority order):

  1. The AzureSubscriptionID set in ProviderConfig (or the Profile fallback).
  2. The AZURE_SUBSCRIPTION_ID environment variable.
  3. The sole subscription, when exactly one is visible (mirrors AWS behavior where the STS-identified account is always the default).

func (*Provider) GetCredentials

func (p *Provider) GetCredentials() (provider.Credentials, error)

GetCredentials returns Azure credentials.

func (*Provider) GetDefaultRegion

func (p *Provider) GetDefaultRegion() string

GetDefaultRegion returns the default Azure region.

func (*Provider) GetRecommendationsClient

func (p *Provider) GetRecommendationsClient(ctx context.Context) (provider.RecommendationsClient, error)

GetRecommendationsClient returns a recommendations client.

When a subscription is pinned (p.subscriptionID set, e.g. by the scheduler or purchase-execution paths that always operate on one registered account), the returned client is scoped to that single subscription -- unchanged from previous behavior.

When no subscription is pinned, GetRecommendationsClient discovers every subscription accessible to the authenticated principal (via the cached getOrFetchAccounts) and then narrows in the same order the rest of the provider does, so widening the scope is never a side effect of adding fan-out:

  1. A default subscription resolvable from the discovered list -- the AZURE_SUBSCRIPTION_ID environment variable, or a lone visible subscription (see resolveDefaultSubscription) -- still scopes the client to that single subscription. Env-pinned callers keep the exact scope they had before org-wide fan-out existed; broadening them to every visible subscription would leak other subscriptions' data into a request that named one.
  2. A configured AZURE_SUBSCRIPTION_ID that names a subscription this principal cannot see is an error, not a request for org-wide coverage.
  3. Only when NO subscription was named at all -- an ambiguous multi-subscription principal with nothing configured, which previously produced the hard "multiple Azure subscriptions found; set AzureSubscriptionID or AZURE_SUBSCRIPTION_ID" error -- does the fan-out engage via MultiSubscriptionRecommendationsClient.

Azure has no organization-wide equivalent of AWS Cost Explorer's AccountScope=Linked -- the Consumption Reservation Recommendations and Advisor APIs are subscription-scoped -- so this client-side fan-out is what brings Azure to parity with the AWS provider's automatic whole-organization coverage.

func (*Provider) GetRecommendationsClientForAccount

func (p *Provider) GetRecommendationsClientForAccount(ctx context.Context, subscriptionID string) (provider.RecommendationsClient, error)

GetRecommendationsClientForAccount returns a recommendations client scoped to the given subscription ID. Use this when iterating over all subscriptions returned by GetAccounts to avoid O(n) redundant API calls.

func (*Provider) GetRegions

func (p *Provider) GetRegions(ctx context.Context) ([]common.Region, error)

GetRegions returns all available Azure regions using the Subscriptions API.

func (*Provider) GetServiceClient

func (p *Provider) GetServiceClient(ctx context.Context, service common.ServiceType, region string) (provider.ServiceClient, error)

GetServiceClient returns a service client for the specified service and region, using the default subscription.

When operating across multiple subscriptions (fan-out), prefer GetServiceClientForAccount: it accepts an explicit subscriptionID and avoids an extra GetAccounts round-trip per iteration.

func (*Provider) GetServiceClientForAccount

func (p *Provider) GetServiceClientForAccount(ctx context.Context, service common.ServiceType, region, subscriptionID string) (provider.ServiceClient, error)

GetServiceClientForAccount returns a service client for the specified service, region, and subscription ID. Use this when iterating over all subscriptions returned by GetAccounts to avoid O(n) redundant API calls.

func (*Provider) GetSupportedServices

func (p *Provider) GetSupportedServices() []common.ServiceType

GetSupportedServices returns the list of services supported by Azure provider.

func (*Provider) InvalidateAccountsCache

func (p *Provider) InvalidateAccountsCache()

InvalidateAccountsCache clears the cached subscription list so the next getOrFetchAccounts call re-fetches from the ARM subscriptions API. Exposed for tests that need to assert cache-miss behavior; production callers currently rely on the cache living for the lifetime of the Provider instance (one instance is constructed per collection/purchase run).

Bumping accountsGen is what makes this safe against a concurrent in-flight fetch: the generation both invalidates that fetch's right to publish its result and moves later callers onto a fresh singleflight key, so no caller can be served a snapshot taken before this call returned.

func (*Provider) IsConfigured

func (p *Provider) IsConfigured() bool

IsConfigured checks if Azure credentials are available. Thread-safe via sync.Once.

Sticky-failure contract: when ambient credential resolution fails (e.g. a transient IMDS timeout during startup), the error is memoised under sync.Once. Subsequent calls return false for the process lifetime with no retry. For long-lived server deployments this means a transient auth failure at boot requires a process restart to recover. This is acceptable for the current Lambda/container deployment model where restarts are cheap; if a long-lived daemon pattern is introduced, replace the sync.Once with a time-bounded cache or single-flight retry.

func (*Provider) Name

func (p *Provider) Name() string

Name returns the provider name.

func (*Provider) SetCredential

func (p *Provider) SetCredential(cred azcore.TokenCredential)

SetCredential sets the credential directly.

Also used in production (the scheduler and purchase-execution paths construct the provider and then install per-account federated credentials), so it must drop any cached subscription list: that cache is the set of subscriptions the PREVIOUS credential could see. Serving it to the new credential would report subscriptions this principal may have no access to, and -- via GetRecommendationsClient's fan-out -- fan out across them. Today every caller installs the credential before the first accounts fetch, so this is a guard against a future reordering rather than a live leak.

The credential is published under accountsMu, the same lock fetchAccounts snapshots it under: writing it outside the lock and only then taking the lock to invalidate leaves a window in which an in-flight fetch pairs the new credential with the old subscriptions client.

func (*Provider) SetCredentialProvider

func (p *Provider) SetCredentialProvider(credProvider CredentialProvider)

SetCredentialProvider sets the credential provider (for testing).

Published under accountsMu -- the same lock every other swappable field on the provider is published under -- because IsConfigured reads credProvider on its lazy ambient-credential path. Leaving this one field outside the lock would make that read an unsynchronized data race.

Unlike SetCredential it does not invalidate the accounts cache: credProvider only feeds IsConfigured's lazy resolution, which runs at most once and only when no credential was installed at all, so at the moment it is read there is no cached subscription list resolved under a different credential.

func (*Provider) SetSubscriptionsClient

func (p *Provider) SetSubscriptionsClient(client SubscriptionsClient)

SetSubscriptionsClient sets the subscriptions client (for testing).

Drops any cached subscription list: the cache holds what the PREVIOUS client returned, and serving that after the client is swapped would answer with a different source's subscriptions.

The swap and the invalidation happen under a single accountsMu write so a concurrent fetch can never observe the new client alongside the old cache generation -- see the accountsMu comment on Provider.

func (*Provider) ValidateCredentials

func (p *Provider) ValidateCredentials(ctx context.Context) error

ValidateCredentials validates that Azure credentials are working.

type RecommendationsClientAdapter

type RecommendationsClientAdapter struct {
	// contains filtered or unexported fields
}

RecommendationsClientAdapter aggregates Azure reservation recommendations across all services.

Invariant: subscriptionID must be non-empty. Downstream converters use it as the Recommendation.Account field; an empty subscriptionID would silently produce Account="" recommendations that downstream consumers (account-scoped caches, UI filters, billing reports) can't route. The canonical construction path is NewRecommendationsClientAdapter; direct struct literals bypass the invariant check and should be confined to tests that deliberately exercise the unvalidated shape.

getAdvisorRecsFn defaults to r.getAdvisorRecommendations and may be overridden per-instance in tests to avoid real ARM network calls.

func NewRecommendationsClientAdapter

func NewRecommendationsClientAdapter(cred azcore.TokenCredential, subscriptionID string) (*RecommendationsClientAdapter, error)

NewRecommendationsClientAdapter builds a RecommendationsClientAdapter with the subscriptionID-non-empty invariant enforced. Returns an error when subscriptionID is the empty string so the caller sees the mis-wiring at construction time rather than via confusing Account="" rows later.

func (*RecommendationsClientAdapter) GetAllRecommendations

func (r *RecommendationsClientAdapter) GetAllRecommendations(ctx context.Context) ([]common.Recommendation, error)

GetAllRecommendations retrieves all Azure reservation recommendations across all services.

func (*RecommendationsClientAdapter) GetRIUtilization

func (r *RecommendationsClientAdapter) GetRIUtilization(ctx context.Context, lookbackDays int) ([]common.RIUtilization, error)

GetRIUtilization fetches per-reservation utilization data from the Azure Consumption Reservations Summaries API for the subscription and returns it in the same shape as the AWS recommendations.Client.GetRIUtilization so callers can treat both providers uniformly.

lookbackDays controls the date window: [today - lookbackDays, today]. Values <= 0 default to 30 days, matching the AWS implementation.

The function uses monthly grain so the API returns one row per reservation per calendar month. Rows are accumulated by ReservationID across months and a single common.RIUtilization is returned per reservation with derived UtilizationPercent = (UsedHours / ReservedHours) * 100.

An empty result (no reservations in the subscription, or the subscription has no active reservations in the date range) returns (nil, nil).

func (*RecommendationsClientAdapter) GetRecommendations

GetRecommendations retrieves all Azure reservation recommendations across services.

The Azure Consumption Reservation Recommendations API is subscription-scoped: the response covers every region in one call. Iterating regions and calling each service per region (the previous behavior) produced ~60× duplicate results, hammered the rate limit, and meant downstream consumers had to deduplicate. We now call each service client exactly once. Region is intentionally left blank on the client — converters must populate Region from the response data (see known_issues/10_azure_provider.md CRITICAL "Recommendation converters ignore the API response entirely" for the matching converter work).

All six service calls run concurrently under errgroup. Each goroutine captures its own error and returns nil to the group so that a single service failure does not cancel sibling calls. Results are appended in a deterministic order (compute → database → cache → cosmosdb → savingsplans → advisor) after all goroutines finish.

func (*RecommendationsClientAdapter) GetRecommendationsForService

func (r *RecommendationsClientAdapter) GetRecommendationsForService(ctx context.Context, service common.ServiceType) ([]common.Recommendation, error)

GetRecommendationsForService retrieves Azure reservation recommendations for a specific service.

type SubscriptionFailure

type SubscriptionFailure struct {
	SubscriptionID string
	Err            error
}

SubscriptionFailure records one subscription that could not be queried during an org-wide fan-out.

type SubscriptionsClient

type SubscriptionsClient interface {
	NewListPager(options *armsubscriptions.ClientListOptions) SubscriptionsPager
	NewListLocationsPager(subscriptionID string, options *armsubscriptions.ClientListLocationsOptions) LocationsPager
}

SubscriptionsClient interface for subscription operations (enables mocking).

type SubscriptionsPager

type SubscriptionsPager interface {
	More() bool
	NextPage(ctx context.Context) (armsubscriptions.ClientListResponse, error)
}

SubscriptionsPager interface for subscription pagination (enables mocking).

Directories

Path Synopsis
internal
httpclient
Package httpclient provides a hardened HTTP client for Azure provider use.
Package httpclient provides a hardened HTTP client for Azure provider use.
pricing
Package pricing provides a shared NextPageLink-driven walker for the Azure Retail Prices API, used by every service client (compute, database, cache, cosmosdb).
Package pricing provides a shared NextPageLink-driven walker for the Azure Retail Prices API, used by every service client (compute, database, cache, cosmosdb).
recommendations
Package recommendations owns the shared extraction from Azure's consumption Reservation Recommendations API response into the fields every service converter (compute, database, cache, cosmosdb) needs to populate on common.Recommendation.
Package recommendations owns the shared extraction from Azure's consumption Reservation Recommendations API response into the fields every service converter (compute, database, cache, cosmosdb) needs to populate on common.Recommendation.
Package mocks provides mock implementations of Azure SDK clients for testing
Package mocks provides mock implementations of Azure SDK clients for testing
services
cache
Package cache provides Azure Cache for Redis Reserved Capacity client
Package cache provides Azure Cache for Redis Reserved Capacity client
compute
Package compute provides Azure VM Reserved Instances client
Package compute provides Azure VM Reserved Instances client
cosmosdb
Package cosmosdb provides Azure Cosmos DB Reserved Capacity client
Package cosmosdb provides Azure Cosmos DB Reserved Capacity client
database
Package database provides Azure SQL Database Reserved Capacity client
Package database provides Azure SQL Database Reserved Capacity client
internal/reservations
Package reservations provides shared helpers for Azure Reservations API operations.
Package reservations provides shared helpers for Azure Reservations API operations.
managedredis
Package managedredis provides Azure Managed Redis (Azure Cache for Redis) Reserved Capacity client.
Package managedredis provides Azure Managed Redis (Azure Cache for Redis) Reserved Capacity client.
savingsplans
Package savingsplans provides an Azure Savings Plans service client.
Package savingsplans provides an Azure Savings Plans service client.
search
Package search provides Azure Cognitive Search Reserved Capacity client
Package search provides Azure Cognitive Search Reserved Capacity client
synapse
Package synapse provides Azure Synapse Analytics Reserved Capacity client.
Package synapse provides Azure Synapse Analytics Reserved Capacity client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL