internal/provider/provider.go (831 lines of code) (raw):
package provider
import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
"strings"
"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"
"github.com/Azure/terraform-provider-azapi/internal/azure"
"github.com/Azure/terraform-provider-azapi/internal/azure/location"
"github.com/Azure/terraform-provider-azapi/internal/azure/tags"
"github.com/Azure/terraform-provider-azapi/internal/clients"
"github.com/Azure/terraform-provider-azapi/internal/features"
"github.com/Azure/terraform-provider-azapi/internal/services"
"github.com/Azure/terraform-provider-azapi/internal/services/functions"
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
"github.com/Azure/terraform-provider-azapi/version"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
var _ provider.Provider = &Provider{}
var _ provider.ProviderWithFunctions = &Provider{}
var _ provider.ProviderWithEphemeralResources = &Provider{}
func AzureProvider() provider.Provider {
return &Provider{}
}
type Provider struct {
}
type providerData struct {
SubscriptionID types.String `tfsdk:"subscription_id"`
ClientID types.String `tfsdk:"client_id"`
ClientIDFilePath types.String `tfsdk:"client_id_file_path"`
TenantID types.String `tfsdk:"tenant_id"`
AuxiliaryTenantIDs types.List `tfsdk:"auxiliary_tenant_ids"`
Endpoint types.List `tfsdk:"endpoint"`
Environment types.String `tfsdk:"environment"`
ClientCertificate types.String `tfsdk:"client_certificate"`
ClientCertificatePath types.String `tfsdk:"client_certificate_path"`
ClientCertificatePassword types.String `tfsdk:"client_certificate_password"`
ClientSecret types.String `tfsdk:"client_secret"`
ClientSecretFilePath types.String `tfsdk:"client_secret_file_path"`
SkipProviderRegistration types.Bool `tfsdk:"skip_provider_registration"`
OIDCRequestToken types.String `tfsdk:"oidc_request_token"`
OIDCRequestURL types.String `tfsdk:"oidc_request_url"`
OIDCToken types.String `tfsdk:"oidc_token"`
OIDCTokenFilePath types.String `tfsdk:"oidc_token_file_path"`
OIDCAzureServiceConnectionID types.String `tfsdk:"oidc_azure_service_connection_id"`
UseOIDC types.Bool `tfsdk:"use_oidc"`
UseCLI types.Bool `tfsdk:"use_cli"`
UseMSI types.Bool `tfsdk:"use_msi"`
UseAKSWorkloadIdentity types.Bool `tfsdk:"use_aks_workload_identity"`
PartnerID types.String `tfsdk:"partner_id"`
CustomCorrelationRequestID types.String `tfsdk:"custom_correlation_request_id"`
DisableCorrelationRequestID types.Bool `tfsdk:"disable_correlation_request_id"`
DisableTerraformPartnerID types.Bool `tfsdk:"disable_terraform_partner_id"`
DefaultName types.String `tfsdk:"default_name"`
DefaultLocation types.String `tfsdk:"default_location"`
DefaultTags types.Map `tfsdk:"default_tags"`
EnablePreflight types.Bool `tfsdk:"enable_preflight"`
DisableDefaultOutput types.Bool `tfsdk:"disable_default_output"`
MaximumBusyRetryAttempts types.Int32 `tfsdk:"maximum_busy_retry_attempts"`
}
func (model providerData) GetClientId() (*string, error) {
clientId := strings.TrimSpace(model.ClientID.ValueString())
if path := model.ClientIDFilePath.ValueString(); path != "" {
// #nosec G304
fileClientIdRaw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading Client ID from file %q: %v", path, err)
}
fileClientId := strings.TrimSpace(string(fileClientIdRaw))
if clientId != "" && clientId != fileClientId {
return nil, fmt.Errorf("mismatch between supplied Client ID and supplied Client ID file contents - please either remove one or ensure they match")
}
clientId = fileClientId
}
if model.UseAKSWorkloadIdentity.ValueBool() && os.Getenv("AZURE_CLIENT_ID") != "" {
aksClientId := os.Getenv("AZURE_CLIENT_ID")
if clientId != "" && clientId != aksClientId {
return nil, fmt.Errorf("mismatch between supplied Client ID and that provided by AKS Workload Identity - please remove, ensure they match, or disable use_aks_workload_identity")
}
clientId = aksClientId
}
return &clientId, nil
}
func (model providerData) GetClientSecret() (*string, error) {
clientSecret := strings.TrimSpace(model.ClientSecret.ValueString())
if path := model.ClientSecretFilePath.ValueString(); path != "" {
// #nosec G304
fileSecretRaw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading Client Secret from file %q: %v", path, err)
}
fileSecret := strings.TrimSpace(string(fileSecretRaw))
if clientSecret != "" && clientSecret != fileSecret {
return nil, fmt.Errorf("mismatch between supplied Client Secret and supplied Client Secret file contents - please either remove one or ensure they match")
}
clientSecret = fileSecret
}
return &clientSecret, nil
}
func (model providerData) GetOIDCTokenFilePath() string {
if !model.OIDCTokenFilePath.IsNull() && model.OIDCTokenFilePath.ValueString() != "" {
return model.OIDCTokenFilePath.ValueString()
}
if model.UseAKSWorkloadIdentity.ValueBool() && os.Getenv("AZURE_FEDERATED_TOKEN_FILE") != "" {
return os.Getenv("AZURE_FEDERATED_TOKEN_FILE")
}
return ""
}
type providerEndpointData struct {
ActiveDirectoryAuthorityHost types.String `tfsdk:"active_directory_authority_host"`
ResourceManagerEndpoint types.String `tfsdk:"resource_manager_endpoint"`
ResourceManagerAudience types.String `tfsdk:"resource_manager_audience"`
}
func (p Provider) Metadata(ctx context.Context, request provider.MetadataRequest, response *provider.MetadataResponse) {
response.TypeName = "azapi"
}
func (p Provider) Schema(ctx context.Context, request provider.SchemaRequest, response *provider.SchemaResponse) {
response.Schema = schema.Schema{
Description: "The Azure API Provider",
Attributes: map[string]schema.Attribute{
"subscription_id": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The Subscription ID which should be used. This can also be sourced from the `ARM_SUBSCRIPTION_ID` Environment Variable.",
},
"client_id": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The Client ID which should be used. This can also be sourced from the `ARM_CLIENT_ID` Environment Variable.",
},
"client_id_file_path": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The path to a file containing the Client ID which should be used. This can also be sourced from the `ARM_CLIENT_ID_FILE_PATH` Environment Variable.",
},
"tenant_id": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The Tenant ID should be used. This can also be sourced from the `ARM_TENANT_ID` Environment Variable.",
},
"auxiliary_tenant_ids": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
Validators: []validator.List{listvalidator.SizeAtMost(3)},
MarkdownDescription: "List of auxiliary Tenant IDs required for multi-tenancy and cross-tenant scenarios. This can also be sourced from the `ARM_AUXILIARY_TENANT_IDS` Environment Variable.",
},
"endpoint": schema.ListNestedAttribute{
Optional: true,
Validators: []validator.List{listvalidator.SizeAtMost(1)},
MarkdownDescription: "The Azure API Endpoint Configuration.",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"active_directory_authority_host": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The Azure Resource Manager endpoint to use. This can also be sourced from the `ARM_RESOURCE_MANAGER_ENDPOINT` Environment Variable. Defaults to `https://management.azure.com/` for public cloud.",
},
"resource_manager_endpoint": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The resource ID to obtain AD tokens for. This can also be sourced from the `ARM_RESOURCE_MANAGER_AUDIENCE` Environment Variable. Defaults to `https://management.core.windows.net/` for public cloud.",
},
"resource_manager_audience": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The Azure Active Directory login endpoint to use. This can also be sourced from the `ARM_ACTIVE_DIRECTORY_AUTHORITY_HOST` Environment Variable. Defaults to `https://login.microsoftonline.com/` for public cloud.",
},
},
},
},
"environment": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.OneOfCaseInsensitive("public", "usgovernment", "china"),
},
MarkdownDescription: "The Cloud Environment which should be used. Possible values are `public`, `usgovernment` and `china`. Defaults to `public`. This can also be sourced from the `ARM_ENVIRONMENT` Environment Variable.",
},
// TODO@mgd: the metadata_host is used to retrieve metadata from Azure to identify current environment, this is used to eliminate Azure Stack usage, in which case the provider doesn't support.
// "metadata_host": {
// Type: schema.TypeString,
// Required: true,
// DefaultFunc: schema.EnvDefaultFunc("ARM_METADATA_HOSTNAME", ""),
// Description: "The Hostname which should be used for the Azure Metadata Service.",
// },
// Client Certificate specific fields
"client_certificate_path": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The path to the Client Certificate associated with the Service Principal which should be used. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PATH` Environment Variable.",
},
"client_certificate": schema.StringAttribute{
Optional: true,
MarkdownDescription: "A base64-encoded PKCS#12 bundle to be used as the client certificate for authentication. This can also be sourced from the `ARM_CLIENT_CERTIFICATE` environment variable.",
},
"client_certificate_password": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The password associated with the Client Certificate. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PASSWORD` Environment Variable.",
},
// Client Secret specific fields
"client_secret": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The Client Secret which should be used. This can also be sourced from the `ARM_CLIENT_SECRET` Environment Variable.",
},
"client_secret_file_path": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The path to a file containing the Client Secret which should be used. For use When authenticating as a Service Principal using a Client Secret. This can also be sourced from the `ARM_CLIENT_SECRET_FILE_PATH` Environment Variable.",
},
"skip_provider_registration": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Should the Provider skip registering the Resource Providers it supports? This can also be sourced from the `ARM_SKIP_PROVIDER_REGISTRATION` Environment Variable. Defaults to `false`.",
},
// OIDC specific fields
"oidc_request_token": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The bearer token for the request to the OIDC provider. This can also be sourced from the `ARM_OIDC_REQUEST_TOKEN`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, or `SYSTEM_ACCESSTOKEN` Environment Variables.",
},
"oidc_request_url": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The URL for the OIDC provider from which to request an ID token. This can also be sourced from the `ARM_OIDC_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_URL`, or `SYSTEM_OIDCREQUESTURI` Environment Variables.",
},
"oidc_token": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The ID token when authenticating using OpenID Connect (OIDC). This can also be sourced from the `ARM_OIDC_TOKEN` environment Variable.",
},
"oidc_token_file_path": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The path to a file containing an ID token when authenticating using OpenID Connect (OIDC). This can also be sourced from the `ARM_OIDC_TOKEN_FILE_PATH` environment Variable.",
},
"oidc_azure_service_connection_id": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The Azure Pipelines Service Connection ID to use for authentication. This can also be sourced from the `ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID`, `ARM_OIDC_AZURE_SERVICE_CONNECTION_ID`, or `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` Environment Variables.",
},
"use_oidc": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Should OIDC be used for Authentication? This can also be sourced from the `ARM_USE_OIDC` Environment Variable. Defaults to `false`.",
},
// Azure CLI specific fields
"use_cli": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Should Azure CLI be used for authentication? This can also be sourced from the `ARM_USE_CLI` environment variable. Defaults to `true`.",
},
// Managed Service Identity specific fields
"use_msi": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Should Managed Identity be used for Authentication? This can also be sourced from the `ARM_USE_MSI` Environment Variable. Defaults to `false`.",
},
"use_aks_workload_identity": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Should AKS Workload Identity be used for Authentication? This can also be sourced from the `ARM_USE_AKS_WORKLOAD_IDENTITY` Environment Variable. Defaults to `false`. When set, `client_id`, `tenant_id` and `oidc_token_file_path` will be detected from the environment and do not need to be specified.",
},
// TODO@mgd: azidentity doesn't support msi_endpoint
// "msi_endpoint": {
// Type: schema.TypeString,
// Optional: true,
// DefaultFunc: schema.EnvDefaultFunc("ARM_MSI_ENDPOINT", ""),
// Description: "The path to a custom endpoint for Managed Service Identity - in most circumstances this should be detected automatically. ",
// },
// Managed Tracking GUID for User-agent
"partner_id": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.Any(myvalidator.StringIsUUID(), myvalidator.StringIsEmpty()),
},
MarkdownDescription: "A GUID/UUID that is [registered](https://docs.microsoft.com/azure/marketplace/azure-partner-customer-usage-attribution#register-guids-and-offers) with Microsoft to facilitate partner resource usage attribution. This can also be sourced from the `ARM_PARTNER_ID` Environment Variable.",
},
"custom_correlation_request_id": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The value of the `x-ms-correlation-request-id` header, otherwise an auto-generated UUID will be used. This can also be sourced from the `ARM_CORRELATION_REQUEST_ID` environment variable.",
},
"disable_correlation_request_id": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "This will disable the x-ms-correlation-request-id header.",
},
"disable_terraform_partner_id": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Disable sending the Terraform Partner ID if a custom `partner_id` isn't specified, which allows Microsoft to better understand the usage of Terraform. The Partner ID does not give HashiCorp any direct access to usage information. This can also be sourced from the `ARM_DISABLE_TERRAFORM_PARTNER_ID` environment variable. Defaults to `false`.",
},
"default_name": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The default name to create the azure resource. The `name` in each resource block can override the `default_name`. Changing this forces new resources to be created.",
},
"default_location": schema.StringAttribute{
Optional: true,
MarkdownDescription: " The default Azure Region where the azure resource should exist. The `location` in each resource block can override the `default_location`. Changing this forces new resources to be created.",
},
"default_tags": schema.MapAttribute{
Optional: true,
ElementType: types.StringType,
Validators: []validator.Map{
tags.Validator(),
},
MarkdownDescription: "A mapping of tags which should be assigned to the azure resource as default tags. The`tags` in each resource block can override the `default_tags`.",
},
"enable_preflight": schema.BoolAttribute{
Optional: true,
Description: "Enable Preflight Validation. The default is false. When set to true, the provider will use Preflight to do static validation before really deploying a new resource. When set to false, the provider will disable this validation. This can also be sourced from the `ARM_ENABLE_PREFLIGHT` Environment Variable.",
},
"disable_default_output": schema.BoolAttribute{
Optional: true,
Description: "Disable default output. The default is false. When set to false, the provider will output the read-only properties if `response_export_values` is not specified in the resource block. When set to true, the provider will disable this output. This can also be sourced from the `ARM_DISABLE_DEFAULT_OUTPUT` Environment Variable.",
},
"maximum_busy_retry_attempts": schema.Int32Attribute{
Optional: true,
MarkdownDescription: "The maximum number of retries to attempt if the Azure API returns an HTTP 408, 429, 500, 502, 503, or 504 response. The default is `3`. The resource-specific retry configuration may additionally be used to retry on other errors and conditions.",
},
},
}
}
func (p Provider) Configure(ctx context.Context, request provider.ConfigureRequest, response *provider.ConfigureResponse) {
var model providerData
if response.Diagnostics.Append(request.Config.Get(ctx, &model)...); response.Diagnostics.HasError() {
return
}
// set the defaults from environment variables
if model.SubscriptionID.IsNull() {
if v := os.Getenv("ARM_SUBSCRIPTION_ID"); v != "" {
model.SubscriptionID = types.StringValue(v)
}
}
if model.ClientID.IsNull() {
if v := os.Getenv("ARM_CLIENT_ID"); v != "" {
model.ClientID = types.StringValue(v)
}
}
if model.ClientIDFilePath.IsNull() {
if v := os.Getenv("ARM_CLIENT_ID_FILE_PATH"); v != "" {
model.ClientIDFilePath = types.StringValue(v)
}
}
if model.UseAKSWorkloadIdentity.IsNull() {
if v := os.Getenv("ARM_USE_AKS_WORKLOAD_IDENTITY"); v != "" {
model.UseAKSWorkloadIdentity = types.BoolValue(v == "true")
} else {
model.UseAKSWorkloadIdentity = types.BoolValue(false)
}
}
if model.TenantID.IsNull() {
if v := os.Getenv("ARM_TENANT_ID"); v != "" {
model.TenantID = types.StringValue(v)
}
if model.UseAKSWorkloadIdentity.ValueBool() && os.Getenv("AZURE_TENANT_ID") != "" {
aksTenantID := os.Getenv("AZURE_TENANT_ID")
if model.TenantID.ValueString() != "" && model.TenantID.ValueString() != aksTenantID {
response.Diagnostics.AddError("Invalid `tenant_id` value", "mismatch between supplied Tenant ID and that provided by AKS Workload Identity - please remove, ensure they match, or disable use_aks_workload_identity")
return
}
model.TenantID = types.StringValue(aksTenantID)
}
}
if model.Endpoint.IsNull() {
activeDirectoryAuthorityHost := os.Getenv("ARM_ACTIVE_DIRECTORY_AUTHORITY_HOST")
resourceManagerEndpoint := os.Getenv("ARM_RESOURCE_MANAGER_ENDPOINT")
resourceManagerAudience := os.Getenv("ARM_RESOURCE_MANAGER_AUDIENCE")
attrTypes := make(map[string]attr.Type)
attrTypes["active_directory_authority_host"] = types.StringType
attrTypes["resource_manager_endpoint"] = types.StringType
attrTypes["resource_manager_audience"] = types.StringType
model.Endpoint = types.ListValueMust(types.ObjectType{
AttrTypes: attrTypes,
}, []attr.Value{
types.ObjectValueMust(attrTypes, map[string]attr.Value{
"active_directory_authority_host": types.StringValue(activeDirectoryAuthorityHost),
"resource_manager_endpoint": types.StringValue(resourceManagerEndpoint),
"resource_manager_audience": types.StringValue(resourceManagerAudience),
}),
})
}
if model.Environment.IsNull() {
if v := os.Getenv("ARM_ENVIRONMENT"); v != "" {
model.Environment = types.StringValue(v)
} else {
model.Environment = types.StringValue("public")
}
}
if model.AuxiliaryTenantIDs.IsNull() {
if v := os.Getenv("ARM_AUXILIARY_TENANT_IDS"); v != "" {
values := make([]attr.Value, 0)
for _, v := range strings.Split(v, ";") {
values = append(values, types.StringValue(v))
}
model.AuxiliaryTenantIDs = types.ListValueMust(types.StringType, values)
}
}
if model.ClientCertificate.IsNull() {
if v := os.Getenv("ARM_CLIENT_CERTIFICATE"); v != "" {
model.ClientCertificate = types.StringValue(v)
}
}
if model.ClientCertificatePath.IsNull() {
if v := os.Getenv("ARM_CLIENT_CERTIFICATE_PATH"); v != "" {
model.ClientCertificatePath = types.StringValue(v)
}
}
if model.ClientCertificatePassword.IsNull() {
if v := os.Getenv("ARM_CLIENT_CERTIFICATE_PASSWORD"); v != "" {
model.ClientCertificatePassword = types.StringValue(v)
}
}
if model.ClientSecret.IsNull() {
if v := os.Getenv("ARM_CLIENT_SECRET"); v != "" {
model.ClientSecret = types.StringValue(v)
}
}
if model.ClientSecretFilePath.IsNull() {
if v := os.Getenv("ARM_CLIENT_SECRET_FILE_PATH"); v != "" {
model.ClientSecretFilePath = types.StringValue(v)
}
}
if model.SkipProviderRegistration.IsNull() {
if v := os.Getenv("ARM_SKIP_PROVIDER_REGISTRATION"); v != "" {
model.SkipProviderRegistration = types.BoolValue(v == "true")
} else {
model.SkipProviderRegistration = types.BoolValue(false)
}
}
if model.OIDCRequestToken.IsNull() {
if v := os.Getenv("ARM_OIDC_REQUEST_TOKEN"); v != "" {
model.OIDCRequestToken = types.StringValue(v)
} else if v := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"); v != "" {
model.OIDCRequestToken = types.StringValue(v)
} else if v := os.Getenv("SYSTEM_ACCESSTOKEN"); v != "" {
model.OIDCRequestToken = types.StringValue(v)
}
}
if model.OIDCRequestURL.IsNull() {
if v := os.Getenv("ARM_OIDC_REQUEST_URL"); v != "" {
model.OIDCRequestURL = types.StringValue(v)
} else if v := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL"); v != "" {
model.OIDCRequestURL = types.StringValue(v)
}
}
if model.OIDCToken.IsNull() {
if v := os.Getenv("ARM_OIDC_TOKEN"); v != "" {
model.OIDCToken = types.StringValue(v)
}
}
if model.OIDCTokenFilePath.IsNull() {
if v := os.Getenv("ARM_OIDC_TOKEN_FILE_PATH"); v != "" {
model.OIDCTokenFilePath = types.StringValue(v)
}
}
if model.OIDCAzureServiceConnectionID.IsNull() {
if v := os.Getenv("ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID"); v != "" {
model.OIDCAzureServiceConnectionID = types.StringValue(v)
} else if v := os.Getenv("ARM_OIDC_AZURE_SERVICE_CONNECTION_ID"); v != "" {
model.OIDCAzureServiceConnectionID = types.StringValue(v)
} else if v := os.Getenv("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID"); v != "" {
model.OIDCAzureServiceConnectionID = types.StringValue(v)
}
}
if model.UseOIDC.IsNull() {
if v := os.Getenv("ARM_USE_OIDC"); v != "" {
model.UseOIDC = types.BoolValue(v == "true")
} else {
model.UseOIDC = types.BoolValue(false)
}
}
if model.UseCLI.IsNull() {
if v := os.Getenv("ARM_USE_CLI"); v != "" {
model.UseCLI = types.BoolValue(v == "true")
} else {
model.UseCLI = types.BoolValue(true)
}
}
if model.UseMSI.IsNull() {
if v := os.Getenv("ARM_USE_MSI"); v != "" {
model.UseMSI = types.BoolValue(v == "true")
} else {
model.UseMSI = types.BoolValue(false)
}
}
if model.PartnerID.IsNull() {
if v := os.Getenv("ARM_PARTNER_ID"); v != "" {
model.PartnerID = types.StringValue(v)
}
}
if model.CustomCorrelationRequestID.IsNull() {
if v := os.Getenv("ARM_CORRELATION_REQUEST_ID"); v != "" {
model.CustomCorrelationRequestID = types.StringValue(v)
}
}
if model.DisableCorrelationRequestID.IsNull() {
if v := os.Getenv("ARM_DISABLE_CORRELATION_REQUEST_ID"); v != "" {
model.DisableCorrelationRequestID = types.BoolValue(v == "true")
} else {
model.DisableCorrelationRequestID = types.BoolValue(false)
}
}
if model.DisableTerraformPartnerID.IsNull() {
if v := os.Getenv("ARM_DISABLE_TERRAFORM_PARTNER_ID"); v != "" {
model.DisableTerraformPartnerID = types.BoolValue(v == "true")
} else {
model.DisableTerraformPartnerID = types.BoolValue(false)
}
}
if model.EnablePreflight.IsNull() {
if v := os.Getenv("ARM_ENABLE_PREFLIGHT"); v != "" {
model.EnablePreflight = types.BoolValue(v == "true")
} else {
model.EnablePreflight = types.BoolValue(false)
}
}
if model.DisableDefaultOutput.IsNull() {
if v := os.Getenv("ARM_DISABLE_DEFAULT_OUTPUT"); v != "" {
model.DisableDefaultOutput = types.BoolValue(v == "true")
} else {
model.DisableDefaultOutput = types.BoolValue(false)
}
}
var cloudConfig cloud.Configuration
env := model.Environment.ValueString()
switch strings.ToLower(env) {
case "public":
cloudConfig = cloud.AzurePublic
case "usgovernment":
cloudConfig = cloud.AzureGovernment
case "china":
cloudConfig = cloud.AzureChina
default:
response.Diagnostics.AddError("Invalid `environment` value.", fmt.Sprintf("The `environment` value '%s' is invalid. Valid values are 'public', 'usgovernment' and 'china'.", env))
return
}
if elements := model.Endpoint.Elements(); len(elements) != 0 {
var endpoint providerEndpointData
diags := elements[0].(basetypes.ObjectValue).As(ctx, &endpoint, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: false,
UnhandledUnknownAsEmpty: false,
})
response.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
resourceManagerEndpoint := cloudConfig.Services[cloud.ResourceManager].Endpoint
resourceManagerAudience := cloudConfig.Services[cloud.ResourceManager].Audience
if v := endpoint.ResourceManagerEndpoint.ValueString(); v != "" {
resourceManagerEndpoint = v
}
if v := endpoint.ResourceManagerAudience.ValueString(); v != "" {
resourceManagerAudience = v
}
cloudConfig.Services[cloud.ResourceManager] = cloud.ServiceConfiguration{
Endpoint: resourceManagerEndpoint,
Audience: resourceManagerAudience,
}
if v := endpoint.ActiveDirectoryAuthorityHost.ValueString(); v != "" {
cloudConfig.ActiveDirectoryAuthorityHost = v
}
}
var auxTenants []string
if elements := model.AuxiliaryTenantIDs.Elements(); len(elements) != 0 {
for _, element := range elements {
auxTenants = append(auxTenants, element.(basetypes.StringValue).ValueString())
}
}
option := azidentity.DefaultAzureCredentialOptions{
AdditionallyAllowedTenants: auxTenants,
ClientOptions: azcore.ClientOptions{
Cloud: cloudConfig,
},
TenantID: model.TenantID.ValueString(),
}
cred, err := buildChainedTokenCredential(model, option)
if err != nil {
response.Diagnostics.AddError("Failed to obtain a credential.", err.Error())
return
}
maxGoSdkRetryAttempts := int32(3)
if !model.MaximumBusyRetryAttempts.IsNull() {
maxGoSdkRetryAttempts = model.MaximumBusyRetryAttempts.ValueInt32()
}
copt := &clients.Option{
Cred: cred,
CloudCfg: cloudConfig,
ApplicationUserAgent: buildUserAgent(request.TerraformVersion, model.PartnerID.ValueString(), model.DisableTerraformPartnerID.ValueBool()),
MaxGoSdkRetries: maxGoSdkRetryAttempts,
Features: features.UserFeatures{
DefaultTags: tags.ExpandTags(model.DefaultTags),
DefaultLocation: location.Normalize(model.DefaultLocation.ValueString()),
DefaultNaming: model.DefaultName.ValueString(),
EnablePreflight: model.EnablePreflight.ValueBool(),
DisableDefaultOutput: model.DisableDefaultOutput.ValueBool(),
},
SkipProviderRegistration: model.SkipProviderRegistration.ValueBool(),
DisableCorrelationRequestID: model.DisableCorrelationRequestID.ValueBool(),
CustomCorrelationRequestID: model.CustomCorrelationRequestID.ValueString(),
SubscriptionId: model.SubscriptionID.ValueString(),
TenantId: model.TenantID.ValueString(),
}
client := &clients.Client{}
if err = client.Build(ctx, copt); err != nil {
response.Diagnostics.AddError("Error Building Client", err.Error())
return
}
// load schema
azure.GetAzureSchema()
response.ResourceData = client
response.DataSourceData = client
response.EphemeralResourceData = client
}
func (p Provider) Functions(ctx context.Context) []func() function.Function {
return []func() function.Function{
func() function.Function {
return &functions.ParseResourceIdFunction{}
},
func() function.Function { return &functions.BuildResourceIdFunction{} },
func() function.Function {
return &functions.TenantResourceIdFunction{}
},
func() function.Function {
return &functions.SubscriptionResourceIdFunction{}
},
func() function.Function { return &functions.ResourceGroupResourceIdFunction{} },
func() function.Function { return &functions.ManagementGroupResourceIdFunction{} },
func() function.Function { return &functions.ExtensionResourceIdFunction{} },
}
}
func (p Provider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
func() datasource.DataSource {
return &services.ResourceIdDataSource{}
},
func() datasource.DataSource {
return &services.ResourceListDataSource{}
},
func() datasource.DataSource {
return &services.ResourceActionDataSource{}
},
func() datasource.DataSource {
return &services.AzapiResourceDataSource{}
},
func() datasource.DataSource {
return &services.ClientConfigDataSource{}
},
}
}
func (p Provider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
func() resource.Resource {
return &services.AzapiResource{}
},
func() resource.Resource {
return &services.AzapiUpdateResource{}
},
func() resource.Resource {
return &services.ActionResource{}
},
func() resource.Resource {
return &services.DataPlaneResource{}
},
}
}
func (p Provider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
func() ephemeral.EphemeralResource {
return &services.ActionEphemeral{}
},
}
}
func buildUserAgent(terraformVersion string, partnerID string, disableTerraformPartnerID bool) string {
if terraformVersion == "" {
// Terraform 0.12 introduced this field to the protocol
// We can therefore assume that if it's missing it's 0.10 or 0.11
terraformVersion = "0.11+compatible"
}
tfUserAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io)", terraformVersion)
providerUserAgent := fmt.Sprintf("terraform-provider-azapi/%s", version.ProviderVersion)
userAgent := strings.TrimSpace(fmt.Sprintf("%s %s", tfUserAgent, providerUserAgent))
// append the CloudShell version to the user agent if it exists
if azureAgent := os.Getenv("AZURE_HTTP_USER_AGENT"); azureAgent != "" {
userAgent = fmt.Sprintf("%s %s", userAgent, azureAgent)
}
// only one pid can be interpreted currently
// hence, send partner ID if present, otherwise send Terraform GUID
// unless users have opted out
if partnerID == "" && !disableTerraformPartnerID {
// Microsoft’s Terraform Partner ID is this specific GUID
partnerID = "222c6c49-1b0a-5959-a213-6608f9eb8820"
}
if partnerID != "" {
userAgent = fmt.Sprintf("%s pid-%s", userAgent, partnerID)
}
return userAgent
}
func buildChainedTokenCredential(model providerData, options azidentity.DefaultAzureCredentialOptions) (*azidentity.ChainedTokenCredential, error) {
log.Printf("[DEBUG] building chained token credential")
var creds []azcore.TokenCredential
if model.UseOIDC.ValueBool() || model.UseAKSWorkloadIdentity.ValueBool() {
log.Printf("[DEBUG] oidc credential or AKS Workload Identity enabled")
if cred, err := buildOidcCredential(model, options); err == nil {
creds = append(creds, cred)
} else {
log.Printf("[DEBUG] failed to initialize oidc credential: %v", err)
}
log.Printf("[DEBUG] azure pipelines credential enabled")
if cred, err := buildAzurePipelinesCredential(model, options); err == nil {
creds = append(creds, cred)
} else {
log.Printf("[DEBUG] failed to initialize azure pipelines credential: %v", err)
}
}
if cred, err := buildClientSecretCredential(model, options); err == nil {
creds = append(creds, cred)
} else {
log.Printf("[DEBUG] failed to initialize client secret credential: %v", err)
}
if cred, err := buildClientCertificateCredential(model, options); err == nil {
creds = append(creds, cred)
} else {
log.Printf("[DEBUG] failed to initialize client certificate credential: %v", err)
}
if model.UseMSI.ValueBool() {
log.Printf("[DEBUG] msi credential enabled")
if cred, err := buildManagedIdentityCredential(model, options); err == nil {
creds = append(creds, cred)
} else {
log.Printf("[DEBUG] failed to initialize msi credential: %v", err)
}
}
if model.UseCLI.ValueBool() {
log.Printf("[DEBUG] cli credential enabled")
if cred, err := buildAzureCLICredential(options); err == nil {
creds = append(creds, cred)
} else {
log.Printf("[DEBUG] failed to initialize cli credential: %v", err)
}
}
if len(creds) == 0 {
return nil, fmt.Errorf("no credentials were successfully initialized")
}
return azidentity.NewChainedTokenCredential(creds, nil)
}
func buildClientSecretCredential(model providerData, options azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) {
log.Printf("[DEBUG] building client secret credential")
clientID, err := model.GetClientId()
if err != nil {
return nil, err
}
clientSecret, err := model.GetClientSecret()
if err != nil {
return nil, err
}
o := &azidentity.ClientSecretCredentialOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
return azidentity.NewClientSecretCredential(options.TenantID, *clientID, *clientSecret, o)
}
func buildClientCertificateCredential(model providerData, options azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) {
log.Printf("[DEBUG] building client certificate credential")
clientID, err := model.GetClientId()
if err != nil {
return nil, err
}
var certData []byte
if certPath := model.ClientCertificatePath.ValueString(); certPath != "" {
log.Printf("[DEBUG] reading certificate from file %s", certPath)
// #nosec G304
certData, err = os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf(`failed to read certificate file "%s": %v`, certPath, err)
}
}
if certBase64 := model.ClientCertificate.ValueString(); certBase64 != "" {
log.Printf("[DEBUG] decoding certificate from base64")
certData, err = decodeCertificate(certBase64)
if err != nil {
return nil, err
}
}
if len(certData) == 0 {
return nil, fmt.Errorf("no certificate data provided")
}
var password []byte
if v := model.ClientCertificatePassword.ValueString(); v != "" {
password = []byte(v)
}
certs, key, err := azidentity.ParseCertificates(certData, password)
if err != nil {
return nil, fmt.Errorf(`failed to load certificate": %v`, err)
}
o := &azidentity.ClientCertificateCredentialOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
return azidentity.NewClientCertificateCredential(options.TenantID, *clientID, certs, key, o)
}
func buildOidcCredential(model providerData, options azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) {
log.Printf("[DEBUG] building oidc credential")
clientId, err := model.GetClientId()
if err != nil {
return nil, err
}
if model.OIDCToken.ValueString() == "" && model.GetOIDCTokenFilePath() == "" && (model.OIDCRequestToken.ValueString() == "" || model.OIDCRequestURL.ValueString() == "") {
return nil, fmt.Errorf("missing required OIDC configuration")
}
o := &OidcCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: options.Cloud,
},
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
TenantID: options.TenantID,
ClientID: *clientId,
RequestToken: model.OIDCRequestToken.ValueString(),
RequestUrl: model.OIDCRequestURL.ValueString(),
Token: model.OIDCToken.ValueString(),
TokenFilePath: model.GetOIDCTokenFilePath(),
}
return NewOidcCredential(o)
}
func buildManagedIdentityCredential(model providerData, options azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) {
log.Printf("[DEBUG] building managed identity credential")
clientId, err := model.GetClientId()
if err != nil {
return nil, err
}
o := &azidentity.ManagedIdentityCredentialOptions{
ClientOptions: options.ClientOptions,
ID: azidentity.ClientID(*clientId),
}
return NewManagedIdentityCredential(o)
}
func buildAzureCLICredential(options azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) {
log.Printf("[DEBUG] building azure cli credential")
o := &azidentity.AzureCLICredentialOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
TenantID: options.TenantID,
}
return azidentity.NewAzureCLICredential(o)
}
func buildAzurePipelinesCredential(model providerData, options azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) {
log.Printf("[DEBUG] building azure pipeline credential")
o := &azidentity.AzurePipelinesCredentialOptions{
ClientOptions: options.ClientOptions,
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
clientId, err := model.GetClientId()
if err != nil {
return nil, err
}
return azidentity.NewAzurePipelinesCredential(options.TenantID, *clientId, model.OIDCAzureServiceConnectionID.ValueString(), model.OIDCRequestToken.ValueString(), o)
}
func decodeCertificate(clientCertificate string) ([]byte, error) {
var pfx []byte
if clientCertificate != "" {
out := make([]byte, base64.StdEncoding.DecodedLen(len(clientCertificate)))
n, err := base64.StdEncoding.Decode(out, []byte(clientCertificate))
if err != nil {
return pfx, fmt.Errorf("could not decode client certificate data: %v", err)
}
pfx = out[:n]
}
return pfx, nil
}