Documentation
¶
Overview ¶
Package azsweep is the Azure estate sweep's client (GitHub issue #1120, the Azure platform epic, under #1122's ruling of 2026-10-19): which live resources in a set of subscriptions carry this estate's owner tag, so that one whose block the configuration no longer declares can be proposed for removal, exactly as the AWS tagging sweep, the Kubernetes label list (pkg/kubesweep) and the GCP asset search (pkg/gcpsweep) do on their substrates.
The package is public and engine-neutral (GitHub issue #2281): the caller names the owner tag key (Config.Markers; choudoufu's is tofu-estate, which the examples below use) and hands over the credential, endpoints and HTTP client every call goes through. It builds no credential of its own.
What a sweep is here ¶
Azure has a cross-service, tag-filtered query: Azure Resource Graph (POST {resourceManager}/providers/Microsoft.ResourceGraph/resources, api-version 2022-10-01). One query per subscription, read page by page through $skipToken until the response carries none, returns every resource and resource group of every type that carries the tag. The query is Query; its comment says why it is shaped the way it is.
One subscription per query, never several in one request, is a correctness choice and not a convenience. Resource Graph answers a request naming several subscriptions from the ones the caller can read and leaves the rest out without saying so, so a subscription the credential cannot read would come back as one holding nothing tagged. Asked about that subscription alone, the service has to answer with an error (UnreadableSubscriptionError).
Case ¶
ARM tag keys are case-insensitive: ARM keeps the spelling the writer used and refuses a second key that differs from the first only by case. The marker is written lowercase and read case-insensitively (#1122), so a resource tagged Tofu-Estate by its own configuration is this estate's as surely as one tagged tofu-estate. Tag values are case-sensitive in ARM, and the estate name is compared exactly.
Failure is never an empty answer ¶
Every way a subscription's query can fail is an error, typed so that the discovery leg can name the fix: NotRegisteredError (the subscription has not registered Microsoft.ResourceGraph, a refusal), UnreadableSubscriptionError (the credential cannot read it, an incomplete sweep), ThrottledError (the service kept answering 429 past the retries its own headers allow, an incomplete sweep), PartialError (a page after the first failed, so the pages already read are discarded), and UncoveredSubscriptionError (no configured credential covers the subscription). Each carries the diagnostic summary a later unit registers in internal/live/discovery/refusals.go (Summary). A 200 with no rows is the one empty answer.
Credentials ¶
ProviderAttrs reads an azurerm or azapi provider block's evaluated configuration, and Attrs.WithEnv applies the ARM_* environment variables the provider itself reads for an argument the block leaves unset. The credential is the caller's: any azcore.TokenCredential in Config. choudoufu builds it with the azurerm state backend's chain (internal/live/sweepconfig), plunge with azidentity, and the tests hand it a fake token.
Index ¶
- Constants
- func EnvEnvironment(getenv func(string) string) string
- func EnvNames() []string
- func EnvSubscriptionID(getenv func(string) string) string
- func OwnerTag(keys markerkeys.Keys, tags map[string]string) (string, bool)
- func Query(keys markerkeys.Keys, estate string) string
- func Summary(err error) string
- func TypesQuery(keys markerkeys.Keys, estate string, armTypes []string) string
- type APIError
- type Attrs
- type Client
- type Config
- type Coverage
- type Credentials
- type Endpoints
- type KeyVaultClient
- type KeyVaultItem
- type NotRegisteredError
- type PartialError
- type Resource
- type Searcher
- type SubscriptionResult
- type Tagger
- type ThrottledError
- type TypeSearcher
- type UncoveredSubscriptionError
- type UnreadableSubscriptionError
Constants ¶
const ( ProviderAzureRM = "azurerm" ProviderAzAPI = "azapi" )
The two provider types the Azure family covers (#1122): one Attrs shape reads either block, since azapi's connection arguments are azurerm's with two exceptions, named on the fields they affect.
const ( MaxThrottleRetries = 4 MaxThrottleWait = 60 * time.Second )
MaxThrottleRetries is how many 429s one page's request waits out before the query is given up as ThrottledError. MaxThrottleWait is the longest single wait it will take; an answer asking for longer is given up at once rather than holding a plan open.
const ( // SummaryResourceGraphNotRegistered refuses the sweep: a subscription // the estate's resources resolve to has not registered the // Microsoft.ResourceGraph resource provider (#1122). SummaryResourceGraphNotRegistered = "Microsoft.ResourceGraph not registered" // SummarySubscriptionUnreadable warns of an incomplete sweep: the // credential cannot read a subscription, so its tagged resources are // unknown, never zero. SummarySubscriptionUnreadable = "Azure subscription not readable by the sweep" // SummaryResourceGraphThrottled refuses the sweep: Resource Graph kept // throttling past the retries its headers allow, and a plan made // without the subscription would propose no removal in it (#1120 // unit 11, internal/live/discovery's resourcegraph.go). SummaryResourceGraphThrottled = "Azure Resource Graph throttled the sweep" // SummarySubscriptionUncovered warns of an incomplete sweep: no // configured azurerm or azapi credential covers the subscription. SummarySubscriptionUncovered = "Azure subscription not covered by any provider block" // in a way none of the above names, or a later page failed. SummarySweepUnavailable = "Azure sweep unavailable" )
The diagnostic summaries a failed sweep is reported under, one per typed error, named so that the discovery leg that consumes this client can register each in internal/live/discovery/refusals.go (the refusalscan lockstep finds a Summary-prefixed constant in the package that emits the diagnostic) without retyping the words.
const APIVersion = "2022-10-01"
APIVersion is the Resource Graph API version every query is sent at.
const KeyVaultAPIVersion = "7.4"
KeyVaultAPIVersion is the Key Vault data-plane API version every listing is sent at.
const KeyVaultPageSize = 25
KeyVaultPageSize is the maxresults every listing asks for: the API's documented maximum.
const PageSize = 1000
PageSize is the $top every query asks for: the API's maximum.
const ResourceProvider = "Microsoft.ResourceGraph"
ResourceProvider is the resource provider namespace the sweep calls, which a subscription has to have registered.
const TagsAPIVersion = "2021-04-01"
TagsAPIVersion is the Microsoft.Resources api-version the post-create marker write is sent at: the version Tags - Update At Scope is documented at, and the one go-azure-sdk's resources/2021-04-01/tags client (which azurerm 5.10.0 vendors for its own tag writes) sends.
Variables ¶
This section is empty.
Functions ¶
func EnvEnvironment ¶
EnvEnvironment is the Azure cloud the environment gives an azurerm or azapi provider block that sets no `environment`, for internal/live/identity's CloudContext.Environment (GitHub issue #2206): ARM_ENVIRONMENT through getenv, and "public" when that is unset, the default both providers apply. "" when ARM_METADATA_HOSTNAME names a metadata host, because azurerm then takes its cloud from that host and not from either argument, and nothing here reads it.
func EnvNames ¶
func EnvNames() []string
EnvNames are every environment variable Attrs.WithEnv can read, so a test can clear them all.
func EnvSubscriptionID ¶
EnvSubscriptionID is the subscription the environment gives an azurerm or azapi provider block that sets no subscription_id: the one envDefaults entry both providers share, ARM_SUBSCRIPTION_ID (azurerm 5.10.0 internal/provider/provider.go:115-120, schema.EnvDefaultFunc; azapi 2.13.0 internal/provider/provider.go:363-368, Configure's IsNull fallback), looked up through getenv. It is Attrs.WithEnv's answer for a block with no subscription_id, so the sweep's subscription and the one internal/live/identity builds ARM ids under (its CloudContext.SubscriptionID, which the run fills from this) cannot read the environment differently (GitHub issue #2133, finding 4). "" when unset.
Neither provider reads any other variable for it. Both fall back further, to the Azure CLI's default subscription when use_cli is on (azurerm provider.go:449, AzureCliSubscriptionIDHint), which needs a process call and is not answered here.
func OwnerTag ¶
OwnerTag is the owner tag's value in tags (keys.Owner: choudoufu's tofu-estate), its key matched case-insensitively (#1122): ARM tag keys are case-insensitive, so Tofu-Estate is the marker as surely as tofu-estate is.
func Query ¶
func Query(keys markerkeys.Keys, estate string) string
Query is the KQL for estate's tag.
The plain form, `where tags['tofu-estate'] == 'prod'`, is wrong here: KQL's dynamic property access is case-sensitive, and ARM keeps a tag key in the casing its writer used, so a resource whose configuration set Tofu-Estate (#1122 keeps that spelling rather than add a second, lowercase key ARM would refuse) would never be found. So a copy of the tag bag is expanded one key per row (mv-expand over markerTag, leaving tags whole for the projection), the key is read with bag_keys, and compared with =~, KQL's case-insensitive equality. ARM refuses two keys that differ only by case, so a resource yields at most one row; the value is compared exactly, since ARM tag values are case-sensitive.
resourcecontainers is unioned in for resource groups, which the resources table does not hold and which an estate declares and tags like any other resource (azurerm_resource_group). Subscriptions and management groups are in resourcecontainers too and are left out: they are not objects an estate creates.
id is projected, which Resource Graph requires for $skipToken paging (without it the result is truncated at one page).
func Summary ¶
Summary is the diagnostic summary err is reported under: the summary of the first typed error in its chain, or SummarySweepUnavailable.
func TypesQuery ¶
func TypesQuery(keys markerkeys.Keys, estate string, armTypes []string) string
TypesQuery is Query narrowed to armTypes: the same estate-tag match, keeping only rows whose type is one of them. An empty armTypes is the plain Query.
Types ¶
type Attrs ¶
type Attrs struct {
// Provider is the provider type the block configures,
// [ProviderAzureRM] or [ProviderAzAPI]. It decides which environment
// variables apply ([Attrs.WithEnv]) and which endpoint arguments are
// read.
Provider string
// SubscriptionID is the subscription the block defaults to: the one a
// client covers when its caller names no other ([Client.Subscription]).
SubscriptionID string
TenantID string
ClientID string
ClientIDFilePath string
ClientSecret string
ClientSecretFilePath string
ClientCertificate string
ClientCertificatePath string
ClientCertificatePassword string
// UseCLI, UseMSI, UseOIDC and UseAKSWorkloadIdentity are the four
// switches. Nil is unset, which both providers default to true for
// use_cli and false for the rest.
UseCLI *bool
UseMSI *bool
UseOIDC *bool
UseAKSWorkloadIdentity *bool
// MSIEndpoint is azurerm's msi_endpoint; azapi has none.
MSIEndpoint string
OIDCToken string
OIDCTokenFilePath string
OIDCRequestURL string
OIDCRequestToken string
// ADOServiceConnectionID is azurerm's ado_pipeline_service_connection_id
// or azapi's oidc_azure_service_connection_id: the same Azure DevOps
// service connection under two names.
ADOServiceConnectionID string
// Environment is the cloud: public (the default), usgovernment or
// china.
Environment string
// MetadataHost is azurerm's metadata_host, a host serving
// /metadata/endpoints that names the cloud's endpoints in place of
// Environment. azapi has none.
MetadataHost string
// ResourceManagerEndpoint, ResourceManagerAudience and
// ActiveDirectoryAuthorityHost are azapi's endpoint block, each
// overriding Environment's value. azurerm has no endpoint block.
ResourceManagerEndpoint string
ResourceManagerAudience string
ActiveDirectoryAuthorityHost string
// DisableInstanceDiscovery is azapi's disable_instance_discovery (azapi
// 2.13.0 provider.go:566-576, ARM_DISABLE_INSTANCE_DISCOVERY when
// unset): the token request skips Microsoft's instance discovery
// service. azurerm has none; its go-azure-sdk credentials never ask.
DisableInstanceDiscovery *bool
// Marked names the arguments the block sets to a marked value that
// [ProviderAttrs] therefore left unread. [New] refuses a client when
// any is set ([Attrs.refusedMarks]).
Marked []string
}
Attrs are the azurerm or azapi provider block arguments the sweep understands, read off its evaluated configuration by ProviderAttrs. The names are the providers' own (hashicorp/azurerm 5.10.0 and azure/azapi 2.13.0 provider schemas); an empty string or a nil bool is an argument the block leaves unset.
func ProviderAttrs ¶
ProviderAttrs reads the arguments the Azure sweep understands off the evaluated azurerm or azapi provider block (providerType names which), following pkg/gcpsweep's ProviderAttrs: a marked value is left unread rather than unmarked, except for the arguments that are themselves the credential (client_secret, client_certificate and its password, oidc_token, oidc_request_token), which are unmarked and read. GitHub issue #1527 is what leaving a credential unread cost on Kubernetes: every ordinary way of supplying one marks it, and the sweep then called the API as someone other than the provider did. They go into a token request and out over TLS; nothing here renders them.
func (Attrs) ProviderName ¶
ProviderName is the block's provider, azurerm when unset: what errors about the block name it as.
func (Attrs) RefusedMarks ¶
RefusedMarks are the arguments a credential built from a is refused over: set from a marked value the caller did not read.
func (Attrs) WithEnv ¶
WithEnv fills every argument a left unset from the environment variable its provider reads for it, looked up through getenv (os.Getenv). An argument the block sets, or left unread because it was marked, is never replaced: the block wins, as it does in the provider.
A switch's variable is parsed with strconv.ParseBool, which is terraform-plugin-sdk's reading for azurerm; azapi compares with "true" alone, so "1" turns a switch on here that azapi would leave off. No switch here widens what the sweep can read beyond the credential the provider would also find.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is Searcher over the Resource Graph API, authenticated as one provider block's credential.
func New ¶
New builds a client over the caller's credential, endpoints and HTTP client (Config). No token is requested until Client.Search.
func (*Client) Credentials ¶
func (c *Client) Credentials() Credentials
Credentials is how this client authenticates.
func (*Client) Search ¶
Search reads every page of subscription's query for estate's tag. It returns all of them or none: a failure on any page is an error, typed when the API's answer is one this package names, and a PartialError wrapping it when it came after the first page. A 200 with no rows is an empty answer and a nil error.
Each row is checked again here: its own tofu-estate tag, the key in any case, must be exactly estate, and its subscriptionId must be the one asked about. A row failing either is dropped, so a query that matched too widely can never read another estate's resource as this one's. A row repeated (by id, case-insensitively) is kept once.
func (*Client) SearchTypes ¶
func (c *Client) SearchTypes(ctx context.Context, subscription, estate string, armTypes []string) ([]Resource, error)
SearchTypes implements TypeSearcher: Client.Search with TypesQuery, and every row checked again against armTypes as well as the estate.
func (*Client) Subscription ¶
Subscription is the provider block's subscription_id, the one subscription this client is set to query when the configuration names no other: what a Coverage built from provider blocks keys the client by.
type Config ¶
type Config struct {
// Markers names the owner tag key a search matches. Only [New] reads
// it.
Markers markerkeys.Keys
// Credential is what every token is requested from.
Credential azcore.TokenCredential
// Endpoints are the ARM base URL and token audience; the Key Vault
// listing reads neither, since a vault's URL names its own cloud.
Endpoints Endpoints
// HTTPClient carries every call; nil is http.DefaultClient.
HTTPClient *http.Client
// Credentials is what diagnostics name the credential as.
Credentials Credentials
// Subscription is the provider block's subscription_id, the one a
// [Coverage] built from provider blocks keys the client by.
Subscription string
}
Config is what the caller supplies a Client, Tagger or KeyVaultClient (GitHub issue #2281): the marker vocabulary, the credential, where it calls and the HTTP client. The package builds no credential of its own and reads no environment to find one; choudoufu passes what OpenTofu's azurerm backend credential chain builds, plunge an azidentity credential.
type Coverage ¶
Coverage is which client queries which subscription: the subscription id a provider block names, keyed to the client its credential built. Keys are compared case-insensitively.
type Credentials ¶
type Credentials struct {
// Method is the credential chain's choice, in its own words: "Client
// Secret Auth", "Azure CLI Auth", "Managed Service Identity Auth".
// Empty when the caller names none ([Config.Credentials]).
Method string
// TenantID and ClientID are the block's, when it names them.
TenantID string
ClientID string
}
Credentials is how a client was told to authenticate, so that a failure can be reported against what was actually used. It never holds the credential itself.
func (Credentials) String ¶
func (c Credentials) String() string
String is the one line a diagnostic names the credential with.
type Endpoints ¶
type Endpoints struct {
// ResourceManager is the ARM base URL: https://management.azure.com/
// for the public cloud.
ResourceManager string
// Audience is the resource a token is requested for; the scope asked
// for is Audience + "/.default".
Audience string
}
Endpoints are where a client sends its query and what it asks a token for.
type KeyVaultClient ¶
type KeyVaultClient struct {
// contains filtered or unexported fields
}
KeyVaultClient lists the objects of a vault, authenticated as one provider block's credential.
func NewKeyVault ¶
func NewKeyVault(cfg Config) *KeyVaultClient
NewKeyVault builds a client over the caller's credential and HTTP client (Config; its Markers and Endpoints are not read, since a vault's URL names its own cloud).
func (*KeyVaultClient) List ¶
func (c *KeyVaultClient) List(ctx context.Context, baseURL, collection string) ([]KeyVaultItem, error)
List returns every object in the vault's collection (keys, secrets or certificates), all pages read, or an error and no items. A key or secret the vault manages for a certificate is left out: it is the certificate's, listed once as the certificate.
type KeyVaultItem ¶
KeyVaultItem is one listed object. ID is the identifier the listing returns (the "kid" of a key, the "id" of a secret or certificate), which names the object without a version: the versioned id azurerm imports by is not derived here (it waits on GitHub issue #2202).
type NotRegisteredError ¶
type NotRegisteredError struct {
Subscription string
// Code is ARM's error code, one of [unregisteredCodes].
Code string
Message string
}
NotRegisteredError is a subscription that has not registered Microsoft.ResourceGraph. The fix is registering it (`az provider register --namespace Microsoft.ResourceGraph`), never a retry and never a grant, and the sweep is refused until then: a sweep that skipped the subscription would hide every orphan in it.
func (*NotRegisteredError) Error ¶
func (e *NotRegisteredError) Error() string
type PartialError ¶
PartialError is a query that read at least one page and then failed: the pages already read are discarded rather than returned, for pkg/gcpsweep's PartialError reason (a missed page is an orphan nobody is told about). Err may itself be one of the typed errors above.
func (*PartialError) Error ¶
func (e *PartialError) Error() string
func (*PartialError) Unwrap ¶
func (e *PartialError) Unwrap() error
type Resource ¶
type Resource struct {
// ID is the ARM id, in the casing Resource Graph returns it. ARM ids
// are case-insensitive; compare them with strings.EqualFold.
ID string
// Type is the ARM type, which Resource Graph lowercases:
// "microsoft.network/virtualnetworks".
Type string
Name string
ResourceGroup string
SubscriptionID string
Location string
// Tags are the resource's tags as carried, the estate's among them
// under whatever casing its writer used.
Tags map[string]string
// ManagedBy is ARM's managedBy: the id of the resource that manages
// this one (an AKS cluster on its node resource group, a managed
// application on its resources), or "" when nothing does. Set, the
// resource is controller-held evidence (#1122) for the family to
// judge; this client does not exclude it.
ManagedBy string
}
Resource is one resource or resource group Resource Graph holds with this estate's tag, as the query projected it.
Resource Graph is eventually consistent, the Azure counterpart of the AWS tagging index's lag: a resource deleted moments ago may still be listed, with its tags as they were, which is why the family answers MarkerOutlivesObject true (#1122). A listed Resource is one the index last saw carrying the tag, not proof it exists now; a caller proposing a removal reads the resource itself before acting.
type Searcher ¶
type Searcher interface {
// Search returns every resource in subscription carrying the tag
// tofu-estate=estate (the key in any case), all pages read, or an
// error and no resources.
Search(ctx context.Context, subscription, estate string) ([]Resource, error)
}
Searcher is what a sweep asks of one credential's client, so a test can stand in for the API.
type SubscriptionResult ¶
SubscriptionResult is one subscription's sweep: its resources, or the error that stopped it and no resources. Resources nil with Err nil is a subscription that holds nothing tagged for the estate.
func Sweep ¶
func Sweep(ctx context.Context, cov Coverage, subscriptions []string, estate string) []SubscriptionResult
Sweep queries each of subscriptions for estate's tag with the client cov assigns it, in subscriptions' order, each subscription once (subscription ids compared case-insensitively). A subscription cov has no client for is an UncoveredSubscriptionError result, never a missing row. A failed subscription does not stop the others: each result stands alone, so the discovery leg can warn of the one that failed, refuse on NotRegisteredError, and still read the ones that answered.
type Tagger ¶
type Tagger struct {
// contains filtered or unexported fields
}
Tagger is the Azure family's post-create marker write (#1120 unit 12, substrate.WriteARMTagsAPI): a merge of tags onto one ARM scope through the Tags API, authenticated as one provider block's credential. It lives beside the sweep client because it shares that client's credential chain, endpoints and error reading, as kubesweep's LabelPatcher shares the cluster client.
func NewTagger ¶
NewTagger builds a Tagger over the caller's credential, endpoints and HTTP client (Config; its Markers are not read).
func (*Tagger) MergeTags ¶
MergeTags merges tags onto the resource, resource group or subscription whose ARM id is id:
PATCH {resourceManager}{id}/providers/Microsoft.Resources/tags/default?api-version=2021-04-01
{"operation": "Merge", "properties": {"tags": {...}}}
Merge replaces the value of a key the object already carries and adds the rest; every other tag on the object is left as it is. ARM compares tag keys case-insensitively, so a key the object already carries in another spelling is updated in place under that spelling rather than added beside it. Any answer other than 2xx is an error naming the status, the ARM error code and message, and the credential the call was made as.
type ThrottledError ¶
type ThrottledError struct {
Subscription string
Attempts int
// RetryAfter is the last wait the service asked for.
RetryAfter time.Duration
}
ThrottledError is a query Resource Graph still answered 429 after MaxThrottleRetries waits, or asked for a wait longer than MaxThrottleWait.
func (*ThrottledError) Error ¶
func (e *ThrottledError) Error() string
type TypeSearcher ¶
type TypeSearcher interface {
SearchTypes(ctx context.Context, subscription, estate string, armTypes []string) ([]Resource, error)
}
TypeSearcher is Searcher narrowed by ARM type: every resource in subscription carrying the estate's tag whose type is one of armTypes (compared case-insensitively). live-plan -resource asks it for the cone's types alone, where the estate sweep asks Searcher.Search for everything (GitHub issue #2260).
type UncoveredSubscriptionError ¶
type UncoveredSubscriptionError struct {
Subscription string
}
UncoveredSubscriptionError is a subscription the sweep was asked to query that no client in its Coverage is set to query.
func (*UncoveredSubscriptionError) Error ¶
func (e *UncoveredSubscriptionError) Error() string
type UnreadableSubscriptionError ¶
type UnreadableSubscriptionError struct {
Subscription string
Credentials Credentials
Status int
Code string
Message string
}
UnreadableSubscriptionError is a subscription the credential cannot query: no role assignment gives it read on the subscription, the subscription does not exist, or it belongs to a tenant the token was not issued for. The sweep of that subscription is incomplete, and is warned about by name rather than read as empty.
func (*UnreadableSubscriptionError) Error ¶
func (e *UnreadableSubscriptionError) Error() string
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package azsweeptest is a fake Azure sweep for tests in any module (GitHub issue #2281): an in-memory index that answers azsweep.Searcher and azsweep.TypeSearcher by the same tag match the real client makes (the owner key in any case, the value exactly), under the caller's marker vocabulary, and records what it was asked.
|
Package azsweeptest is a fake Azure sweep for tests in any module (GitHub issue #2281): an in-memory index that answers azsweep.Searcher and azsweep.TypeSearcher by the same tag match the real client makes (the owner key in any case, the value exactly), under the caller's marker vocabulary, and records what it was asked. |