├── .github ├── labeler.yml ├── pull_request_template.md └── workflows │ ├── automation-release.yaml │ ├── lint.yaml │ └── test.yaml ├── .gitignore ├── .go-version ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── authentication ├── environment.go └── environment_test.go ├── eventhub ├── sas_token.go └── sas_token_test.go ├── framework ├── commonschema │ ├── id.go │ ├── names.go │ ├── resourcegroupname.go │ └── tags.go ├── convert │ ├── convert.go │ ├── expand.go │ ├── expand_test.go │ ├── flatten.go │ ├── flatten_test.go │ └── models_test.go ├── fwdiag │ ├── diags.go │ ├── diags_test.go │ └── must.go ├── identity │ ├── common.go │ ├── system_and_user_assigned_list.go │ ├── system_and_user_assigned_list_test.go │ ├── system_and_user_assigned_map.go │ ├── system_and_user_assigned_map_test.go │ ├── system_assigned.go │ ├── system_assigned_test.go │ ├── system_or_user_assigned_list.go │ ├── system_or_user_assigned_list_test.go │ ├── system_or_user_assigned_map.go │ └── system_or_user_assigned_map_test.go ├── location │ ├── attributes.go │ └── helpers.go └── typehelpers │ ├── attributes.go │ ├── bool.go │ ├── int64.go │ ├── list.go │ ├── list_of_objects.go │ ├── list_of_primitive.go │ ├── map.go │ ├── map_object_interface.go │ ├── map_of_objects.go │ ├── map_of_primitives.go │ ├── nested_object_interface.go │ ├── null.go │ ├── object_generics.go │ ├── set_of_objects.go │ ├── set_of_primitive.go │ ├── string.go │ └── timeouts.go ├── go.mod ├── go.sum ├── lang ├── dates │ └── parse.go ├── pointer │ ├── from.go │ ├── generic.go │ ├── pointer_test.go │ └── to.go └── response │ ├── response.go │ └── response_test.go ├── polling └── poller.go ├── resourcemanager ├── commonids │ ├── app_service.go │ ├── app_service_environment.go │ ├── app_service_environment_test.go │ ├── app_service_function_app.go │ ├── app_service_logic_app.go │ ├── app_service_plan.go │ ├── app_service_plan_test.go │ ├── app_service_test.go │ ├── app_service_web_app.go │ ├── automation_compilation_job.go │ ├── automation_compilation_job_test.go │ ├── availability_set_id.go │ ├── availability_set_id_test.go │ ├── billing_account_customer.go │ ├── billing_account_customer_test.go │ ├── billing_account_invoice_section.go │ ├── billing_account_invoice_section_test.go │ ├── billing_enrollment_account.go │ ├── billing_enrollment_account_test.go │ ├── bot_service.go │ ├── bot_service_channel.go │ ├── bot_service_channel_test.go │ ├── bot_service_test.go │ ├── chaos_studio_capability.go │ ├── chaos_studio_capability_test.go │ ├── chaos_studio_target.go │ ├── chaos_studio_target_test.go │ ├── cloud_services_ip_configuration.go │ ├── cloud_services_ip_configuration_test.go │ ├── cloud_services_public_ip.go │ ├── cloud_services_public_ip_test.go │ ├── community_gallery_image.go │ ├── community_gallery_image_test.go │ ├── community_gallery_image_version.go │ ├── community_gallery_image_version_test.go │ ├── composite_resource_id.go │ ├── composite_resource_id_test.go │ ├── dedicated_host.go │ ├── dedicated_host_group.go │ ├── dedicated_host_group_test.go │ ├── dedicated_host_test.go │ ├── dev_center.go │ ├── dev_center_test.go │ ├── disk_encryption_set.go │ ├── disk_encryption_set_test.go │ ├── express_route_circuit_peering.go │ ├── express_route_circuit_peering_test.go │ ├── hdinsight_cluster.go │ ├── hdinsight_cluster_test.go │ ├── hyperv_site_job.go │ ├── hyperv_site_job_test.go │ ├── hyperv_site_machine.go │ ├── hyperv_site_machine_test.go │ ├── hyperv_site_runasaccount.go │ ├── hyperv_site_runasaccount_test.go │ ├── ids.go │ ├── key_vault.go │ ├── key_vault_key.go │ ├── key_vault_key_test.go │ ├── key_vault_key_version.go │ ├── key_vault_key_version_test.go │ ├── key_vault_private_endpoint_connection.go │ ├── key_vault_private_endpoint_connection_test.go │ ├── key_vault_test.go │ ├── kubernetes_cluster.go │ ├── kubernetes_cluster_test.go │ ├── kubernetes_fleet.go │ ├── kubernetes_fleet_test.go │ ├── kusto_cluster.go │ ├── kusto_cluster_test.go │ ├── kusto_database.go │ ├── kusto_database_test.go │ ├── managed_disk.go │ ├── managed_disk_test.go │ ├── management_group.go │ ├── management_group_test.go │ ├── network_interface.go │ ├── network_interface_ip_configuration.go │ ├── network_interface_ip_configuration_test.go │ ├── network_interface_test.go │ ├── provisioning_service.go │ ├── provisioning_service_test.go │ ├── public_ip_address.go │ ├── public_ip_address_test.go │ ├── resource_group.go │ ├── resource_group_test.go │ ├── scope.go │ ├── scope_test.go │ ├── shared_image_gallery.go │ ├── shared_image_gallery_test.go │ ├── spring_cloud_service.go │ ├── spring_cloud_service_test.go │ ├── sql_database.go │ ├── sql_database_test.go │ ├── sql_elastic_pool.go │ ├── sql_elastic_pool_test.go │ ├── sql_managed_instance.go │ ├── sql_managed_instance_database.go │ ├── sql_managed_instance_database_test.go │ ├── sql_managed_instance_test.go │ ├── sql_server.go │ ├── sql_server_test.go │ ├── storage_account.go │ ├── storage_account_test.go │ ├── storage_container.go │ ├── storage_container_test.go │ ├── subnet.go │ ├── subnet_test.go │ ├── subscription.go │ ├── subscription_test.go │ ├── user_assigned_identity.go │ ├── user_assigned_identity_test.go │ ├── virtual_hub_bgp_connection.go │ ├── virtual_hub_bgp_connection_test.go │ ├── virtual_hub_ip_configuration.go │ ├── virtual_hub_ip_configuration_test.go │ ├── virtual_machine.go │ ├── virtual_machine_scale_set.go │ ├── virtual_machine_scale_set_ip_configuration.go │ ├── virtual_machine_scale_set_ip_configuration_test.go │ ├── virtual_machine_scale_set_network_interface.go │ ├── virtual_machine_scale_set_network_interface_test.go │ ├── virtual_machine_scale_set_public_ip_address.go │ ├── virtual_machine_scale_set_public_ip_address_test.go │ ├── virtual_machine_scale_set_test.go │ ├── virtual_machine_test.go │ ├── virtual_network.go │ ├── virtual_network_test.go │ ├── virtual_router_peering.go │ ├── virtual_router_peering_test.go │ ├── virtual_wan_p2s_vpn_gateway.go │ ├── virtual_wan_p2s_vpn_gateway_test.go │ ├── vmware_site_job.go │ ├── vmware_site_job_test.go │ ├── vmware_site_machine.go │ ├── vmware_site_machine_test.go │ ├── vmware_site_runasaccount.go │ ├── vmware_site_runasaccount_test.go │ ├── vpn_connection.go │ └── vpn_connection_test.go ├── commonschema │ ├── edge_zone.go │ ├── identity_system.go │ ├── identity_system_or_user.go │ ├── identity_system_user.go │ ├── identity_user.go │ ├── location.go │ ├── resource_group_name.go │ ├── resource_id_reference.go │ ├── tags.go │ ├── zone.go │ └── zones.go ├── edgezones │ ├── model.go │ ├── model_test.go │ ├── normalize.go │ ├── normalize_test.go │ └── schema.go ├── features │ └── user_specified_segments.go ├── identity │ ├── constants.go │ ├── legacy_system_and_user_assigned_list.go │ ├── legacy_system_and_user_assigned_list_test.go │ ├── legacy_system_and_user_assigned_map.go │ ├── legacy_system_and_user_assigned_map_test.go │ ├── model.go │ ├── system_and_user_assigned_list.go │ ├── system_and_user_assigned_list_test.go │ ├── system_and_user_assigned_map.go │ ├── system_and_user_assigned_map_test.go │ ├── system_assigned.go │ ├── system_assigned_test.go │ ├── system_or_user_assigned_list.go │ ├── system_or_user_assigned_list_test.go │ ├── system_or_user_assigned_map.go │ ├── system_or_user_assigned_map_test.go │ ├── tfschema_model.go │ ├── user_assigned_list.go │ ├── user_assigned_list_test.go │ ├── user_assigned_map.go │ └── user_assigned_map_test.go ├── location │ ├── normalize.go │ ├── normalize_test.go │ ├── schema.go │ ├── supported.go │ ├── supported_azure.go │ ├── validation.go │ └── validation_test.go ├── recaser │ ├── recase.go │ ├── recaser_test.go │ └── registration.go ├── resourcegroups │ ├── validate.go │ └── validate_test.go ├── resourceids │ ├── error_number_of_segments_didnt_match.go │ ├── error_number_of_segments_didnt_match_test.go │ ├── error_segment_not_specified.go │ ├── error_segment_not_specified_test.go │ ├── errors.go │ ├── helpers_test.go │ ├── interface.go │ ├── legacy.go │ ├── match.go │ ├── match_test.go │ ├── parse.go │ └── parse_test.go ├── systemdata │ └── model.go ├── tags │ ├── expand.go │ ├── flatten.go │ ├── validate.go │ └── validate_test.go └── zones │ ├── expand.go │ ├── flatten.go │ └── schema.go ├── scripts ├── determine-and-publish-git-tag.sh └── update-azurerm-provider.sh ├── sender └── sender.go └── storage ├── sas_token.go └── sas_token_test.go /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | release-once-merged: 5 | - changed-files: 6 | - any-glob-to-any-file: 7 | - '**' -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Community Note 4 | 5 | * Please vote on this PR by adding a :thumbsup: [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original PR to help the community and maintainers prioritize for review 6 | * Please do not leave comments along the lines of "+1", "me too" or "any updates", they generate extra noise for PR followers and do not help prioritize for review 7 | 8 | 9 | ## Description 10 | 11 | 13 | 14 | 15 | ## Change Log 16 | 17 | Below please provide what should go into the changelog (if anything) conforming to the [Changelog Format documented here](../blob/main/contributing/topics/maintainer-changelog.md). 18 | 19 | 20 | 21 | * added new function `AFunction` [GH-00000] 22 | 23 | 24 | 25 | This is a (please select all that apply): 26 | 27 | - [ ] Bug Fix 28 | - [ ] New Feature 29 | - [ ] Enhancement 30 | - [ ] Breaking Change 31 | 32 | 33 | ## Related Issue(s) 34 | Fixes #0000 35 | 36 | 37 | 38 | ## Rollback Plan 39 | 40 | If a change needs to be reverted, we will publish an updated version of the provider. 41 | 42 | ## Changes to Security Controls 43 | 44 | Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain. 45 | 46 | > [!NOTE] 47 | > If this PR changes meaningfully during the course of review please update the title and description as required. 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linting 3 | on: 4 | pull_request: 5 | types: ["opened", "synchronize"] 6 | paths: 7 | - '.github/workflows/lint.yaml' 8 | - '**.go' 9 | 10 | jobs: 11 | linting: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 16 | with: 17 | go-version-file: ./.go-version 18 | - uses: golangci/golangci-lint-action@2e788936b09dd82dc280e845628a40d2ba6b204c # v6.3.1 19 | with: 20 | version: 'v1.64.8' 21 | args: -v -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Unit Tests 3 | on: 4 | pull_request: 5 | types: ['opened', 'synchronize'] 6 | 7 | jobs: 8 | unit-test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 12 | 13 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 14 | with: 15 | go-version-file: ./.go-version 16 | 17 | - name: Run unit tests 18 | run: make test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | .python-version 4 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.24.1 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/terraform-azure 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO111MODULE=on 2 | 3 | default: test 4 | 5 | dependencies: 6 | go mod download 7 | 8 | tools: 9 | @go install mvdan.cc/gofumpt@latest 10 | @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH || $GOPATH)/bin v1.64.8 11 | 12 | lint: 13 | @golangci-lint -j 12 run --fast --exclude-dirs="/sdk/" ./... 14 | 15 | test: dependencies 16 | go vet ./... 17 | go test -race -v ./... 18 | 19 | .PHONY: default test 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## `hashicorp/go-azure-helpers` 2 | 3 | This repository contains various helpers and wrappers for working with Azure - and contains a number of common types used in `hashicorp/go-azure-sdk`. 4 | -------------------------------------------------------------------------------- /authentication/environment_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package authentication 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestAzureEnvironmentNames(t *testing.T) { 13 | testData := map[string]string{ 14 | "": "public", 15 | "AzureChinaCloud": "china", 16 | "AzureCloud": "public", 17 | "AZUREUSGOVERNMENTCLOUD": "usgovernment", 18 | "AzurePublicCloud": "public", 19 | } 20 | 21 | for input, expected := range testData { 22 | actual := normalizeEnvironmentName(input) 23 | if actual != expected { 24 | t.Fatalf("Expected %q for input %q: got %q!", expected, input, actual) 25 | } 26 | } 27 | } 28 | 29 | func TestAccAzureEnvironmentByName(t *testing.T) { 30 | env, err := AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "public") 31 | if err != nil { 32 | t.Fatalf("Error getting Endpoint: %s", err) 33 | } 34 | if !strings.EqualFold(env.Name, "AzurePublicCloud") { 35 | t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzurePublicCloud", env.Name) 36 | } 37 | env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "usgovernment") 38 | if err != nil { 39 | t.Fatalf("Error getting Endpoint: %s", err) 40 | } 41 | if !strings.EqualFold(env.Name, "AzureUSGovernmentCloud") { 42 | t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureUSGovernmentCloud", env.Name) 43 | } 44 | env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "china") 45 | if err != nil { 46 | t.Fatalf("Error getting Endpoint: %s", err) 47 | } 48 | if !strings.EqualFold(env.Name, "AzureChinaCloud") { 49 | t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureChinaCloud", env.Name) 50 | } 51 | } 52 | 53 | func TestAccAzureEnvironmentByNameFromEndpoint(t *testing.T) { 54 | env, err := AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "AzureCloud") 55 | if err != nil { 56 | t.Fatalf("Error getting Endpoint: %s", err) 57 | } 58 | if !strings.EqualFold(env.Name, "AzureCloud") { 59 | t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureCloud", env.Name) 60 | } 61 | env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "AzureChinaCloud") 62 | if err != nil { 63 | t.Fatalf("Error getting Endpoint: %s", err) 64 | } 65 | if !strings.EqualFold(env.Name, "AzureChinaCloud") { 66 | t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureChinaCloud", env.Name) 67 | } 68 | env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "AzureUSGovernment") 69 | if err != nil { 70 | t.Fatalf("Error getting Endpoint: %s", err) 71 | } 72 | if !strings.EqualFold(env.Name, "AzureUSGovernment") { 73 | t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureUSGovernment", env.Name) 74 | } 75 | _, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "badurl", "AzureChinaCloud") 76 | if err == nil { 77 | t.Fatal("Expected error from bad endpoint") 78 | } 79 | _, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "badEnvironment") 80 | if err == nil { 81 | t.Fatal("Expected error from bad environment") 82 | } 83 | } 84 | 85 | func TestAccIsEnvironmentAzureStack(t *testing.T) { 86 | ok, err := IsEnvironmentAzureStack(context.TODO(), "management.azure.com", "public") 87 | if err != nil { 88 | t.Fatalf("Error getting Endpoint: %s", err) 89 | } 90 | if ok { 91 | t.Fatal("Expected `public` environment to not be Azure Stack") 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /eventhub/sas_token.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package eventhub 5 | 6 | import ( 7 | "crypto/hmac" 8 | "crypto/sha256" 9 | "encoding/base64" 10 | "fmt" 11 | "net/url" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | const ( 18 | connStringSharedAccessKeyKey = "SharedAccessKey" 19 | connStringSharedAccessKeyNameKey = "SharedAccessKeyName" 20 | ) 21 | 22 | func ComputeEventHubSASToken(sharedAccessKeyName string, 23 | sharedAccessKey string, 24 | eventHubUri string, 25 | expiry string, 26 | ) (string, error) { 27 | uri := url.QueryEscape(eventHubUri) 28 | 29 | expireTime, err := time.Parse(time.RFC3339, expiry) 30 | if err != nil { 31 | return "", err 32 | } 33 | expireTimestamp := expireTime.Unix() 34 | expireStr := strconv.FormatInt(expireTimestamp, 10) 35 | 36 | stringToSign := uri + "\n" + expireStr 37 | 38 | key := []byte(sharedAccessKey) 39 | h := hmac.New(sha256.New, key) 40 | h.Write([]byte(stringToSign)) 41 | signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) 42 | 43 | sasToken := "sr=" + uri 44 | sasToken += "&sig=" + url.QueryEscape(signature) 45 | sasToken += "&se=" + (expireStr) 46 | sasToken += "&skn=" + (sharedAccessKeyName) 47 | 48 | return sasToken, nil 49 | } 50 | 51 | func ComputeEventHubSASConnectionString(sasToken string) string { 52 | return fmt.Sprintf("SharedAccessSignature %s", sasToken) 53 | } 54 | 55 | func ComputeEventHubSASConnectionUrl(endpoint string, entityPath string) (*string, error) { 56 | if endpoint == "" { 57 | return nil, fmt.Errorf("endpoint cannot be empty") 58 | } 59 | 60 | var url string 61 | if entityPath == "" { 62 | url = strings.TrimRight(endpoint, "/") 63 | } else { 64 | url = endpoint + entityPath 65 | } 66 | 67 | return &url, nil 68 | } 69 | 70 | func ParseEventHubSASConnectionString(connString string) (map[string]string, error) { 71 | // This connection string was for a real Event Hub which has been deleted 72 | // so its safe to include here for reference to understand the format. 73 | // Endpoint=sb://example-ehn.servicebus.windows.net/;SharedAccessKeyName=example-ehar;SharedAccessKey=DzGpfdyJda9D/xIkME0FLA66wZnheOBID0s1/rrtlHg=;EntityPath=example-eh 74 | validKeys := map[string]bool{"Endpoint": true, "SharedAccessKeyName": true, "SharedAccessKey": true, "EntityPath": true} 75 | // The k-v pairs are separated with semi-colons 76 | tokens := strings.Split(connString, ";") 77 | 78 | kvp := make(map[string]string) 79 | 80 | for _, atoken := range tokens { 81 | // The individual k-v are separated by an equals sign. 82 | kv := strings.SplitN(atoken, "=", 2) 83 | if len(kv) != 2 { 84 | return nil, fmt.Errorf("[ERROR] token `%s` is an invalid key=pair (connection string %s)", atoken, connString) 85 | } 86 | 87 | key := kv[0] 88 | val := kv[1] 89 | 90 | if _, present := validKeys[key]; !present { 91 | return nil, fmt.Errorf("[ERROR] Unknown Key `%s` in connection string %s", key, connString) 92 | } 93 | kvp[key] = val 94 | } 95 | 96 | if _, present := kvp[connStringSharedAccessKeyKey]; !present { 97 | return nil, fmt.Errorf("[ERROR] Shared Access Key not found in connection string: %s", connString) 98 | } 99 | 100 | return kvp, nil 101 | } 102 | -------------------------------------------------------------------------------- /framework/commonschema/id.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 9 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 11 | ) 12 | 13 | func IDAttribute() schema.StringAttribute { 14 | return schema.StringAttribute{ 15 | Computed: true, 16 | PlanModifiers: []planmodifier.String{ 17 | stringplanmodifier.UseStateForUnknown(), 18 | }, 19 | } 20 | } 21 | 22 | func IDAttributeDataSource() datasourceschema.StringAttribute { 23 | return datasourceschema.StringAttribute{ 24 | Computed: true, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /framework/commonschema/names.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | // This file supplies common standardised names for key properties used in the AzureRM provider. Use this package to ensure consistency for these names in resources and data sources. 7 | 8 | const ( 9 | Identity string = "identity" 10 | Location string = "location" 11 | Name string = "name" 12 | ResourceGroupName string = "resource_group_name" 13 | Tags string = "tags" 14 | Zones string = "zones" 15 | ) 16 | -------------------------------------------------------------------------------- /framework/commonschema/resourcegroupname.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "github.com/hashicorp/go-azure-helpers/framework/typehelpers" 8 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourcegroups" 9 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 12 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 13 | ) 14 | 15 | func ResourceGroupNameAttribute() schema.StringAttribute { 16 | return schema.StringAttribute{ 17 | Required: true, 18 | Description: "The name of the resource group", 19 | MarkdownDescription: "The name of the resource group", 20 | Validators: []validator.String{ 21 | typehelpers.WrappedStringValidator{ 22 | Func: resourcegroups.ValidateName, 23 | }, 24 | }, 25 | PlanModifiers: []planmodifier.String{ 26 | stringplanmodifier.RequiresReplace(), 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /framework/commonschema/tags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/framework/typehelpers" 10 | "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" 11 | datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" 14 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 15 | "github.com/hashicorp/terraform-plugin-framework/types" 16 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 17 | ) 18 | 19 | func TagsResourceAttribute(ctx context.Context) resourceschema.MapAttribute { 20 | return resourceschema.MapAttribute{ 21 | CustomType: typehelpers.NewMapTypeOf[types.String](ctx), 22 | ElementType: basetypes.StringType{}, 23 | Optional: true, 24 | Description: "A map of tags to be assigned to the resource", 25 | MarkdownDescription: "A map of tags to be assigned to the resource", 26 | Validators: []validator.Map{ 27 | mapvalidator.SizeAtLeast(1), 28 | }, 29 | } 30 | } 31 | 32 | func TagsDataSourceAttribute(ctx context.Context) datasourceschema.MapAttribute { 33 | return datasourceschema.MapAttribute{ 34 | CustomType: typehelpers.NewMapTypeOf[types.String](ctx), 35 | ElementType: basetypes.StringType{}, 36 | Optional: true, 37 | Description: "A map of tags assigned to the resource", 38 | MarkdownDescription: "A map of tags assigned to the resource", 39 | Validators: []validator.Map{ 40 | mapvalidator.SizeAtLeast(1), 41 | }, 42 | } 43 | } 44 | 45 | func ExpandTags(input types.Map) (result *map[string]string, diags diag.Diagnostics) { 46 | if input.IsNull() || input.IsUnknown() { 47 | return 48 | } 49 | 50 | diags = input.ElementsAs(context.Background(), &result, false) 51 | 52 | return 53 | } 54 | 55 | func FlattenTags(tags *map[string]string) (result basetypes.MapValue, diags diag.Diagnostics) { 56 | if tags == nil { 57 | return basetypes.NewMapNull(basetypes.StringType{}), nil 58 | } 59 | 60 | return types.MapValueFrom(context.Background(), basetypes.StringType{}, tags) 61 | } 62 | -------------------------------------------------------------------------------- /framework/convert/convert.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package convert 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | 10 | "github.com/hashicorp/terraform-plugin-framework/diag" 11 | ) 12 | 13 | // convert handles the conversion between Framework types and Go native types used by go-azure-sdk and 14 | // returns reflect.Value for each in the order from -> to and diag.Diagnostics for errors where incorrect types are 15 | // provided. `source` can be a pointer, `target` must be a pointer. It is intended to be used via Expand and Flatten and 16 | // is intentionally not exported. 17 | func convert(source, target any) (reflect.Value, reflect.Value, diag.Diagnostics) { 18 | diags := diag.Diagnostics{} 19 | sourceVal := reflect.ValueOf(source) 20 | targetVal := reflect.ValueOf(target) 21 | 22 | if sourceVal.Kind() == reflect.Ptr { 23 | sourceVal = sourceVal.Elem() 24 | } 25 | 26 | if kind := targetVal.Kind(); kind != reflect.Ptr { 27 | diags.AddError("convert", fmt.Sprintf("target (%T): %s is not a pointer", target, kind)) 28 | return reflect.Value{}, reflect.Value{}, diags 29 | } 30 | 31 | targetVal = targetVal.Elem() 32 | 33 | return sourceVal, targetVal, diags 34 | } 35 | -------------------------------------------------------------------------------- /framework/fwdiag/diags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package fwdiag 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/hashicorp/terraform-plugin-framework/diag" 12 | ) 13 | 14 | // DiagnosticsError returns an error containing all Diagnostic with SeverityError 15 | func DiagnosticsError(diags diag.Diagnostics) error { 16 | var errs []error 17 | 18 | for _, d := range diags.Errors() { 19 | errs = append(errs, errors.New(DiagnosticString(d))) 20 | } 21 | 22 | return errors.Join(errs...) 23 | } 24 | 25 | // DiagnosticString formats a Diagnostic 26 | // If there is no `Detail`, only prints summary, otherwise prints both 27 | func DiagnosticString(d diag.Diagnostic) string { 28 | var buf strings.Builder 29 | 30 | fmt.Fprint(&buf, d.Summary()) 31 | if d.Detail() != "" { 32 | fmt.Fprintf(&buf, "\n\n%s", d.Detail()) 33 | } 34 | if withPath, ok := d.(diag.DiagnosticWithPath); ok { 35 | fmt.Fprintf(&buf, "\n%s", withPath.Path().String()) 36 | } 37 | 38 | return buf.String() 39 | } 40 | 41 | func NewResourceNotFoundWarningDiagnostic(err error) diag.Diagnostic { 42 | return diag.NewWarningDiagnostic( 43 | "Azure resource not found during refresh", 44 | "Automatically removing from Terraform State instead of returning the error, which may trigger resource recreation. Original error: "+err.Error(), 45 | ) 46 | } 47 | 48 | func AsError[T any](x T, diags diag.Diagnostics) (T, error) { 49 | return x, DiagnosticsError(diags) 50 | } 51 | -------------------------------------------------------------------------------- /framework/fwdiag/diags_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package fwdiag_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/go-azure-helpers/framework/fwdiag" 10 | "github.com/hashicorp/terraform-plugin-framework/diag" 11 | ) 12 | 13 | func TestDiagnosticsError(t *testing.T) { 14 | t.Parallel() 15 | 16 | testCases := []struct { 17 | testName string 18 | diags diag.Diagnostics 19 | wantErr bool 20 | }{ 21 | { 22 | testName: "nil Diagnostics", 23 | }, 24 | { 25 | testName: "single warning Diagnostics", 26 | diags: diag.Diagnostics{diag.NewWarningDiagnostic("summary", "detail")}, 27 | }, 28 | { 29 | testName: "single error Diagnostics", 30 | diags: diag.Diagnostics{diag.NewErrorDiagnostic("summary", "detail")}, 31 | wantErr: true, 32 | }, 33 | { 34 | testName: "mixed warning and error Diagnostics", 35 | diags: diag.Diagnostics{ 36 | diag.NewWarningDiagnostic("summary1", "detail1"), 37 | diag.NewErrorDiagnostic("summary2", "detail2"), 38 | }, 39 | wantErr: true, 40 | }, 41 | { 42 | testName: "multiple error Diagnostics", 43 | diags: diag.Diagnostics{ 44 | diag.NewErrorDiagnostic("summary1", "detail1"), 45 | diag.NewErrorDiagnostic("summary2", "detail2"), 46 | }, 47 | wantErr: true, 48 | }, 49 | { 50 | testName: "multiple warning Diagnostics", 51 | diags: diag.Diagnostics{ 52 | diag.NewWarningDiagnostic("summary1", "detail1"), 53 | diag.NewWarningDiagnostic("summary2", "detail2"), 54 | }, 55 | }, 56 | } 57 | 58 | for _, testCase := range testCases { 59 | testCase := testCase 60 | t.Run(testCase.testName, func(t *testing.T) { 61 | t.Parallel() 62 | 63 | err := fwdiag.DiagnosticsError(testCase.diags) 64 | gotErr := err != nil 65 | 66 | if gotErr != testCase.wantErr { 67 | t.Errorf("gotErr = %v, wantErr = %v", gotErr, testCase.wantErr) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /framework/fwdiag/must.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package fwdiag 5 | 6 | import ( 7 | "errors" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/diag" 10 | ) 11 | 12 | // Must - ensures a hard fail if diags contains errors from the supplied function x. This should 13 | // protect developers from shipping panics from incompatible type conversions / 14 | func Must[T any](x T, diags diag.Diagnostics) T { 15 | return ErrMust(x, DiagAsError(diags)) 16 | } 17 | 18 | func ErrMust[T any](x T, err error) T { 19 | if err != nil { 20 | panic(err) 21 | } 22 | return x 23 | } 24 | 25 | func DiagAsError(diags diag.Diagnostics) error { 26 | errs := make([]error, 0) 27 | 28 | for _, err := range diags.Errors() { 29 | errStr := err.Summary() 30 | if err.Detail() != "" { 31 | errStr += ": " + err.Detail() 32 | } 33 | errs = append(errs, errors.New(errStr)) 34 | } 35 | 36 | return errors.Join(errs...) 37 | } 38 | -------------------------------------------------------------------------------- /framework/identity/system_and_user_assigned_list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/framework/convert" 10 | "github.com/hashicorp/go-azure-helpers/framework/typehelpers" 11 | "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | "github.com/hashicorp/terraform-plugin-framework/types" 14 | ) 15 | 16 | func ExpandToSystemAndUserAssignedList(ctx context.Context, input typehelpers.ListNestedObjectValueOf[IdentityModel], result *identity.SystemAndUserAssignedList, diags *diag.Diagnostics) { 17 | if result == nil { 18 | diags.AddError("Expanding identity", "could not expand identity as target was a nil pointer") 19 | return 20 | } 21 | 22 | if input.IsNull() || input.IsUnknown() || len(input.Elements()) == 0 { 23 | result.Type = identity.TypeNone 24 | result.IdentityIds = nil 25 | result.PrincipalId = "" 26 | result.TenantId = "" 27 | 28 | return 29 | } 30 | 31 | identityList := make([]IdentityModel, len(input.Elements())) 32 | 33 | d := input.ElementsAs(ctx, &identityList, true) 34 | if d.HasError() { 35 | diags.Append(d...) 36 | return 37 | } 38 | 39 | if len(identityList) == 1 { 40 | ident := identityList[0] 41 | convert.Expand(ctx, ident, result, diags) 42 | } 43 | } 44 | 45 | func FlattenFromSystemAndUserAssignedList(ctx context.Context, input *identity.SystemAndUserAssignedList, result *typehelpers.ListNestedObjectValueOf[IdentityModel], diags *diag.Diagnostics) { 46 | if input == nil { 47 | r := typehelpers.NewListNestedObjectValueOfNull[IdentityModel](ctx) 48 | *result = r 49 | 50 | return 51 | } 52 | 53 | flat := IdentityModel{ 54 | IdentityIDs: typehelpers.NewSetValueOfNull[types.String](ctx), 55 | } 56 | 57 | convert.Flatten(ctx, input, &flat, diags) 58 | list, d := typehelpers.NewListNestedObjectValueOfValueSlice(ctx, []IdentityModel{flat}) 59 | if d.HasError() { 60 | diags.Append(d...) 61 | return 62 | } 63 | 64 | *result = list 65 | } 66 | -------------------------------------------------------------------------------- /framework/identity/system_and_user_assigned_map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/framework/typehelpers" 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" 11 | "github.com/hashicorp/terraform-plugin-framework/attr" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | "github.com/hashicorp/terraform-plugin-framework/types" 14 | ) 15 | 16 | func ExpandToSystemAndUserAssignedMap(ctx context.Context, input typehelpers.ListNestedObjectValueOf[IdentityModel], result *identity.SystemAndUserAssignedMap, diags *diag.Diagnostics) { 17 | if result == nil { 18 | diags.AddError("Expanding identity", "could not expand identity as target was a nil pointer") 19 | return 20 | } 21 | 22 | if input.IsNull() || input.IsUnknown() || len(input.Elements()) == 0 { 23 | result.Type = identity.TypeNone 24 | result.IdentityIds = nil 25 | result.PrincipalId = "" 26 | result.TenantId = "" 27 | 28 | return 29 | } 30 | 31 | identityList := make([]IdentityModel, len(input.Elements())) 32 | 33 | d := input.ElementsAs(ctx, &identityList, true) 34 | if d.HasError() { 35 | diags.Append(d...) 36 | return 37 | } 38 | 39 | if len(identityList) == 1 { 40 | ident := identityList[0] 41 | 42 | res := identity.SystemAndUserAssignedMap{} 43 | 44 | res.Type = identity.Type(ident.Type.ValueString()) 45 | res.PrincipalId = ident.PrincipalID.ValueString() 46 | res.TenantId = ident.TenantID.ValueString() 47 | 48 | // convert identities from list to map construct 49 | identities := map[string]identity.UserAssignedIdentityDetails{} 50 | idList := make([]string, 0) 51 | ident.IdentityIDs.ElementsAs(ctx, &idList, false) 52 | 53 | for _, id := range idList { 54 | identities[id] = identity.UserAssignedIdentityDetails{} 55 | } 56 | 57 | res.IdentityIds = identities 58 | *result = res 59 | } 60 | } 61 | 62 | func FlattenFromSystemAndUserAssignedMap(ctx context.Context, input *identity.SystemAndUserAssignedMap, result *typehelpers.ListNestedObjectValueOf[IdentityModel], diags *diag.Diagnostics) { 63 | if input == nil { 64 | r := typehelpers.NewListNestedObjectValueOfNull[IdentityModel](ctx) 65 | *result = r 66 | 67 | return 68 | } 69 | 70 | i := *input 71 | 72 | ident := IdentityModel{ 73 | Type: types.StringValue(string(i.Type)), 74 | PrincipalID: types.StringValue(i.PrincipalId), 75 | TenantID: types.StringValue(i.TenantId), 76 | } 77 | 78 | if len(i.IdentityIds) > 0 { 79 | ids := make([]attr.Value, 0) 80 | for id := range i.IdentityIds { 81 | ids = append(ids, types.StringValue(id)) 82 | } 83 | 84 | ident.IdentityIDs, *diags = typehelpers.NewSetValueOf[types.String](ctx, ids) 85 | if diags.HasError() { 86 | return 87 | } 88 | } else { 89 | ident.IdentityIDs = typehelpers.NewSetValueOfNull[types.String](ctx) 90 | } 91 | 92 | r, d := typehelpers.NewListNestedObjectValueOfValueSlice(ctx, []IdentityModel{ident}) 93 | if d.HasError() { 94 | diags.Append(d...) 95 | return 96 | } 97 | 98 | *result = r 99 | } 100 | -------------------------------------------------------------------------------- /framework/identity/system_assigned.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/framework/convert" 10 | "github.com/hashicorp/go-azure-helpers/framework/typehelpers" 11 | "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | ) 14 | 15 | func ExpandToSystemAssigned(ctx context.Context, input typehelpers.ListNestedObjectValueOf[SystemIdentityModel], result *identity.SystemAssigned, diags *diag.Diagnostics) { 16 | if result == nil { 17 | diags.AddError("Expanding identity", "could not expand identity as target was a nil pointer") 18 | return 19 | } 20 | 21 | if input.IsNull() || input.IsUnknown() || len(input.Elements()) == 0 { 22 | result.Type = identity.TypeNone 23 | result.PrincipalId = "" 24 | result.TenantId = "" 25 | 26 | return 27 | } 28 | 29 | identityList := make([]SystemIdentityModel, len(input.Elements())) 30 | 31 | d := input.ElementsAs(ctx, &identityList, true) 32 | if d.HasError() { 33 | diags.Append(d...) 34 | return 35 | } 36 | 37 | if len(identityList) == 1 { 38 | ident := identityList[0] 39 | convert.Expand(ctx, ident, result, diags) 40 | } 41 | } 42 | 43 | func FlattenFromSystemAssigned(ctx context.Context, input *identity.SystemAssigned, result *typehelpers.ListNestedObjectValueOf[SystemIdentityModel], diags *diag.Diagnostics) { 44 | if input == nil { 45 | r := typehelpers.NewListNestedObjectValueOfNull[SystemIdentityModel](ctx) 46 | *result = r 47 | 48 | return 49 | } 50 | 51 | flat := SystemIdentityModel{} 52 | 53 | convert.Flatten(ctx, input, &flat, diags) 54 | list, d := typehelpers.NewListNestedObjectValueOfValueSlice(ctx, []SystemIdentityModel{flat}) 55 | if d.HasError() { 56 | diags.Append(d...) 57 | return 58 | } 59 | 60 | *result = list 61 | } 62 | -------------------------------------------------------------------------------- /framework/identity/system_or_user_assigned_list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/framework/convert" 10 | "github.com/hashicorp/go-azure-helpers/framework/typehelpers" 11 | "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | "github.com/hashicorp/terraform-plugin-framework/types" 14 | ) 15 | 16 | func ExpandToSystemOrUserAssignedList(ctx context.Context, input typehelpers.ListNestedObjectValueOf[IdentityModel], result *identity.SystemOrUserAssignedList, diags *diag.Diagnostics) { 17 | if result == nil { 18 | diags.AddError("Expanding identity", "could not expand identity as target was a nil pointer") 19 | return 20 | } 21 | 22 | if input.IsNull() || input.IsUnknown() || len(input.Elements()) == 0 { 23 | result.Type = identity.TypeNone 24 | result.IdentityIds = nil 25 | result.PrincipalId = "" 26 | result.TenantId = "" 27 | 28 | return 29 | } 30 | 31 | identityList := make([]IdentityModel, len(input.Elements())) 32 | 33 | d := input.ElementsAs(ctx, &identityList, true) 34 | if d.HasError() { 35 | diags.Append(d...) 36 | return 37 | } 38 | 39 | if len(identityList) == 1 { 40 | ident := identityList[0] 41 | convert.Expand(ctx, ident, result, diags) 42 | } 43 | } 44 | 45 | func FlattenFromSystemOrUserAssignedList(ctx context.Context, input *identity.SystemOrUserAssignedList, result *typehelpers.ListNestedObjectValueOf[IdentityModel], diags *diag.Diagnostics) { 46 | if input == nil { 47 | r := typehelpers.NewListNestedObjectValueOfNull[IdentityModel](ctx) 48 | *result = r 49 | 50 | return 51 | } 52 | 53 | flat := IdentityModel{ 54 | IdentityIDs: typehelpers.NewSetValueOfNull[types.String](ctx), 55 | } 56 | 57 | convert.Flatten(ctx, input, &flat, diags) 58 | list, d := typehelpers.NewListNestedObjectValueOfValueSlice(ctx, []IdentityModel{flat}) 59 | if d.HasError() { 60 | diags.Append(d...) 61 | return 62 | } 63 | 64 | *result = list 65 | } 66 | -------------------------------------------------------------------------------- /framework/identity/system_or_user_assigned_map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/framework/typehelpers" 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" 11 | "github.com/hashicorp/terraform-plugin-framework/attr" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | "github.com/hashicorp/terraform-plugin-framework/types" 14 | ) 15 | 16 | func ExpandToSystemOrUserAssignedMap(ctx context.Context, input typehelpers.ListNestedObjectValueOf[IdentityModel], result *identity.SystemOrUserAssignedMap, diags *diag.Diagnostics) { 17 | if result == nil { 18 | diags.AddError("Expanding identity", "could not expand identity as target was a nil pointer") 19 | return 20 | } 21 | 22 | if input.IsNull() || input.IsUnknown() || len(input.Elements()) == 0 { 23 | result.Type = identity.TypeNone 24 | result.IdentityIds = nil 25 | result.PrincipalId = "" 26 | result.TenantId = "" 27 | 28 | return 29 | } 30 | 31 | identityList := make([]IdentityModel, len(input.Elements())) 32 | 33 | d := input.ElementsAs(ctx, &identityList, true) 34 | if d.HasError() { 35 | diags.Append(d...) 36 | return 37 | } 38 | 39 | if len(identityList) == 1 { 40 | ident := identityList[0] 41 | 42 | res := identity.SystemOrUserAssignedMap{} 43 | 44 | res.Type = identity.Type(ident.Type.ValueString()) 45 | res.PrincipalId = ident.PrincipalID.ValueString() 46 | res.TenantId = ident.TenantID.ValueString() 47 | 48 | // convert identities from list to map construct 49 | identities := map[string]identity.UserAssignedIdentityDetails{} 50 | idList := make([]string, 0) 51 | ident.IdentityIDs.ElementsAs(ctx, &idList, false) 52 | 53 | for _, id := range idList { 54 | identities[id] = identity.UserAssignedIdentityDetails{} 55 | } 56 | 57 | res.IdentityIds = identities 58 | *result = res 59 | } 60 | } 61 | 62 | func FlattenFromSystemOrUserAssignedMap(ctx context.Context, input *identity.SystemOrUserAssignedMap, result *typehelpers.ListNestedObjectValueOf[IdentityModel], diags *diag.Diagnostics) { 63 | if input == nil { 64 | r := typehelpers.NewListNestedObjectValueOfNull[IdentityModel](ctx) 65 | *result = r 66 | 67 | return 68 | } 69 | 70 | i := *input 71 | 72 | ident := IdentityModel{ 73 | Type: types.StringValue(string(i.Type)), 74 | PrincipalID: types.StringValue(i.PrincipalId), 75 | TenantID: types.StringValue(i.TenantId), 76 | } 77 | 78 | if len(i.IdentityIds) > 0 { 79 | ids := make([]attr.Value, 0) 80 | for id := range i.IdentityIds { 81 | ids = append(ids, types.StringValue(id)) 82 | } 83 | 84 | ident.IdentityIDs, *diags = typehelpers.NewSetValueOf[types.String](ctx, ids) 85 | if diags.HasError() { 86 | return 87 | } 88 | } else { 89 | ident.IdentityIDs = typehelpers.NewSetValueOfNull[types.String](ctx) 90 | } 91 | 92 | r, d := typehelpers.NewListNestedObjectValueOfValueSlice(ctx, []IdentityModel{ident}) 93 | if d.HasError() { 94 | diags.Append(d...) 95 | return 96 | } 97 | 98 | *result = r 99 | } 100 | -------------------------------------------------------------------------------- /framework/location/attributes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import ( 7 | "github.com/hashicorp/go-azure-helpers/framework/typehelpers" 8 | "github.com/hashicorp/go-azure-helpers/resourcemanager/location" 9 | datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 14 | ) 15 | 16 | func LocationAttribute() schema.StringAttribute { 17 | return schema.StringAttribute{ 18 | Required: true, 19 | Validators: []validator.String{ 20 | typehelpers.WrappedStringValidator{ 21 | Func: location.EnhancedValidate, 22 | }, 23 | }, 24 | PlanModifiers: []planmodifier.String{ 25 | stringplanmodifier.RequiresReplace(), 26 | }, 27 | } 28 | } 29 | 30 | func LocationComputedAttribute() schema.StringAttribute { 31 | return schema.StringAttribute{ 32 | Computed: true, 33 | } 34 | } 35 | 36 | func LocationDataSourceAttribute() datasourceschema.StringAttribute { 37 | return datasourceschema.StringAttribute{ 38 | Required: true, 39 | Validators: []validator.String{ 40 | typehelpers.WrappedStringValidator{ 41 | Func: location.EnhancedValidate, 42 | }, 43 | }, 44 | } 45 | } 46 | 47 | func LocationRelocatableAttribute() schema.StringAttribute { 48 | return schema.StringAttribute{ 49 | Required: true, 50 | Validators: []validator.String{ 51 | typehelpers.WrappedStringValidator{ 52 | Func: location.EnhancedValidate, 53 | }, 54 | }, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /framework/location/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import ( 7 | resourcemanagerlocation "github.com/hashicorp/go-azure-helpers/resourcemanager/location" 8 | ) 9 | 10 | // Normalize transforms the human readable Azure Region/Location names (e.g. `West US`) 11 | // into the canonical value to allow comparisons between user-code and API Responses 12 | func Normalize(input string) string { 13 | return resourcemanagerlocation.Normalize(input) 14 | } 15 | 16 | // NormalizeNilable normalizes the Location field even if it's nil to ensure this field 17 | // can always have a value 18 | func NormalizeNilable(input *string) string { 19 | return resourcemanagerlocation.NormalizeNilable(input) 20 | } 21 | -------------------------------------------------------------------------------- /framework/typehelpers/attributes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "reflect" 10 | 11 | "github.com/hashicorp/go-azure-helpers/framework/fwdiag" 12 | "github.com/hashicorp/terraform-plugin-framework/attr" 13 | "github.com/hashicorp/terraform-plugin-framework/diag" 14 | ) 15 | 16 | // AttributeTypes returns a map of attribute types for the specified type T. 17 | // T must be a struct and reflection is used to find exported fields of T with the `tfsdk` tag. 18 | func AttributeTypes[T any](ctx context.Context) (map[string]attr.Type, diag.Diagnostics) { 19 | var diags diag.Diagnostics 20 | var t T 21 | val := reflect.ValueOf(t) 22 | typ := val.Type() 23 | 24 | if typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct { 25 | val = reflect.New(typ.Elem()).Elem() 26 | typ = typ.Elem() 27 | } 28 | 29 | if typ.Kind() != reflect.Struct { 30 | diags.Append(diag.NewErrorDiagnostic("Invalid type", fmt.Sprintf("%T has unsupported type: %s", t, typ))) 31 | return nil, diags 32 | } 33 | 34 | attributeTypes := make(map[string]attr.Type) 35 | for i := 0; i < typ.NumField(); i++ { 36 | field := typ.Field(i) 37 | if field.PkgPath != "" { 38 | continue // Skip unexported fields. 39 | } 40 | 41 | tag := field.Tag.Get(`tfsdk`) 42 | if tag == "-" { 43 | continue // Skip explicitly excluded fields. 44 | } 45 | 46 | if tag == "" { 47 | diags.Append(diag.NewErrorDiagnostic("Invalid type", fmt.Sprintf(`%T needs a struct tag for "tfsdk" on %s`, t, field.Name))) 48 | return nil, diags 49 | } 50 | 51 | if v, ok := val.Field(i).Interface().(attr.Value); ok { 52 | attributeTypes[tag] = v.Type(ctx) 53 | } 54 | } 55 | 56 | return attributeTypes, nil 57 | } 58 | 59 | func newAttrTypeOf[T attr.Value](ctx context.Context) attr.Type { 60 | var zero T 61 | 62 | return zero.Type(ctx) 63 | } 64 | 65 | func AttributeTypesMust[T any](ctx context.Context) map[string]attr.Type { 66 | return fwdiag.Must(AttributeTypes[T](ctx)) 67 | } 68 | -------------------------------------------------------------------------------- /framework/typehelpers/bool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/lang/pointer" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 11 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 12 | ) 13 | 14 | type WrappedBoolDefault struct { 15 | Desc *string 16 | Markdown *string 17 | Value bool 18 | } 19 | 20 | var _ defaults.Bool = WrappedBoolDefault{} 21 | 22 | func NewWrappedBoolDefault[T ~bool](value T) WrappedBoolDefault { 23 | return WrappedBoolDefault{ 24 | Value: bool(value), 25 | } 26 | } 27 | 28 | func (w WrappedBoolDefault) Description(_ context.Context) string { 29 | return pointer.From(w.Desc) 30 | } 31 | 32 | func (w WrappedBoolDefault) MarkdownDescription(_ context.Context) string { 33 | return pointer.From(w.Markdown) 34 | } 35 | 36 | func (w WrappedBoolDefault) DefaultBool(_ context.Context, _ defaults.BoolRequest, response *defaults.BoolResponse) { 37 | d := basetypes.NewBoolValue(w.Value) 38 | response.PlanValue = d 39 | } 40 | -------------------------------------------------------------------------------- /framework/typehelpers/int64.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/lang/pointer" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 11 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 12 | ) 13 | 14 | type WrappedInt64Default struct { 15 | Desc *string 16 | Markdown *string 17 | Value int64 18 | } 19 | 20 | var _ defaults.Int64 = WrappedInt64Default{} 21 | 22 | func NewWrappedInt64Default[T ~int](value T) WrappedInt64Default { 23 | return WrappedInt64Default{ 24 | Value: int64(value), 25 | } 26 | } 27 | 28 | func (w WrappedInt64Default) Description(_ context.Context) string { 29 | return pointer.From(w.Desc) 30 | } 31 | 32 | func (w WrappedInt64Default) MarkdownDescription(_ context.Context) string { 33 | return pointer.From(w.Markdown) 34 | } 35 | 36 | func (w WrappedInt64Default) DefaultInt64(ctx context.Context, request defaults.Int64Request, response *defaults.Int64Response) { 37 | d := basetypes.NewInt64Value(w.Value) 38 | response.PlanValue = d 39 | } 40 | -------------------------------------------------------------------------------- /framework/typehelpers/list_of_primitive.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/hashicorp/go-azure-helpers/framework/fwdiag" 11 | "github.com/hashicorp/terraform-plugin-framework/attr" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 14 | "github.com/hashicorp/terraform-plugin-go/tftypes" 15 | ) 16 | 17 | var ( 18 | _ basetypes.ListTypable = (*listTypeOf[basetypes.StringValue])(nil) 19 | _ basetypes.ListValuable = (*ListValueOf[basetypes.StringValue])(nil) 20 | ) 21 | 22 | type listTypeOf[T attr.Value] struct { 23 | basetypes.ListType 24 | } 25 | 26 | var ( 27 | ListOfStringType = listTypeOf[basetypes.StringValue]{basetypes.ListType{ElemType: basetypes.StringType{}}} 28 | ListOfInt64Type = listTypeOf[basetypes.Int64Value]{basetypes.ListType{ElemType: basetypes.Int64Type{}}} 29 | ListOfFloat64Type = listTypeOf[basetypes.Float64Value]{basetypes.ListType{ElemType: basetypes.Float64Type{}}} 30 | ) 31 | 32 | func NewListTypeOf[T attr.Value](ctx context.Context) listTypeOf[T] { 33 | return newListTypeOf[T](ctx) 34 | } 35 | 36 | func newListTypeOf[T attr.Value](ctx context.Context) listTypeOf[T] { 37 | return listTypeOf[T]{basetypes.ListType{ElemType: newAttrTypeOf[T](ctx)}} 38 | } 39 | 40 | func (t listTypeOf[T]) Equal(o attr.Type) bool { 41 | other, ok := o.(listTypeOf[T]) 42 | 43 | if !ok { 44 | return false 45 | } 46 | 47 | return t.ListType.Equal(other.ListType) 48 | } 49 | 50 | func (t listTypeOf[T]) String() string { 51 | var zero T 52 | return fmt.Sprintf("ListTypeOf[%T]", zero) 53 | } 54 | 55 | func (t listTypeOf[T]) ValueFromList(ctx context.Context, in basetypes.ListValue) (basetypes.ListValuable, diag.Diagnostics) { 56 | var diags diag.Diagnostics 57 | 58 | if in.IsNull() { 59 | return NewListValueOfNull[T](ctx), diags 60 | } 61 | 62 | if in.IsUnknown() { 63 | return NewListValueOfUnknown[T](ctx), diags 64 | } 65 | 66 | v, d := basetypes.NewListValue(newAttrTypeOf[T](ctx), in.Elements()) 67 | diags.Append(d...) 68 | if diags.HasError() { 69 | return NewListValueOfUnknown[T](ctx), diags 70 | } 71 | 72 | return ListValueOf[T]{ListValue: v}, diags 73 | } 74 | 75 | func (t listTypeOf[T]) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { 76 | attrValue, err := t.ListType.ValueFromTerraform(ctx, in) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | listValue, ok := attrValue.(basetypes.ListValue) 82 | 83 | if !ok { 84 | return nil, fmt.Errorf("unexpected value type of %T", attrValue) 85 | } 86 | 87 | listValuable, diags := t.ValueFromList(ctx, listValue) 88 | 89 | if diags.HasError() { 90 | return nil, fmt.Errorf("unexpected error converting ListValue to ListValuable: %v", diags) 91 | } 92 | 93 | return listValuable, nil 94 | } 95 | 96 | func (t listTypeOf[T]) ValueType(ctx context.Context) attr.Value { 97 | return ListValueOf[T]{} 98 | } 99 | 100 | type ListValueOf[T attr.Value] struct { 101 | basetypes.ListValue 102 | } 103 | 104 | func (v ListValueOf[T]) Equal(o attr.Value) bool { 105 | other, ok := o.(ListValueOf[T]) 106 | 107 | if !ok { 108 | return false 109 | } 110 | 111 | return v.ListValue.Equal(other.ListValue) 112 | } 113 | 114 | func (v ListValueOf[T]) Type(ctx context.Context) attr.Type { 115 | return newListTypeOf[T](ctx) 116 | } 117 | 118 | func NewListValueOfNull[T attr.Value](ctx context.Context) ListValueOf[T] { 119 | return ListValueOf[T]{ListValue: basetypes.NewListNull(newAttrTypeOf[T](ctx))} 120 | } 121 | 122 | func NewListValueOfUnknown[T attr.Value](ctx context.Context) ListValueOf[T] { 123 | return ListValueOf[T]{ListValue: basetypes.NewListUnknown(newAttrTypeOf[T](ctx))} 124 | } 125 | 126 | func NewListValueOf[T attr.Value](ctx context.Context, elements []attr.Value) (ListValueOf[T], diag.Diagnostics) { 127 | var diags diag.Diagnostics 128 | 129 | v, d := basetypes.NewListValue(newAttrTypeOf[T](ctx), elements) 130 | diags.Append(d...) 131 | if diags.HasError() { 132 | return NewListValueOfUnknown[T](ctx), diags 133 | } 134 | 135 | return ListValueOf[T]{ListValue: v}, diags 136 | } 137 | 138 | func NewListValueOfMust[T attr.Value](ctx context.Context, elements []attr.Value) ListValueOf[T] { 139 | return fwdiag.Must(NewListValueOf[T](ctx, elements)) 140 | } 141 | -------------------------------------------------------------------------------- /framework/typehelpers/map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "reflect" 10 | 11 | "github.com/hashicorp/terraform-plugin-framework/diag" 12 | "github.com/hashicorp/terraform-plugin-framework/types" 13 | ) 14 | 15 | func FlattenMapPointer[T any](input *map[string]T) (result types.Map, diags diag.Diagnostics) { 16 | outType := reflect.TypeOf(input).Elem().Elem().Kind() 17 | diags = make(diag.Diagnostics, 0) 18 | switch outType { 19 | case reflect.String: 20 | if len(*input) > 0 { 21 | result, diags = types.MapValueFrom(context.Background(), types.StringType, input) 22 | } else { 23 | result = types.MapNull(types.StringType) 24 | } 25 | case reflect.Int64: 26 | if len(*input) > 0 { 27 | result, diags = types.MapValueFrom(context.Background(), types.Int64Type, input) 28 | } else { 29 | result = types.MapNull(types.Int64Type) 30 | } 31 | case reflect.Float64: 32 | if len(*input) > 0 { 33 | result, diags = types.MapValueFrom(context.Background(), types.Float64Type, input) 34 | } else { 35 | result = types.MapNull(types.Float64Type) 36 | } 37 | case reflect.Bool: 38 | if len(*input) > 0 { 39 | result, diags = types.MapValueFrom(context.Background(), types.BoolType, input) 40 | } else { 41 | result = types.MapNull(types.BoolType) 42 | } 43 | default: 44 | diags.AddError("unsupported map element type", fmt.Sprintf("got %s", outType.String())) 45 | } 46 | 47 | return result, diags 48 | } 49 | 50 | func FlattenMap[T any](input map[string]T) (result types.Map, diags diag.Diagnostics) { 51 | outType := reflect.TypeOf(input).Elem().Kind() 52 | diags = make(diag.Diagnostics, 0) 53 | switch outType { 54 | case reflect.String: 55 | if len(input) > 0 { 56 | result, diags = types.MapValueFrom(context.Background(), types.StringType, input) 57 | } else { 58 | result = types.MapNull(types.StringType) 59 | } 60 | case reflect.Int64: 61 | if len(input) > 0 { 62 | result, diags = types.MapValueFrom(context.Background(), types.Int64Type, input) 63 | } else { 64 | result = types.MapNull(types.Int64Type) 65 | } 66 | case reflect.Float64: 67 | if len(input) > 0 { 68 | result, diags = types.MapValueFrom(context.Background(), types.Float64Type, input) 69 | } else { 70 | result = types.MapNull(types.Float64Type) 71 | } 72 | case reflect.Bool: 73 | if len(input) > 0 { 74 | result, diags = types.MapValueFrom(context.Background(), types.BoolType, input) 75 | } else { 76 | result = types.MapNull(types.BoolType) 77 | } 78 | default: 79 | diags.AddError("unsupported map element type", fmt.Sprintf("got %s", outType.String())) 80 | } 81 | 82 | return result, diags 83 | } 84 | 85 | func ExpandMap[T any](input types.Map) (result map[string]T, diags diag.Diagnostics) { 86 | if input.IsNull() || input.IsUnknown() { 87 | return nil, diags 88 | } 89 | 90 | diags = input.ElementsAs(context.Background(), &result, false) 91 | 92 | return 93 | } 94 | 95 | func ExpandMapPointer[T any](input types.Map) (*map[string]T, diag.Diagnostics) { 96 | r, d := ExpandMap[T](input) 97 | 98 | if r == nil { 99 | return nil, d 100 | } 101 | 102 | return &r, d 103 | } 104 | -------------------------------------------------------------------------------- /framework/typehelpers/map_object_interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/attr" 10 | "github.com/hashicorp/terraform-plugin-framework/diag" 11 | ) 12 | 13 | type NestedObjectMapValue interface { 14 | NestedObjectValue 15 | 16 | // ToObjectMap returns the Value as an object pointer ( 17 | ToObjectMap(context.Context) (any, diag.Diagnostics) 18 | } 19 | 20 | type MapObjectType interface { 21 | attr.Type 22 | 23 | // NewObjectPtr returns a new, empty value as an object pointer (Go *struct). 24 | NewObjectPtr(context.Context) (any, diag.Diagnostics) 25 | 26 | // NullValue returns a Null Value. 27 | NullValue(context.Context) (attr.Value, diag.Diagnostics) 28 | 29 | // ValueFromObjectPtr returns a Value given an object pointer (Go *struct). 30 | ValueFromObjectPtr(context.Context, any) (attr.Value, diag.Diagnostics) 31 | } 32 | 33 | type MapObjectCollectionType interface { 34 | MapObjectType 35 | 36 | // NewObjectMap returns a new value as an object map (Go map[string]struct). 37 | NewObjectMap(context.Context, int, int) (any, diag.Diagnostics) 38 | 39 | // ValueFromObjectMap returns a Value given an object pointer (Go map[string]struct). 40 | ValueFromObjectMap(context.Context, any) (attr.Value, diag.Diagnostics) 41 | } 42 | 43 | type MapObjectValue interface { 44 | attr.Value 45 | 46 | // ToObjectMapPtr returns the value as an object pointer (Go *struct). 47 | ToObjectMapPtr(context.Context) (any, diag.Diagnostics) 48 | } 49 | 50 | // MapObjectCollectionValue extends the NestedObjectValue interface for values that represent collections of nested Objects. 51 | type MapObjectCollectionValue interface { 52 | NestedObjectValue 53 | 54 | // ToObjectMap returns the value as an object slice (Go []*struct). 55 | ToObjectMap(context.Context) (map[string]any, diag.Diagnostics) 56 | } 57 | 58 | // // mapValueWithElements extends the Value interface for values that have an Elements method. 59 | // type mapValueWithElements interface { 60 | // attr.Value 61 | // 62 | // Elements() map[string]attr.Value 63 | // } 64 | -------------------------------------------------------------------------------- /framework/typehelpers/map_of_objects.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-azure-helpers/framework/fwdiag" 10 | "github.com/hashicorp/terraform-plugin-framework/attr" 11 | "github.com/hashicorp/terraform-plugin-framework/diag" 12 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 13 | ) 14 | 15 | var ( 16 | _ basetypes.MapTypable = (*mapObjectTypeOf[struct{}])(nil) 17 | _ NestedObjectCollectionType = (*mapObjectTypeOf[struct{}])(nil) 18 | _ basetypes.MapValuable = (*MapObjectValueOf[struct{}])(nil) 19 | _ NestedObjectCollectionValue = (*MapObjectValueOf[struct{}])(nil) 20 | ) 21 | 22 | type mapObjectTypeOf[T any] struct { 23 | basetypes.MapType 24 | } 25 | 26 | func (m mapObjectTypeOf[T]) NewObjectPtr(ctx context.Context) (any, diag.Diagnostics) { 27 | return objectTypeNewObjectPtr[T](ctx) 28 | } 29 | 30 | func (m mapObjectTypeOf[T]) NullValue(ctx context.Context) (attr.Value, diag.Diagnostics) { 31 | diags := diag.Diagnostics{} 32 | 33 | return NewMapObjectValueOfNull[T](ctx), diags 34 | } 35 | 36 | func (m mapObjectTypeOf[T]) ValueFromObjectPtr(ctx context.Context, a any) (attr.Value, diag.Diagnostics) { 37 | // TODO implement me 38 | panic("implement me") 39 | } 40 | 41 | func (m mapObjectTypeOf[T]) NewObjectSlice(ctx context.Context, i int, i2 int) (any, diag.Diagnostics) { 42 | // TODO implement me 43 | panic("implement me") 44 | } 45 | 46 | func (m mapObjectTypeOf[T]) ValueFromObjectSlice(ctx context.Context, a any) (attr.Value, diag.Diagnostics) { 47 | // TODO implement me 48 | panic("implement me") 49 | } 50 | 51 | type MapObjectValueOf[T any] struct { 52 | basetypes.MapValue 53 | } 54 | 55 | func (m MapObjectValueOf[T]) ToObjectPtr(ctx context.Context) (any, diag.Diagnostics) { 56 | // TODO implement me 57 | panic("implement me") 58 | } 59 | 60 | func (m MapObjectValueOf[T]) ToObjectSlice(ctx context.Context) (any, diag.Diagnostics) { 61 | // TODO implement me 62 | panic("implement me") 63 | } 64 | 65 | func NewMapValueObjectOfUnknown[T any](ctx context.Context) MapObjectValueOf[T] { 66 | return MapObjectValueOf[T]{MapValue: basetypes.NewMapUnknown(NewObjectTypeOf[T](ctx))} 67 | } 68 | 69 | func NewMapObjectValueOf[T any](ctx context.Context, t map[string]T) (MapObjectValueOf[T], diag.Diagnostics) { 70 | return newMapObjectValueOf[T](ctx, t) 71 | } 72 | 73 | func NewMapObjectValueOfMust[T any](ctx context.Context, t map[string]T) MapObjectValueOf[T] { 74 | return fwdiag.Must(newMapObjectValueOf[T](ctx, t)) 75 | } 76 | 77 | func newMapObjectValueOf[T any](ctx context.Context, elements any) (MapObjectValueOf[T], diag.Diagnostics) { 78 | diags := diag.Diagnostics{} 79 | 80 | typ, d := newObjectTypeOf[T](ctx) 81 | diags.Append(d...) 82 | if diags.HasError() { 83 | return NewMapValueObjectOfUnknown[T](ctx), diags 84 | } 85 | 86 | v, d := basetypes.NewMapValueFrom(ctx, typ, elements) 87 | diags.Append(d...) 88 | if diags.HasError() { 89 | return NewMapValueObjectOfUnknown[T](ctx), diags 90 | } 91 | 92 | return MapObjectValueOf[T]{MapValue: v}, diags 93 | } 94 | 95 | func NewMapObjectValueOfNull[T any](ctx context.Context) MapObjectValueOf[T] { 96 | return MapObjectValueOf[T]{MapValue: basetypes.NewMapNull(NewObjectTypeOf[T](ctx))} 97 | } 98 | -------------------------------------------------------------------------------- /framework/typehelpers/map_of_primitives.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/hashicorp/go-azure-helpers/framework/fwdiag" 11 | "github.com/hashicorp/terraform-plugin-framework/attr" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 14 | "github.com/hashicorp/terraform-plugin-go/tftypes" 15 | ) 16 | 17 | // MapOfStringType is a custom type used for defining a Map of strings. 18 | var MapOfStringType = mapTypeOf[basetypes.StringValue]{basetypes.MapType{ElemType: basetypes.StringType{}}} 19 | 20 | type mapTypeOf[T attr.Value] struct { 21 | basetypes.MapType 22 | } 23 | 24 | func NewMapTypeOf[T attr.Value](ctx context.Context) mapTypeOf[T] { 25 | return mapTypeOf[T]{basetypes.MapType{ElemType: newAttrTypeOf[T](ctx)}} 26 | } 27 | 28 | func (t mapTypeOf[T]) Equal(o attr.Type) bool { 29 | other, ok := o.(mapTypeOf[T]) 30 | 31 | if !ok { 32 | return false 33 | } 34 | 35 | return t.MapType.Equal(other.MapType) 36 | } 37 | 38 | func (t mapTypeOf[T]) String() string { 39 | var zero T 40 | return fmt.Sprintf("MapTypeOf[%T]", zero) 41 | } 42 | 43 | func (t mapTypeOf[T]) ValueFromMap(ctx context.Context, in basetypes.MapValue) (basetypes.MapValuable, diag.Diagnostics) { 44 | var diags diag.Diagnostics 45 | 46 | if in.IsNull() { 47 | return NewMapValueOfNull[T](ctx), diags 48 | } 49 | 50 | if in.IsUnknown() { 51 | return NewMapValueOfUnknown[T](ctx), diags 52 | } 53 | 54 | // Here marks the spot where countless hours were spent all over the 55 | // internal organs of framework and autoflex only to discover the 56 | // first argument in this call should be an element type not the map 57 | // type. 58 | mapValue, d := basetypes.NewMapValue(newAttrTypeOf[T](ctx), in.Elements()) 59 | diags.Append(d...) 60 | if diags.HasError() { 61 | return NewMapValueOfUnknown[T](ctx), diags 62 | } 63 | 64 | return MapValueOf[T]{MapValue: mapValue}, diags 65 | } 66 | 67 | func (t mapTypeOf[T]) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { 68 | attrValue, err := t.MapType.ValueFromTerraform(ctx, in) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | mapValue, ok := attrValue.(basetypes.MapValue) 74 | if !ok { 75 | return nil, fmt.Errorf("unexpected value type of %T", attrValue) 76 | } 77 | 78 | mapValuable, diags := t.ValueFromMap(ctx, mapValue) 79 | if diags.HasError() { 80 | return nil, fmt.Errorf("unexpected error converting MapValue to MapValuable: %v", diags) 81 | } 82 | 83 | return mapValuable, nil 84 | } 85 | 86 | func (t mapTypeOf[T]) ValueType(ctx context.Context) attr.Value { 87 | return MapValueOf[T]{} 88 | } 89 | 90 | type MapValueOf[T attr.Value] struct { 91 | basetypes.MapValue 92 | } 93 | 94 | func (v MapValueOf[T]) Equal(o attr.Value) bool { 95 | other, ok := o.(MapValueOf[T]) 96 | 97 | if !ok { 98 | return false 99 | } 100 | 101 | return v.MapValue.Equal(other.MapValue) 102 | } 103 | 104 | func (v MapValueOf[T]) Type(ctx context.Context) attr.Type { 105 | return NewMapTypeOf[T](ctx) 106 | } 107 | 108 | func NewMapValueOfNull[T attr.Value](ctx context.Context) MapValueOf[T] { 109 | return MapValueOf[T]{MapValue: basetypes.NewMapNull(newAttrTypeOf[T](ctx))} 110 | } 111 | 112 | func NewMapValueOfUnknown[T attr.Value](ctx context.Context) MapValueOf[T] { 113 | return MapValueOf[T]{MapValue: basetypes.NewMapUnknown(newAttrTypeOf[T](ctx))} 114 | } 115 | 116 | func NewMapValueOf[T attr.Value](ctx context.Context, elements map[string]attr.Value) (MapValueOf[T], diag.Diagnostics) { 117 | var diags diag.Diagnostics 118 | 119 | v, d := basetypes.NewMapValue(newAttrTypeOf[T](ctx), elements) 120 | diags.Append(d...) 121 | if diags.HasError() { 122 | return NewMapValueOfUnknown[T](ctx), diags 123 | } 124 | 125 | return MapValueOf[T]{MapValue: v}, diags 126 | } 127 | 128 | func NewMapValueOfMust[T attr.Value](ctx context.Context, elements map[string]attr.Value) MapValueOf[T] { 129 | return fwdiag.Must(NewMapValueOf[T](ctx, elements)) 130 | } 131 | -------------------------------------------------------------------------------- /framework/typehelpers/nested_object_interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/attr" 10 | "github.com/hashicorp/terraform-plugin-framework/diag" 11 | ) 12 | 13 | type NestedObjectType interface { 14 | attr.Type 15 | 16 | // NewObjectPtr returns a new, empty value as an object pointer (Go *struct). 17 | NewObjectPtr(context.Context) (any, diag.Diagnostics) 18 | 19 | // NullValue returns a Null Value. 20 | NullValue(context.Context) (attr.Value, diag.Diagnostics) 21 | 22 | // ValueFromObjectPtr returns a Value given an object pointer (Go *struct). 23 | ValueFromObjectPtr(context.Context, any) (attr.Value, diag.Diagnostics) 24 | } 25 | 26 | // NestedObjectCollectionType extends the NestedObjectType interface for types that represent 27 | // collections (Lists or Sets) of nested Objects. 28 | type NestedObjectCollectionType interface { 29 | NestedObjectType 30 | 31 | // NewObjectSlice returns a new value as an object slice (Go []*struct). 32 | NewObjectSlice(context.Context, int, int) (any, diag.Diagnostics) 33 | 34 | // ValueFromObjectSlice returns a Value given an object pointer (Go []*struct). 35 | ValueFromObjectSlice(context.Context, any) (attr.Value, diag.Diagnostics) 36 | } 37 | 38 | type NestedObjectValue interface { 39 | attr.Value 40 | 41 | // ToObjectPtr returns the value as an object pointer (Go *struct). 42 | ToObjectPtr(context.Context) (any, diag.Diagnostics) 43 | } 44 | 45 | // NestedObjectCollectionValue extends the NestedObjectValue interface for values that represent collections of nested Objects. 46 | type NestedObjectCollectionValue interface { 47 | NestedObjectValue 48 | 49 | // ToObjectSlice returns the value as an object slice (Go []*struct). 50 | ToObjectSlice(context.Context) (any, diag.Diagnostics) 51 | } 52 | 53 | // valueWithElements extends the Value interface for values that have an Elements method. 54 | type valueWithElements interface { 55 | attr.Value 56 | 57 | Elements() []attr.Value 58 | } 59 | -------------------------------------------------------------------------------- /framework/typehelpers/null.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/attr" 10 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 11 | "github.com/hashicorp/terraform-plugin-go/tftypes" 12 | ) 13 | 14 | func NullValueOf(ctx context.Context, v any) (attr.Value, error) { 15 | var attrType attr.Type 16 | var tfType tftypes.Type 17 | 18 | switch t := v.(type) { 19 | case basetypes.BoolValuable: 20 | attrType = t.Type(ctx) 21 | tfType = tftypes.Bool 22 | case basetypes.Float64Valuable: 23 | attrType = t.Type(ctx) 24 | tfType = tftypes.Number 25 | case basetypes.Int64Valuable: 26 | attrType = t.Type(ctx) 27 | tfType = tftypes.Number 28 | case basetypes.StringValuable: 29 | attrType = t.Type(ctx) 30 | tfType = tftypes.String 31 | case basetypes.ListValuable: 32 | attrType = t.Type(ctx) 33 | if v, ok := attrType.(attr.TypeWithElementType); ok { 34 | tfType = tftypes.List{ElementType: v.ElementType().TerraformType(ctx)} 35 | } else { 36 | tfType = tftypes.List{} 37 | } 38 | case basetypes.SetValuable: 39 | attrType = t.Type(ctx) 40 | if tWithE, ok := attrType.(attr.TypeWithElementType); ok { 41 | tfType = tftypes.Set{ElementType: tWithE.ElementType().TerraformType(ctx)} 42 | } else { 43 | tfType = tftypes.Set{} 44 | } 45 | case basetypes.MapValuable: 46 | attrType = t.Type(ctx) 47 | if tWithE, ok := attrType.(attr.TypeWithElementType); ok { 48 | tfType = tftypes.Map{ElementType: tWithE.ElementType().TerraformType(ctx)} 49 | } else { 50 | tfType = tftypes.Map{} 51 | } 52 | case basetypes.ObjectValuable: 53 | attrType = t.Type(ctx) 54 | if tWithE, ok := attrType.(attr.TypeWithAttributeTypes); ok { 55 | tfType = tftypes.Object{AttributeTypes: translateMapTypes(tWithE.AttributeTypes(), func(attrType attr.Type) tftypes.Type { 56 | return attrType.TerraformType(ctx) 57 | })} 58 | } else { 59 | tfType = tftypes.Object{} 60 | } 61 | default: 62 | return nil, nil 63 | } 64 | 65 | return attrType.ValueFromTerraform(ctx, tftypes.NewValue(tfType, nil)) 66 | } 67 | 68 | func translateMapTypes[M ~map[K]V1, K comparable, V1, V2 any](m M, f func(V1) V2) map[K]V2 { 69 | n := make(map[K]V2, len(m)) 70 | 71 | for k, v := range m { 72 | n[k] = f(v) 73 | } 74 | 75 | return n 76 | } 77 | -------------------------------------------------------------------------------- /framework/typehelpers/set_of_primitive.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/hashicorp/go-azure-helpers/framework/fwdiag" 11 | "github.com/hashicorp/terraform-plugin-framework/attr" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 14 | "github.com/hashicorp/terraform-plugin-go/tftypes" 15 | ) 16 | 17 | var ( 18 | _ basetypes.SetTypable = (*setTypeOf[basetypes.StringValue])(nil) 19 | _ basetypes.SetValuable = (*SetValueOf[basetypes.StringValue])(nil) 20 | ) 21 | 22 | type setTypeOf[T attr.Value] struct { 23 | basetypes.SetType 24 | } 25 | 26 | var ( 27 | SetOfStringType = setTypeOf[basetypes.StringValue]{basetypes.SetType{ElemType: basetypes.StringType{}}} 28 | SetOfInt64Type = setTypeOf[basetypes.Int64Value]{basetypes.SetType{ElemType: basetypes.Int64Type{}}} 29 | SetOfFloat64Type = setTypeOf[basetypes.Float64Value]{basetypes.SetType{ElemType: basetypes.Float64Type{}}} 30 | ) 31 | 32 | func NewSetTypeOf[T attr.Value](ctx context.Context) setTypeOf[T] { 33 | return newSetTypeOf[T](ctx) 34 | } 35 | 36 | func newSetTypeOf[T attr.Value](ctx context.Context) setTypeOf[T] { 37 | return setTypeOf[T]{basetypes.SetType{ElemType: newAttrTypeOf[T](ctx)}} 38 | } 39 | 40 | func (t setTypeOf[T]) Equal(o attr.Type) bool { 41 | other, ok := o.(setTypeOf[T]) 42 | 43 | if !ok { 44 | return false 45 | } 46 | 47 | return t.SetType.Equal(other.SetType) 48 | } 49 | 50 | func (t setTypeOf[T]) String() string { 51 | var zero T 52 | return fmt.Sprintf("SetTypeOf[%T]", zero) 53 | } 54 | 55 | func (t setTypeOf[T]) ValueFromSet(ctx context.Context, in basetypes.SetValue) (basetypes.SetValuable, diag.Diagnostics) { 56 | var diags diag.Diagnostics 57 | 58 | if in.IsNull() { 59 | return NewSetValueOfNull[T](ctx), diags 60 | } 61 | 62 | if in.IsUnknown() { 63 | return NewSetValueOfUnknown[T](ctx), diags 64 | } 65 | 66 | v, d := basetypes.NewSetValue(newAttrTypeOf[T](ctx), in.Elements()) 67 | diags.Append(d...) 68 | if diags.HasError() { 69 | return NewSetValueOfUnknown[T](ctx), diags 70 | } 71 | 72 | return SetValueOf[T]{SetValue: v}, diags 73 | } 74 | 75 | func (t setTypeOf[T]) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { 76 | attrValue, err := t.SetType.ValueFromTerraform(ctx, in) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | setValue, ok := attrValue.(basetypes.SetValue) 82 | 83 | if !ok { 84 | return nil, fmt.Errorf("unexpected value type of %T", attrValue) 85 | } 86 | 87 | setValuable, diags := t.ValueFromSet(ctx, setValue) 88 | 89 | if diags.HasError() { 90 | return nil, fmt.Errorf("unexpected error converting SetValue to SetValuable: %v", diags) 91 | } 92 | 93 | return setValuable, nil 94 | } 95 | 96 | func (t setTypeOf[T]) ValueType(_ context.Context) attr.Value { 97 | return SetValueOf[T]{} 98 | } 99 | 100 | type SetValueOf[T attr.Value] struct { 101 | basetypes.SetValue 102 | } 103 | 104 | func (v SetValueOf[T]) Equal(o attr.Value) bool { 105 | other, ok := o.(SetValueOf[T]) 106 | 107 | if !ok { 108 | return false 109 | } 110 | 111 | return v.SetValue.Equal(other.SetValue) 112 | } 113 | 114 | func (v SetValueOf[T]) Type(ctx context.Context) attr.Type { 115 | return newSetTypeOf[T](ctx) 116 | } 117 | 118 | func NewSetValueOfNull[T attr.Value](ctx context.Context) SetValueOf[T] { 119 | return SetValueOf[T]{SetValue: basetypes.NewSetNull(newAttrTypeOf[T](ctx))} 120 | } 121 | 122 | func NewSetValueOfUnknown[T attr.Value](ctx context.Context) SetValueOf[T] { 123 | return SetValueOf[T]{SetValue: basetypes.NewSetUnknown(newAttrTypeOf[T](ctx))} 124 | } 125 | 126 | func NewSetValueOf[T attr.Value](ctx context.Context, elements []attr.Value) (SetValueOf[T], diag.Diagnostics) { 127 | var diags diag.Diagnostics 128 | 129 | v, d := basetypes.NewSetValue(newAttrTypeOf[T](ctx), elements) 130 | diags.Append(d...) 131 | if diags.HasError() { 132 | return NewSetValueOfUnknown[T](ctx), diags 133 | } 134 | 135 | return SetValueOf[T]{SetValue: v}, diags 136 | } 137 | 138 | func NewSetValueOfMust[T attr.Value](ctx context.Context, elements []attr.Value) SetValueOf[T] { 139 | return fwdiag.Must(NewSetValueOf[T](ctx, elements)) 140 | } 141 | -------------------------------------------------------------------------------- /framework/typehelpers/string.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/hashicorp/go-azure-helpers/lang/pointer" 11 | "github.com/hashicorp/terraform-plugin-framework/diag" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 13 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 14 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 15 | ) 16 | 17 | // WrappedStringValidator provides a wrapper for legacy SDKv2 type validations to ease migration to Framework Native 18 | // The provided function is tested against the value in the configuration and populates the diagnostics accordingly. 19 | type WrappedStringValidator struct { 20 | Func func(v interface{}, k string) (warnings []string, errors []error) 21 | Desc string 22 | MarkdownDesc string 23 | } 24 | 25 | func (w WrappedStringValidator) Description(_ context.Context) string { 26 | return w.Desc 27 | } 28 | 29 | func (w WrappedStringValidator) MarkdownDescription(_ context.Context) string { 30 | return w.MarkdownDesc 31 | } 32 | 33 | func (w WrappedStringValidator) ValidateString(_ context.Context, request validator.StringRequest, response *validator.StringResponse) { 34 | if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { 35 | return 36 | } 37 | 38 | value := request.ConfigValue.ValueString() 39 | path := request.Path.String() 40 | warnings, err := w.Func(value, path) 41 | if err != nil { 42 | response.Diagnostics.AddError(fmt.Sprintf("invalid value for %s", path), fmt.Sprintf("%+v", err)) 43 | return 44 | } 45 | 46 | if len(warnings) > 0 { // This may be redundant - legacy validators never really used warnings. 47 | for _, v := range warnings { 48 | response.Diagnostics.Append(diag.NewWarningDiagnostic(fmt.Sprintf("validating %s", path), v)) 49 | } 50 | } 51 | } 52 | 53 | var _ validator.String = &WrappedStringValidator{} 54 | 55 | type WrappedStringDefault struct { 56 | Desc *string 57 | Markdown *string 58 | Value string 59 | } 60 | 61 | var _ defaults.String = WrappedStringDefault{} 62 | 63 | // NewWrappedStringDefault is a helper function to return a new defaults.String implementation for any type that 64 | // implements the Go string type. 65 | func NewWrappedStringDefault[T ~string](value T) WrappedStringDefault { 66 | return WrappedStringDefault{ 67 | Value: string(value), 68 | } 69 | } 70 | 71 | func (w WrappedStringDefault) Description(_ context.Context) string { 72 | return pointer.From(w.Desc) 73 | } 74 | 75 | func (w WrappedStringDefault) MarkdownDescription(_ context.Context) string { 76 | return pointer.From(w.Markdown) 77 | } 78 | 79 | func (w WrappedStringDefault) DefaultString(_ context.Context, _ defaults.StringRequest, response *defaults.StringResponse) { 80 | d := basetypes.NewStringValue(w.Value) 81 | response.PlanValue = d 82 | } 83 | -------------------------------------------------------------------------------- /framework/typehelpers/timeouts.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package typehelpers 5 | 6 | import "time" 7 | 8 | type Timeouts struct { 9 | DefaultCreateTimeout time.Duration 10 | DefaultReadTimeout time.Duration 11 | DefaultUpdateTimeout time.Duration 12 | DefaultDeleteTimeout time.Duration 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/go-azure-helpers 2 | 3 | require ( 4 | github.com/Azure/go-autorest/autorest v0.11.30 5 | github.com/hashicorp/terraform-plugin-framework v1.14.1 6 | github.com/hashicorp/terraform-plugin-framework-validators v0.17.0 7 | github.com/hashicorp/terraform-plugin-go v0.26.0 8 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 9 | ) 10 | 11 | require ( 12 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 13 | github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect 14 | github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect 15 | github.com/Azure/go-autorest/logger v0.2.2 // indirect 16 | github.com/Azure/go-autorest/tracing v0.6.1 // indirect 17 | github.com/agext/levenshtein v1.2.3 // indirect 18 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 19 | github.com/fatih/color v1.18.0 // indirect 20 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 21 | github.com/golang/protobuf v1.5.4 // indirect 22 | github.com/google/go-cmp v0.7.0 // indirect 23 | github.com/hashicorp/go-cty v1.4.1 // indirect 24 | github.com/hashicorp/go-hclog v1.6.3 // indirect 25 | github.com/hashicorp/go-uuid v1.0.3 // indirect 26 | github.com/hashicorp/go-version v1.7.0 // indirect 27 | github.com/hashicorp/hcl/v2 v2.23.0 // indirect 28 | github.com/hashicorp/logutils v1.0.0 // indirect 29 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect 30 | github.com/mattn/go-colorable v0.1.14 // indirect 31 | github.com/mattn/go-isatty v0.0.20 // indirect 32 | github.com/mitchellh/copystructure v1.2.0 // indirect 33 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 34 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 35 | github.com/mitchellh/mapstructure v1.5.0 // indirect 36 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 37 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 38 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 39 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 40 | github.com/zclconf/go-cty v1.16.2 // indirect 41 | golang.org/x/crypto v0.36.0 // indirect 42 | golang.org/x/mod v0.24.0 // indirect 43 | golang.org/x/sync v0.12.0 // indirect 44 | golang.org/x/sys v0.31.0 // indirect 45 | golang.org/x/text v0.23.0 // indirect 46 | golang.org/x/tools v0.31.0 // indirect 47 | google.golang.org/appengine v1.6.8 // indirect 48 | google.golang.org/protobuf v1.36.5 // indirect 49 | ) 50 | 51 | go 1.24.1 52 | -------------------------------------------------------------------------------- /lang/dates/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package dates 5 | 6 | import ( 7 | "fmt" 8 | "time" 9 | ) 10 | 11 | // ParseAsFormat parses the given nilable string as a time.Time using the specified 12 | // format (for example RFC3339) 13 | func ParseAsFormat(input *string, format string) (*time.Time, error) { 14 | if input == nil { 15 | return nil, nil 16 | } 17 | 18 | val, err := time.Parse(format, *input) 19 | if err != nil { 20 | return nil, fmt.Errorf("parsing %q: %+v", *input, err) 21 | } 22 | 23 | return &val, nil 24 | } 25 | -------------------------------------------------------------------------------- /lang/pointer/from.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package pointer 5 | 6 | // FromBool turns a boolean into a pointer to a boolean 7 | func FromBool(input bool) *bool { 8 | return &input 9 | } 10 | 11 | // FromFloat64 turns a float64 into a pointer to a float64 12 | func FromFloat64(input float64) *float64 { 13 | return &input 14 | } 15 | 16 | // FromInt turns a int into a pointer to a int 17 | func FromInt(input int) *int { 18 | return &input 19 | } 20 | 21 | // FromInt64 turns a int64 into a pointer to a int64 22 | func FromInt64(input int64) *int64 { 23 | return &input 24 | } 25 | 26 | // FromMapOfStringInterfaces turns a map[string]interface{} into a pointer to a map[string]interface{} 27 | func FromMapOfStringInterfaces(input map[string]interface{}) *map[string]interface{} { 28 | return &input 29 | } 30 | 31 | // FromMapOfStringStrings turns a map[string]string into a pointer to a map[string]string 32 | func FromMapOfStringStrings(input map[string]string) *map[string]string { 33 | return &input 34 | } 35 | 36 | // FromSliceOfStrings turns a slice of stirngs into a pointer to a slice of strings 37 | func FromSliceOfStrings(input []string) *[]string { 38 | return &input 39 | } 40 | 41 | // FromString turns a string into a pointer to a string 42 | func FromString(input string) *string { 43 | return &input 44 | } 45 | -------------------------------------------------------------------------------- /lang/pointer/generic.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package pointer 5 | 6 | // From is a generic function that returns the value of a pointer 7 | // If the pointer is nil, a zero value for the underlying type of the pointer is returned. 8 | func From[T any](input *T) (output T) { 9 | var v T 10 | if input != nil { 11 | return *input 12 | } 13 | return v 14 | } 15 | 16 | // FromEnum is a helper function to return a string from a pointer to an Enum without having to cast it 17 | // example code simplification: 18 | // myStruct.SomeStringValue = string(pointer.From(model.EnumValue)) 19 | // becomes 20 | // myStruct.SomeStringValue = pointer.FromEnum(model.EnumValue) 21 | // if input is nil, returns an empty string 22 | func FromEnum[T ~string](input *T) (output string) { 23 | if input == nil { 24 | return "" 25 | } 26 | 27 | return string(*input) 28 | } 29 | 30 | // To is a generic function that returns a pointer to the value provided. 31 | func To[T any](input T) *T { 32 | return &input 33 | } 34 | 35 | // ToEnum is a helper function to cast strings as an Enum type where API objects expect a pointer to the Enum value 36 | // example code simplification: 37 | // APIModel.SomeValue = pointer.To(someservice.SomeEnumType(model.SomeVariable)) 38 | // becomes 39 | // APIModel.SomeValue = pointer.ToEnum[someservice.SomeEnumType](model.SomeVariable) 40 | func ToEnum[T ~string](input string) *T { 41 | result := T(input) 42 | return &result 43 | } 44 | -------------------------------------------------------------------------------- /lang/pointer/pointer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package pointer_test 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/hashicorp/go-azure-helpers/lang/pointer" 11 | ) 12 | 13 | func TestFrom_ListOfStrings(t *testing.T) { 14 | testCases := []struct { 15 | Input *[]string 16 | Expected []string 17 | }{ 18 | { 19 | Input: &[]string{}, 20 | Expected: []string{}, 21 | }, 22 | { 23 | Input: &[]string{"1", "2"}, 24 | Expected: []string{"1", "2"}, 25 | }, 26 | } 27 | 28 | for _, v := range testCases { 29 | actual := pointer.From(v.Input) 30 | if !reflect.DeepEqual(actual, v.Expected) { 31 | t.Fatalf("expectd %#v, got %#v", v.Expected, actual) 32 | } 33 | } 34 | } 35 | 36 | func TestFrom_NilTypes(t *testing.T) { 37 | var stringInput *string 38 | var intInput *int64 39 | var floatInput *float64 40 | type customType struct{} 41 | if actual := pointer.From(stringInput); actual != "" { 42 | t.Fatal("stringInput") 43 | } 44 | if actual := pointer.From(intInput); actual != 0 { 45 | t.Fatal("intInput") 46 | } 47 | if actual := pointer.From(floatInput); actual != 0 { 48 | t.Fatal("floatInput") 49 | } 50 | var custom *customType 51 | customExpected := customType{} 52 | if actual := pointer.From(custom); actual != customExpected { 53 | t.Fatal("customType") 54 | } 55 | var complex *map[string]customType 56 | complexExpected := map[string]customType{} 57 | if actual := pointer.From(complex); reflect.DeepEqual(actual, complexExpected) { 58 | t.Fatal("complexType") 59 | } 60 | } 61 | 62 | func TestFrom_MapOfInterface(t *testing.T) { 63 | testCases := []struct { 64 | Input *map[string]interface{} 65 | Expected map[string]interface{} 66 | }{ 67 | { 68 | Input: &map[string]interface{}{}, 69 | Expected: map[string]interface{}{}, 70 | }, 71 | { 72 | Input: &map[string]interface{}{ 73 | "foo": "bar", 74 | }, 75 | Expected: map[string]interface{}{ 76 | "foo": "bar", 77 | }, 78 | }, 79 | } 80 | 81 | for _, v := range testCases { 82 | actual := pointer.From(v.Input) 83 | if !reflect.DeepEqual(actual, v.Expected) { 84 | t.Fatalf("expectd %#v, got %#v", v.Expected, actual) 85 | } 86 | } 87 | } 88 | 89 | type SomeEnum string 90 | 91 | const ( 92 | SomeEnumFoo SomeEnum = "Foo" 93 | SomeEnumBar SomeEnum = "Bar" 94 | ) 95 | 96 | func TestToEnum(t *testing.T) { 97 | testCases := []struct { 98 | Input string 99 | Expected SomeEnum 100 | }{ 101 | { 102 | Input: "Foo", 103 | Expected: SomeEnumFoo, 104 | }, 105 | { 106 | Input: "Bar", 107 | Expected: SomeEnumBar, 108 | }, 109 | { 110 | Input: "Baz", 111 | Expected: SomeEnum("Baz"), 112 | }, 113 | { 114 | Input: "", 115 | Expected: SomeEnum(""), 116 | }, 117 | } 118 | 119 | for _, v := range testCases { 120 | actual := pointer.ToEnum[SomeEnum](v.Input) 121 | if *actual != v.Expected { 122 | t.Fatalf("expectd %#v, got %#v", v.Expected, *actual) 123 | } 124 | } 125 | } 126 | 127 | func TestFromEnum(t *testing.T) { 128 | testCases := []struct { 129 | Input *SomeEnum 130 | Expected string 131 | }{ 132 | { 133 | Input: pointer.To(SomeEnumFoo), 134 | Expected: "Foo", 135 | }, 136 | { 137 | Input: pointer.To(SomeEnumBar), 138 | Expected: "Bar", 139 | }, 140 | { 141 | Input: pointer.To(SomeEnum("")), 142 | Expected: "", 143 | }, 144 | { 145 | Input: pointer.To(SomeEnum("Baz")), 146 | Expected: "Baz", 147 | }, 148 | } 149 | 150 | for _, v := range testCases { 151 | actual := pointer.FromEnum(v.Input) 152 | if actual != v.Expected { 153 | t.Fatalf("expectd %#v, got %#v", v.Expected, actual) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lang/pointer/to.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package pointer 5 | 6 | // ToBool turns a pointer to a bool into a bool, returning the default value for a bool if it's nil 7 | func ToBool(input *bool) bool { 8 | if input != nil { 9 | return *input 10 | } 11 | 12 | return false 13 | } 14 | 15 | // ToFloat64 turns a pointer to a float64 into a float64, returning the default value for a float64 if it's nil 16 | func ToFloat64(input *float64) float64 { 17 | if input != nil { 18 | return *input 19 | } 20 | 21 | return 0.0 22 | } 23 | 24 | // ToInt turns a pointer to an int into an int, returning the default value for an int if it's nil 25 | func ToInt(input *int) int { 26 | if input != nil { 27 | return *input 28 | } 29 | 30 | return 0 31 | } 32 | 33 | // ToInt64 turns a pointer to an int64 into an int64, returning the default value for an int64 if it's nil 34 | func ToInt64(input *int64) int64 { 35 | if input != nil { 36 | return *input 37 | } 38 | 39 | return 0 40 | } 41 | 42 | // ToMapOfStringInterfaces turns a pointer to a map[string]interface{} into a map[string]interface{} 43 | // returning an empty map[string]interface{} if it's nil 44 | func ToMapOfStringInterfaces(input *map[string]interface{}) map[string]interface{} { 45 | if input != nil { 46 | return *input 47 | } 48 | 49 | return map[string]interface{}{} 50 | } 51 | 52 | // ToMapOfStringStrings turns a pointer to a map[string]string into a map[string]string returning 53 | // an empty map[string]string if it's nil 54 | func ToMapOfStringStrings(input *map[string]string) map[string]string { 55 | if input != nil { 56 | return *input 57 | } 58 | 59 | return map[string]string{} 60 | } 61 | 62 | // ToSliceOfStrings turns a pointer to a slice of strings into a slice of strings returning 63 | // an empty slice of strings if it's nil 64 | func ToSliceOfStrings(input *[]string) []string { 65 | if input != nil { 66 | return *input 67 | } 68 | 69 | return []string{} 70 | } 71 | 72 | // ToString turns a pointer to a string into a string, returning an empty string if it's nil 73 | func ToString(input *string) string { 74 | if input != nil { 75 | return *input 76 | } 77 | 78 | return "" 79 | } 80 | -------------------------------------------------------------------------------- /lang/response/response.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package response 5 | 6 | import ( 7 | "net/http" 8 | ) 9 | 10 | // WasBadRequest returns true if the HttpResponse is non-nil and has a status code of BadRequest 11 | func WasBadRequest(resp *http.Response) bool { 12 | return WasStatusCode(resp, http.StatusBadRequest) 13 | } 14 | 15 | // WasConflict returns true if the HttpResponse is non-nil and has a status code of Conflict 16 | func WasConflict(resp *http.Response) bool { 17 | return WasStatusCode(resp, http.StatusConflict) 18 | } 19 | 20 | // WasForbidden returns true if the HttpResponse is non-nil and has a status code of Forbidden 21 | func WasForbidden(resp *http.Response) bool { 22 | return WasStatusCode(resp, http.StatusForbidden) 23 | } 24 | 25 | // WasNotFound returns true if the HttpResponse is non-nil and has a status code of NotFound 26 | func WasNotFound(resp *http.Response) bool { 27 | return WasStatusCode(resp, http.StatusNotFound) 28 | } 29 | 30 | // WasStatusCode returns true if the HttpResponse is non-nil and matches the Status Code 31 | // It's recommended to use WasBadRequest/WasConflict/WasNotFound where possible instead 32 | func WasStatusCode(resp *http.Response, statusCode int) bool { 33 | if r := resp; r != nil { 34 | if r.StatusCode == statusCode { 35 | return true 36 | } 37 | } 38 | 39 | return false 40 | } 41 | -------------------------------------------------------------------------------- /polling/poller.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package polling 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "strings" 11 | 12 | "github.com/Azure/go-autorest/autorest" 13 | "github.com/Azure/go-autorest/autorest/azure" 14 | "github.com/hashicorp/go-azure-helpers/lang/response" 15 | ) 16 | 17 | type LongRunningPoller struct { 18 | // HttpResponse is the latest HTTP Response 19 | HttpResponse *http.Response 20 | 21 | future *azure.Future 22 | ctx context.Context 23 | client autorest.Client 24 | method string 25 | } 26 | 27 | // NewLongRunningPollerFromResponse creates a new LongRunningPoller from the HTTP Response 28 | // this is deprecated and replaced by NewPollerFromResponse. Can be removed once all the 29 | // embedded SDKs have been removed. 30 | func NewLongRunningPollerFromResponse(ctx context.Context, resp *http.Response, client autorest.Client) (LongRunningPoller, error) { 31 | poller := LongRunningPoller{ 32 | ctx: ctx, 33 | client: client, 34 | } 35 | future, err := azure.NewFutureFromResponse(resp) 36 | if err != nil { 37 | return poller, err 38 | } 39 | poller.future = &future 40 | return poller, nil 41 | } 42 | 43 | // NewPollerFromResponse creates a new LongRunningPoller from the HTTP Response 44 | func NewPollerFromResponse(ctx context.Context, resp *http.Response, client autorest.Client, method string) (LongRunningPoller, error) { 45 | poller := LongRunningPoller{ 46 | ctx: ctx, 47 | client: client, 48 | method: method, 49 | } 50 | future, err := azure.NewFutureFromResponse(resp) 51 | if err != nil { 52 | return poller, err 53 | } 54 | poller.future = &future 55 | return poller, nil 56 | } 57 | 58 | // PollUntilDone polls until this Long Running Poller is completed 59 | func (fw *LongRunningPoller) PollUntilDone() error { 60 | if fw.future == nil { 61 | return fmt.Errorf("internal error: cannot poll on a nil-future") 62 | } 63 | 64 | err := fw.future.WaitForCompletionRef(fw.ctx, fw.client) 65 | fw.HttpResponse = fw.future.Response() 66 | if strings.EqualFold(fw.method, "DELETE") { 67 | if response.WasNotFound(fw.HttpResponse) { 68 | err = nil 69 | } 70 | } 71 | return err 72 | } 73 | -------------------------------------------------------------------------------- /resourcemanager/commonids/app_service_function_app.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import "fmt" 7 | 8 | // NOTE: A Function App is just an App Service instance - but to allow for potential differentiation in the future 9 | // we're wrapping this to provide a unique Type, Parse and Validation functions. 10 | 11 | type FunctionAppId = AppServiceId 12 | 13 | // ParseFunctionAppID parses 'input' into a FunctionAppId 14 | func ParseFunctionAppID(input string) (*FunctionAppId, error) { 15 | parsed, err := ParseAppServiceID(input) 16 | if err != nil { 17 | return nil, fmt.Errorf("parsing %q as a Function App ID: %+v", input, err) 18 | } 19 | 20 | return &FunctionAppId{ 21 | SubscriptionId: parsed.SubscriptionId, 22 | ResourceGroupName: parsed.ResourceGroupName, 23 | SiteName: parsed.SiteName, 24 | }, nil 25 | } 26 | 27 | // ParseFunctionAppIDInsensitively parses 'input' case-insensitively into a FunctionAppId 28 | // note: this method should only be used for API response data and not user input 29 | func ParseFunctionAppIDInsensitively(input string) (*FunctionAppId, error) { 30 | parsed, err := ParseAppServiceIDInsensitively(input) 31 | if err != nil { 32 | return nil, fmt.Errorf("parsing %q as a Function App ID: %+v", input, err) 33 | } 34 | 35 | return &FunctionAppId{ 36 | SubscriptionId: parsed.SubscriptionId, 37 | ResourceGroupName: parsed.ResourceGroupName, 38 | SiteName: parsed.SiteName, 39 | }, nil 40 | } 41 | 42 | // ValidateFunctionAppID checks that 'input' can be parsed as a Function App ID 43 | func ValidateFunctionAppID(input interface{}, key string) (warnings []string, errors []error) { 44 | v, ok := input.(string) 45 | if !ok { 46 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 47 | return 48 | } 49 | 50 | if _, err := ParseFunctionAppID(v); err != nil { 51 | errors = append(errors, err) 52 | } 53 | 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /resourcemanager/commonids/app_service_logic_app.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import "fmt" 7 | 8 | // NOTE: A Logic App is just an App Service instance - but to allow for potential differentiation in the future 9 | // we're wrapping this to provide a unique Type, Parse and Validation functions. 10 | 11 | type LogicAppId = AppServiceId 12 | 13 | // ParseLogicAppId parses 'input' into a LogicAppId 14 | func ParseLogicAppId(input string) (*LogicAppId, error) { 15 | parsed, err := ParseAppServiceID(input) 16 | if err != nil { 17 | return nil, fmt.Errorf("parsing %q as a Logic App ID: %+v", input, err) 18 | } 19 | 20 | return &LogicAppId{ 21 | SubscriptionId: parsed.SubscriptionId, 22 | ResourceGroupName: parsed.ResourceGroupName, 23 | SiteName: parsed.SiteName, 24 | }, nil 25 | } 26 | 27 | // ParseLogicAppIdInsensitively parses 'input' case-insensitively into a LogicAppId 28 | // note: this method should only be used for API response data and not user input 29 | func ParseLogicAppIdInsensitively(input string) (*LogicAppId, error) { 30 | parsed, err := ParseAppServiceIDInsensitively(input) 31 | if err != nil { 32 | return nil, fmt.Errorf("parsing %q as a Logic App ID: %+v", input, err) 33 | } 34 | 35 | return &LogicAppId{ 36 | SubscriptionId: parsed.SubscriptionId, 37 | ResourceGroupName: parsed.ResourceGroupName, 38 | SiteName: parsed.SiteName, 39 | }, nil 40 | } 41 | 42 | // ValidateLogicAppId checks that 'input' can be parsed as a Logic App ID 43 | func ValidateLogicAppId(input interface{}, key string) (warnings []string, errors []error) { 44 | v, ok := input.(string) 45 | if !ok { 46 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 47 | return 48 | } 49 | 50 | if _, err := ParseLogicAppId(v); err != nil { 51 | errors = append(errors, err) 52 | } 53 | 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /resourcemanager/commonids/app_service_web_app.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import "fmt" 7 | 8 | // NOTE: A Web App is just an App Service instance - but to allow for potential differentiation in the future 9 | // we're wrapping this to provide a unique Type, Parse and Validation functions. 10 | 11 | type WebAppId = AppServiceId 12 | 13 | // ParseWebAppID parses 'input' into a WebAppId 14 | func ParseWebAppID(input string) (*WebAppId, error) { 15 | parsed, err := ParseAppServiceID(input) 16 | if err != nil { 17 | return nil, fmt.Errorf("parsing %q as a Web App ID: %+v", input, err) 18 | } 19 | 20 | return &WebAppId{ 21 | SubscriptionId: parsed.SubscriptionId, 22 | ResourceGroupName: parsed.ResourceGroupName, 23 | SiteName: parsed.SiteName, 24 | }, nil 25 | } 26 | 27 | // ParseWebAppIDInsensitively parses 'input' case-insensitively into a WebAppId 28 | // note: this method should only be used for API response data and not user input 29 | func ParseWebAppIDInsensitively(input string) (*WebAppId, error) { 30 | parsed, err := ParseAppServiceIDInsensitively(input) 31 | if err != nil { 32 | return nil, fmt.Errorf("parsing %q as a Web App ID: %+v", input, err) 33 | } 34 | 35 | return &WebAppId{ 36 | SubscriptionId: parsed.SubscriptionId, 37 | ResourceGroupName: parsed.ResourceGroupName, 38 | SiteName: parsed.SiteName, 39 | }, nil 40 | } 41 | 42 | // ValidateWebAppID checks that 'input' can be parsed as a Web App Id 43 | func ValidateWebAppID(input interface{}, key string) (warnings []string, errors []error) { 44 | v, ok := input.(string) 45 | if !ok { 46 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 47 | return 48 | } 49 | 50 | if _, err := ParseWebAppID(v); err != nil { 51 | errors = append(errors, err) 52 | } 53 | 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /resourcemanager/commonids/billing_enrollment_account.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 11 | ) 12 | 13 | var _ resourceids.ResourceId = &BillingEnrollmentAccountId{} 14 | 15 | // BillingEnrollmentAccountId is a struct representing the Resource ID for a Billing Enrollment Account 16 | type BillingEnrollmentAccountId struct { 17 | EnrollmentAccountName string 18 | } 19 | 20 | // NewBillingEnrollmentAccountID returns a new BillingEnrollmentAccountId struct 21 | func NewBillingEnrollmentAccountID(enrollmentAccountName string) BillingEnrollmentAccountId { 22 | return BillingEnrollmentAccountId{ 23 | EnrollmentAccountName: enrollmentAccountName, 24 | } 25 | } 26 | 27 | // ParseBillingEnrollmentAccountID parses 'input' into a BillingEnrollmentAccountId 28 | func ParseBillingEnrollmentAccountID(input string) (*BillingEnrollmentAccountId, error) { 29 | parser := resourceids.NewParserFromResourceIdType(&BillingEnrollmentAccountId{}) 30 | parsed, err := parser.Parse(input, false) 31 | if err != nil { 32 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 33 | } 34 | 35 | id := BillingEnrollmentAccountId{} 36 | if err = id.FromParseResult(*parsed); err != nil { 37 | return nil, err 38 | } 39 | 40 | return &id, nil 41 | } 42 | 43 | // ParseBillingEnrollmentAccountIDInsensitively parses 'input' case-insensitively into a BillingEnrollmentAccountId 44 | // note: this method should only be used for API response data and not user input 45 | func ParseBillingEnrollmentAccountIDInsensitively(input string) (*BillingEnrollmentAccountId, error) { 46 | parser := resourceids.NewParserFromResourceIdType(&BillingEnrollmentAccountId{}) 47 | parsed, err := parser.Parse(input, true) 48 | if err != nil { 49 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 50 | } 51 | 52 | id := BillingEnrollmentAccountId{} 53 | if err = id.FromParseResult(*parsed); err != nil { 54 | return nil, err 55 | } 56 | 57 | return &id, nil 58 | } 59 | 60 | func (id *BillingEnrollmentAccountId) FromParseResult(input resourceids.ParseResult) error { 61 | var ok bool 62 | 63 | if id.EnrollmentAccountName, ok = input.Parsed["enrollmentAccountName"]; !ok { 64 | return resourceids.NewSegmentNotSpecifiedError(id, "enrollmentAccountName", input) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // ValidateBillingEnrollmentAccountID checks that 'input' can be parsed as a Billing Enrollment Account ID 71 | func ValidateBillingEnrollmentAccountID(input interface{}, key string) (warnings []string, errors []error) { 72 | v, ok := input.(string) 73 | if !ok { 74 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 75 | return 76 | } 77 | 78 | if _, err := ParseBillingEnrollmentAccountID(v); err != nil { 79 | errors = append(errors, err) 80 | } 81 | 82 | return 83 | } 84 | 85 | // ID returns the formatted Billing Enrollment Account ID 86 | func (id BillingEnrollmentAccountId) ID() string { 87 | fmtString := "/providers/Microsoft.Billing/enrollmentAccounts/%s" 88 | return fmt.Sprintf(fmtString, id.EnrollmentAccountName) 89 | } 90 | 91 | // Segments returns a slice of Resource ID Segments which comprise this Billing Enrollment Account ID 92 | func (id BillingEnrollmentAccountId) Segments() []resourceids.Segment { 93 | return []resourceids.Segment{ 94 | resourceids.StaticSegment("providers", "providers", "providers"), 95 | resourceids.ResourceProviderSegment("resourceProvider", "Microsoft.Billing", "Microsoft.Billing"), 96 | resourceids.StaticSegment("enrollmentAccounts", "enrollmentAccounts", "enrollmentAccounts"), 97 | resourceids.UserSpecifiedSegment("enrollmentAccountName", "enrollmentAccountValue"), 98 | } 99 | } 100 | 101 | // String returns a human-readable description of this Billing Enrollment Account ID 102 | func (id BillingEnrollmentAccountId) String() string { 103 | components := []string{ 104 | fmt.Sprintf("Enrollment Account Name: %q", id.EnrollmentAccountName), 105 | } 106 | return fmt.Sprintf("Billing Enrollment Account (%s)", strings.Join(components, "\n")) 107 | } 108 | -------------------------------------------------------------------------------- /resourcemanager/commonids/chaos_studio_target.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 11 | ) 12 | 13 | var _ resourceids.ResourceId = &ChaosStudioTargetId{} 14 | 15 | // ChaosStudioTargetId is a struct representing the Resource ID for an App Service Plan 16 | type ChaosStudioTargetId struct { 17 | Scope string 18 | TargetName string 19 | } 20 | 21 | // NewChaosStudioTargetID returns a new ChaosStudioTargetId struct 22 | func NewChaosStudioTargetID(scope string, targetName string) ChaosStudioTargetId { 23 | return ChaosStudioTargetId{ 24 | Scope: scope, 25 | TargetName: targetName, 26 | } 27 | } 28 | 29 | // ParseChaosStudioTargetID parses 'input' into a ChaosStudioTargetId 30 | func ParseChaosStudioTargetID(input string) (*ChaosStudioTargetId, error) { 31 | parser := resourceids.NewParserFromResourceIdType(&ChaosStudioTargetId{}) 32 | parsed, err := parser.Parse(input, false) 33 | if err != nil { 34 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 35 | } 36 | 37 | id := ChaosStudioTargetId{} 38 | if err := id.FromParseResult(*parsed); err != nil { 39 | return nil, err 40 | } 41 | 42 | return &id, nil 43 | } 44 | 45 | // ParseChaosStudioTargetIDInsensitively parses 'input' case-insensitively into a ChaosStudioTargetId 46 | // note: this method should only be used for API response data and not user input 47 | func ParseChaosStudioTargetIDInsensitively(input string) (*ChaosStudioTargetId, error) { 48 | parser := resourceids.NewParserFromResourceIdType(&ChaosStudioTargetId{}) 49 | parsed, err := parser.Parse(input, true) 50 | if err != nil { 51 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 52 | } 53 | 54 | id := ChaosStudioTargetId{} 55 | if err = id.FromParseResult(*parsed); err != nil { 56 | return nil, err 57 | } 58 | 59 | return &id, nil 60 | } 61 | 62 | func (id *ChaosStudioTargetId) FromParseResult(input resourceids.ParseResult) error { 63 | var ok bool 64 | 65 | if id.Scope, ok = input.Parsed["scope"]; !ok { 66 | return resourceids.NewSegmentNotSpecifiedError(id, "scope", input) 67 | } 68 | 69 | if id.TargetName, ok = input.Parsed["targetName"]; !ok { 70 | return resourceids.NewSegmentNotSpecifiedError(id, "targetName", input) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // ValidateChaosStudioTargetID checks that 'input' can be parsed as an App Service Plan ID 77 | func ValidateChaosStudioTargetID(input interface{}, key string) (warnings []string, errors []error) { 78 | v, ok := input.(string) 79 | if !ok { 80 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 81 | return 82 | } 83 | 84 | if _, err := ParseChaosStudioTargetID(v); err != nil { 85 | errors = append(errors, err) 86 | } 87 | 88 | return 89 | } 90 | 91 | // ID returns the formatted App Service Plan ID 92 | func (id ChaosStudioTargetId) ID() string { 93 | fmtString := "%s/providers/Microsoft.Chaos/targets/%s" 94 | return fmt.Sprintf(fmtString, id.Scope, id.TargetName) 95 | } 96 | 97 | // Segments returns a slice of Resource ID Segments which comprise this App Service Plan ID 98 | func (id ChaosStudioTargetId) Segments() []resourceids.Segment { 99 | return []resourceids.Segment{ 100 | resourceids.ScopeSegment("scope", "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group"), 101 | resourceids.StaticSegment("staticProviders", "providers", "providers"), 102 | resourceids.ResourceProviderSegment("staticMicrosoftWeb", "Microsoft.Chaos", "Microsoft.Chaos"), 103 | resourceids.StaticSegment("staticTargets", "targets", "targets"), 104 | resourceids.UserSpecifiedSegment("targetName", "targetName"), 105 | } 106 | } 107 | 108 | // String returns a human-readable description of this App Service Plan ID 109 | func (id ChaosStudioTargetId) String() string { 110 | components := []string{ 111 | fmt.Sprintf("Scope: %q", id.Scope), 112 | fmt.Sprintf("Target Name: %q", id.TargetName), 113 | } 114 | return fmt.Sprintf("Chaos Studio Target (%s)", strings.Join(components, "\n")) 115 | } 116 | -------------------------------------------------------------------------------- /resourcemanager/commonids/community_gallery_image.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 11 | ) 12 | 13 | var _ resourceids.ResourceId = &CommunityGalleryImageId{} 14 | 15 | // CommunityGalleryImageId is a struct representing the Resource ID for a Community Gallery Image 16 | type CommunityGalleryImageId struct { 17 | CommunityGalleryName string 18 | ImageName string 19 | } 20 | 21 | // NewCommunityGalleryImageID returns a new CommunityGalleryImageId struct 22 | func NewCommunityGalleryImageID(communityGallery string, image string) CommunityGalleryImageId { 23 | return CommunityGalleryImageId{ 24 | CommunityGalleryName: communityGallery, 25 | ImageName: image, 26 | } 27 | } 28 | 29 | // ParseCommunityGalleryImageID parses 'input' into a CommunityGalleryImageId 30 | func ParseCommunityGalleryImageID(input string) (*CommunityGalleryImageId, error) { 31 | parser := resourceids.NewParserFromResourceIdType(&CommunityGalleryImageId{}) 32 | parsed, err := parser.Parse(input, false) 33 | if err != nil { 34 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 35 | } 36 | 37 | id := CommunityGalleryImageId{} 38 | if err = id.FromParseResult(*parsed); err != nil { 39 | return nil, err 40 | } 41 | 42 | return &id, nil 43 | } 44 | 45 | // ParseCommunityGalleryImageIDInsensitively parses 'input' case-insensitively into a CommunityGalleryImageId 46 | // note: this method should only be used for API response data and not user input 47 | func ParseCommunityGalleryImageIDInsensitively(input string) (*CommunityGalleryImageId, error) { 48 | parser := resourceids.NewParserFromResourceIdType(&CommunityGalleryImageId{}) 49 | parsed, err := parser.Parse(input, true) 50 | if err != nil { 51 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 52 | } 53 | 54 | id := CommunityGalleryImageId{} 55 | if err = id.FromParseResult(*parsed); err != nil { 56 | return nil, err 57 | } 58 | 59 | return &id, nil 60 | } 61 | 62 | func (id *CommunityGalleryImageId) FromParseResult(input resourceids.ParseResult) error { 63 | var ok bool 64 | 65 | if id.CommunityGalleryName, ok = input.Parsed["communityGalleryName"]; !ok { 66 | return resourceids.NewSegmentNotSpecifiedError(id, "communityGalleryName", input) 67 | } 68 | 69 | if id.ImageName, ok = input.Parsed["imageName"]; !ok { 70 | return resourceids.NewSegmentNotSpecifiedError(id, "imageName", input) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // ValidateCommunityGalleryImageID checks that 'input' can be parsed as a Community Gallery Image ID 77 | func ValidateCommunityGalleryImageID(input interface{}, key string) (warnings []string, errors []error) { 78 | v, ok := input.(string) 79 | if !ok { 80 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 81 | return 82 | } 83 | 84 | if _, err := ParseCommunityGalleryImageID(v); err != nil { 85 | errors = append(errors, err) 86 | } 87 | 88 | return 89 | } 90 | 91 | // ID returns the formatted Community Gallery Image ID 92 | func (id CommunityGalleryImageId) ID() string { 93 | fmtString := "/communityGalleries/%s/images/%s" 94 | return fmt.Sprintf(fmtString, id.CommunityGalleryName, id.ImageName) 95 | } 96 | 97 | // Segments returns a slice of Resource ID Segments which comprise this Community Gallery Image ID 98 | func (id CommunityGalleryImageId) Segments() []resourceids.Segment { 99 | return []resourceids.Segment{ 100 | resourceids.StaticSegment("staticCommunityGalleries", "communityGalleries", "communityGalleries"), 101 | resourceids.UserSpecifiedSegment("communityGalleryName", "communityGalleryValue"), 102 | resourceids.StaticSegment("staticImages", "images", "images"), 103 | resourceids.UserSpecifiedSegment("imageName", "imageValue"), 104 | } 105 | } 106 | 107 | // String returns a human-readable description of this Community Gallery Image ID 108 | func (id CommunityGalleryImageId) String() string { 109 | components := []string{ 110 | fmt.Sprintf("Community Gallery Name: %q", id.CommunityGalleryName), 111 | fmt.Sprintf("Image Name: %q", id.ImageName), 112 | } 113 | return fmt.Sprintf("Community Gallery Image (%s)", strings.Join(components, "\n")) 114 | } 115 | -------------------------------------------------------------------------------- /resourcemanager/commonids/ids.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 8 | ) 9 | 10 | func CommonIds() []resourceids.ResourceId { 11 | return []resourceids.ResourceId{ 12 | &AppServiceId{}, 13 | &AppServiceEnvironmentId{}, 14 | &AppServicePlanId{}, 15 | &AutomationCompilationJobId{}, 16 | &AvailabilitySetId{}, 17 | &BotServiceId{}, 18 | &BotServiceChannelId{}, 19 | &ChaosStudioCapabilityId{}, 20 | &ChaosStudioTargetId{}, 21 | &CloudServicesIPConfigurationId{}, 22 | &CloudServicesPublicIPAddressId{}, 23 | &DedicatedHostId{}, 24 | &DedicatedHostGroupId{}, 25 | &DevCenterId{}, 26 | &DiskEncryptionSetId{}, 27 | &ExpressRouteCircuitPeeringId{}, 28 | &HDInsightClusterId{}, 29 | &HyperVSiteJobId{}, 30 | &HyperVSiteMachineId{}, 31 | &HyperVSiteRunAsAccountId{}, 32 | &KeyVaultId{}, 33 | &KeyVaultKeyId{}, 34 | &KeyVaultKeyVersionId{}, 35 | &KeyVaultPrivateEndpointConnectionId{}, 36 | &KubernetesClusterId{}, 37 | &KubernetesFleetId{}, 38 | &KustoClusterId{}, 39 | &KustoDatabaseId{}, 40 | &ManagedDiskId{}, 41 | &ManagementGroupId{}, 42 | &NetworkInterfaceId{}, 43 | &NetworkInterfaceIPConfigurationId{}, 44 | &NetworkInterfaceId{}, 45 | &ProvisioningServiceId{}, 46 | &PublicIPAddressId{}, 47 | &ResourceGroupId{}, 48 | &SharedImageGalleryId{}, 49 | &SpringCloudServiceId{}, 50 | &SqlDatabaseId{}, 51 | &SqlElasticPoolId{}, 52 | &SqlManagedInstanceId{}, 53 | &SqlManagedInstanceDatabaseId{}, 54 | &SqlServerId{}, 55 | &StorageAccountId{}, 56 | &StorageContainerId{}, 57 | &SubnetId{}, 58 | &SubscriptionId{}, 59 | &UserAssignedIdentityId{}, 60 | &VirtualHubBGPConnectionId{}, 61 | &VirtualHubIPConfigurationId{}, 62 | &VirtualMachineId{}, 63 | &VirtualMachineScaleSetNetworkInterfaceId{}, 64 | &VirtualMachineScaleSetPublicIPAddressId{}, 65 | &VirtualMachineScaleSetId{}, 66 | &VirtualNetworkId{}, 67 | &VirtualRouterPeeringId{}, 68 | &VirtualWANP2SVPNGatewayId{}, 69 | &VMwareSiteJobId{}, 70 | &VMwareSiteMachineId{}, 71 | &VMwareSiteRunAsAccountId{}, 72 | &VPNConnectionId{}, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /resourcemanager/commonids/management_group.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 11 | ) 12 | 13 | var _ resourceids.ResourceId = &ManagementGroupId{} 14 | 15 | // ManagementGroupId is a struct representing the Resource ID for a Management Group 16 | type ManagementGroupId struct { 17 | GroupId string 18 | } 19 | 20 | // NewManagementGroupID returns a new ManagementGroupId struct 21 | func NewManagementGroupID(groupId string) ManagementGroupId { 22 | return ManagementGroupId{ 23 | GroupId: groupId, 24 | } 25 | } 26 | 27 | // ParseManagementGroupID parses 'input' into a ManagementGroupId 28 | func ParseManagementGroupID(input string) (*ManagementGroupId, error) { 29 | parser := resourceids.NewParserFromResourceIdType(&ManagementGroupId{}) 30 | parsed, err := parser.Parse(input, false) 31 | if err != nil { 32 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 33 | } 34 | 35 | id := ManagementGroupId{} 36 | if err := id.FromParseResult(*parsed); err != nil { 37 | return nil, err 38 | } 39 | 40 | return &id, nil 41 | } 42 | 43 | // ParseManagementGroupIDInsensitively parses 'input' case-insensitively into a ManagementGroupId 44 | // note: this method should only be used for API response data and not user input 45 | func ParseManagementGroupIDInsensitively(input string) (*ManagementGroupId, error) { 46 | parser := resourceids.NewParserFromResourceIdType(&ManagementGroupId{}) 47 | parsed, err := parser.Parse(input, true) 48 | if err != nil { 49 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 50 | } 51 | 52 | id := ManagementGroupId{} 53 | if err = id.FromParseResult(*parsed); err != nil { 54 | return nil, err 55 | } 56 | 57 | return &id, nil 58 | } 59 | 60 | func (id *ManagementGroupId) FromParseResult(input resourceids.ParseResult) error { 61 | var ok bool 62 | 63 | if id.GroupId, ok = input.Parsed["groupId"]; !ok { 64 | return resourceids.NewSegmentNotSpecifiedError(id, "groupId", input) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // ValidateManagementGroupID checks that 'input' can be parsed as a Management Group ID 71 | func ValidateManagementGroupID(input interface{}, key string) (warnings []string, errors []error) { 72 | v, ok := input.(string) 73 | if !ok { 74 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 75 | return 76 | } 77 | 78 | if _, err := ParseManagementGroupID(v); err != nil { 79 | errors = append(errors, err) 80 | } 81 | 82 | return 83 | } 84 | 85 | // ID returns the formatted Management Group ID 86 | func (id ManagementGroupId) ID() string { 87 | fmtString := "/providers/Microsoft.Management/managementGroups/%s" 88 | return fmt.Sprintf(fmtString, id.GroupId) 89 | } 90 | 91 | // Segments returns a slice of Resource ID Segments which comprise this Management Group ID 92 | func (id ManagementGroupId) Segments() []resourceids.Segment { 93 | return []resourceids.Segment{ 94 | resourceids.StaticSegment("providers", "providers", "providers"), 95 | resourceids.ResourceProviderSegment("resourceProvider", "Microsoft.Management", "Microsoft.Management"), 96 | resourceids.StaticSegment("managementGroups", "managementGroups", "managementGroups"), 97 | resourceids.UserSpecifiedSegment("groupId", "groupIdValue"), 98 | } 99 | } 100 | 101 | // String returns a human-readable description of this Management Group ID 102 | func (id ManagementGroupId) String() string { 103 | components := []string{ 104 | fmt.Sprintf("Group: %q", id.GroupId), 105 | } 106 | return fmt.Sprintf("Management Group (%s)", strings.Join(components, "\n")) 107 | } 108 | -------------------------------------------------------------------------------- /resourcemanager/commonids/resource_group.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 11 | ) 12 | 13 | var _ resourceids.ResourceId = &ResourceGroupId{} 14 | 15 | // ResourceGroupId is a struct representing the Resource ID for a Resource Group 16 | type ResourceGroupId struct { 17 | SubscriptionId string 18 | ResourceGroupName string 19 | } 20 | 21 | // NewResourceGroupID returns a new ResourceGroupId struct 22 | func NewResourceGroupID(subscriptionId string, resourceGroupName string) ResourceGroupId { 23 | return ResourceGroupId{ 24 | SubscriptionId: subscriptionId, 25 | ResourceGroupName: resourceGroupName, 26 | } 27 | } 28 | 29 | // ParseResourceGroupID parses 'input' into a ResourceGroupId 30 | func ParseResourceGroupID(input string) (*ResourceGroupId, error) { 31 | parser := resourceids.NewParserFromResourceIdType(&ResourceGroupId{}) 32 | parsed, err := parser.Parse(input, false) 33 | if err != nil { 34 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 35 | } 36 | 37 | id := ResourceGroupId{} 38 | if err := id.FromParseResult(*parsed); err != nil { 39 | return nil, err 40 | } 41 | 42 | return &id, nil 43 | } 44 | 45 | // ParseResourceGroupIDInsensitively parses 'input' case-insensitively into a ResourceGroupId 46 | // note: this method should only be used for API response data and not user input 47 | func ParseResourceGroupIDInsensitively(input string) (*ResourceGroupId, error) { 48 | parser := resourceids.NewParserFromResourceIdType(&ResourceGroupId{}) 49 | parsed, err := parser.Parse(input, true) 50 | if err != nil { 51 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 52 | } 53 | 54 | id := ResourceGroupId{} 55 | if err = id.FromParseResult(*parsed); err != nil { 56 | return nil, err 57 | } 58 | 59 | return &id, nil 60 | } 61 | 62 | func (id *ResourceGroupId) FromParseResult(input resourceids.ParseResult) error { 63 | var ok bool 64 | 65 | if id.SubscriptionId, ok = input.Parsed["subscriptionId"]; !ok { 66 | return resourceids.NewSegmentNotSpecifiedError(id, "subscriptionId", input) 67 | } 68 | 69 | if id.ResourceGroupName, ok = input.Parsed["resourceGroupName"]; !ok { 70 | return resourceids.NewSegmentNotSpecifiedError(id, "resourceGroupName", input) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // ValidateResourceGroupID checks that 'input' can be parsed as a Resource Group ID 77 | func ValidateResourceGroupID(input interface{}, key string) (warnings []string, errors []error) { 78 | v, ok := input.(string) 79 | if !ok { 80 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 81 | return 82 | } 83 | 84 | if _, err := ParseResourceGroupID(v); err != nil { 85 | errors = append(errors, err) 86 | } 87 | 88 | return 89 | } 90 | 91 | // ID returns the formatted Resource Group ID 92 | func (id ResourceGroupId) ID() string { 93 | fmtString := "/subscriptions/%s/resourceGroups/%s" 94 | return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroupName) 95 | } 96 | 97 | // Segments returns a slice of Resource ID Segments which comprise this Resource Group ID 98 | func (id ResourceGroupId) Segments() []resourceids.Segment { 99 | return []resourceids.Segment{ 100 | resourceids.StaticSegment("subscriptions", "subscriptions", "subscriptions"), 101 | resourceids.SubscriptionIdSegment("subscriptionId", "12345678-1234-9876-4563-123456789012"), 102 | resourceids.StaticSegment("resourceGroups", "resourceGroups", "resourceGroups"), 103 | resourceids.ResourceGroupSegment("resourceGroupName", "example-resource-group"), 104 | } 105 | } 106 | 107 | // String returns a human-readable description of this Resource Group ID 108 | func (id ResourceGroupId) String() string { 109 | components := []string{ 110 | fmt.Sprintf("Subscription: %q", id.SubscriptionId), 111 | fmt.Sprintf("Resource Group Name: %q", id.ResourceGroupName), 112 | } 113 | return fmt.Sprintf("Resource Group (%s)", strings.Join(components, "\n")) 114 | } 115 | -------------------------------------------------------------------------------- /resourcemanager/commonids/scope.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 11 | ) 12 | 13 | var _ resourceids.ResourceId = &ScopeId{} 14 | 15 | // ScopeId is a struct representing the Resource ID for a Scope 16 | type ScopeId struct { 17 | Scope string 18 | } 19 | 20 | // NewScopeID returns a new ScopeId struct 21 | func NewScopeID(scope string) ScopeId { 22 | return ScopeId{ 23 | Scope: scope, 24 | } 25 | } 26 | 27 | // ParseScopeID parses 'input' into a ScopeId 28 | func ParseScopeID(input string) (*ScopeId, error) { 29 | parser := resourceids.NewParserFromResourceIdType(&ScopeId{}) 30 | parsed, err := parser.Parse(input, false) 31 | if err != nil { 32 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 33 | } 34 | 35 | id := ScopeId{} 36 | if err := id.FromParseResult(*parsed); err != nil { 37 | return nil, err 38 | } 39 | return &id, nil 40 | } 41 | 42 | // ParseScopeIDInsensitively parses 'input' case-insensitively into a ScopeId 43 | // note: this method should only be used for API response data and not user input 44 | func ParseScopeIDInsensitively(input string) (*ScopeId, error) { 45 | parser := resourceids.NewParserFromResourceIdType(&ScopeId{}) 46 | parsed, err := parser.Parse(input, true) 47 | if err != nil { 48 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 49 | } 50 | 51 | id := ScopeId{} 52 | if err = id.FromParseResult(*parsed); err != nil { 53 | return nil, err 54 | } 55 | 56 | return &id, nil 57 | } 58 | 59 | // ValidateScopeID checks that 'input' can be parsed as a Scope ID 60 | func ValidateScopeID(input interface{}, key string) (warnings []string, errors []error) { 61 | v, ok := input.(string) 62 | if !ok { 63 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 64 | return 65 | } 66 | 67 | if _, err := ParseScopeID(v); err != nil { 68 | errors = append(errors, err) 69 | } 70 | 71 | return 72 | } 73 | 74 | func (id *ScopeId) FromParseResult(input resourceids.ParseResult) error { 75 | var ok bool 76 | 77 | if id.Scope, ok = input.Parsed["scope"]; !ok { 78 | return resourceids.NewSegmentNotSpecifiedError(id, "scope", input) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // ID returns the formatted Scope ID 85 | func (id ScopeId) ID() string { 86 | fmtString := "/%s" 87 | return fmt.Sprintf(fmtString, strings.TrimPrefix(id.Scope, "/")) 88 | } 89 | 90 | // Segments returns a slice of Resource ID Segments which comprise this Scope ID 91 | func (id ScopeId) Segments() []resourceids.Segment { 92 | return []resourceids.Segment{ 93 | resourceids.ScopeSegment("scope", "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group"), 94 | } 95 | } 96 | 97 | // String returns a human-readable description of this Scope ID 98 | func (id ScopeId) String() string { 99 | components := []string{ 100 | fmt.Sprintf("Scope: %q", id.Scope), 101 | } 102 | return fmt.Sprintf("Scope (%s)", strings.Join(components, "\n")) 103 | } 104 | -------------------------------------------------------------------------------- /resourcemanager/commonids/scope_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 10 | ) 11 | 12 | var _ resourceids.ResourceId = &ScopeId{} 13 | 14 | func TestNewScopeID(t *testing.T) { 15 | id := NewScopeID("/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group") 16 | 17 | if id.Scope != "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group" { 18 | t.Fatalf("Expected %q but got %q for Segment 'Scope'", id.Scope, "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group") 19 | } 20 | } 21 | 22 | func TestFormatScopeID(t *testing.T) { 23 | actual := NewScopeID("/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group").ID() 24 | expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group" 25 | if actual != expected { 26 | t.Fatalf("Expected the Formatted ID to be %q but got %q", expected, actual) 27 | } 28 | } 29 | 30 | func TestParseScopeID(t *testing.T) { 31 | testData := []struct { 32 | Input string 33 | Error bool 34 | Expected *ScopeId 35 | }{ 36 | { 37 | // Incomplete URI 38 | Input: "", 39 | Error: true, 40 | }, 41 | { 42 | // Valid URI 43 | Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group", 44 | Expected: &ScopeId{ 45 | Scope: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group", 46 | }, 47 | }, 48 | } 49 | for _, v := range testData { 50 | t.Logf("[DEBUG] Testing %q", v.Input) 51 | 52 | actual, err := ParseScopeID(v.Input) 53 | if err != nil { 54 | if v.Error { 55 | continue 56 | } 57 | 58 | t.Fatalf("Expect a value but got an error: %+v", err) 59 | } 60 | if v.Error { 61 | t.Fatal("Expect an error but didn't get one") 62 | } 63 | 64 | if actual.Scope != v.Expected.Scope { 65 | t.Fatalf("Expected %q but got %q for Scope", v.Expected.Scope, actual.Scope) 66 | } 67 | 68 | } 69 | } 70 | 71 | func TestParseScopeIDInsensitively(t *testing.T) { 72 | testData := []struct { 73 | Input string 74 | Error bool 75 | Expected *ScopeId 76 | }{ 77 | { 78 | // Incomplete URI 79 | Input: "", 80 | Error: true, 81 | }, 82 | { 83 | // Valid URI 84 | Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group", 85 | Expected: &ScopeId{ 86 | Scope: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/some-resource-group", 87 | }, 88 | }, 89 | { 90 | // Valid URI (mIxEd CaSe since this is insensitive) 91 | Input: "/sUbScRiPtIoNs/12345678-1234-9876-4563-123456789012/rEsOuRcEgRoUpS/sOmE-ReSoUrCe-gRoUp", 92 | Expected: &ScopeId{ 93 | Scope: "/sUbScRiPtIoNs/12345678-1234-9876-4563-123456789012/rEsOuRcEgRoUpS/sOmE-ReSoUrCe-gRoUp", 94 | }, 95 | }, 96 | } 97 | for _, v := range testData { 98 | t.Logf("[DEBUG] Testing %q", v.Input) 99 | 100 | actual, err := ParseScopeIDInsensitively(v.Input) 101 | if err != nil { 102 | if v.Error { 103 | continue 104 | } 105 | 106 | t.Fatalf("Expect a value but got an error: %+v", err) 107 | } 108 | if v.Error { 109 | t.Fatal("Expect an error but didn't get one") 110 | } 111 | 112 | if actual.Scope != v.Expected.Scope { 113 | t.Fatalf("Expected %q but got %q for Scope", v.Expected.Scope, actual.Scope) 114 | } 115 | 116 | } 117 | } 118 | 119 | func TestSegmentsForScopeId(t *testing.T) { 120 | segments := ScopeId{}.Segments() 121 | if len(segments) == 0 { 122 | t.Fatalf("ScopeId has no segments") 123 | } 124 | 125 | uniqueNames := make(map[string]struct{}, 0) 126 | for _, segment := range segments { 127 | uniqueNames[segment.Name] = struct{}{} 128 | } 129 | if len(uniqueNames) != len(segments) { 130 | t.Fatalf("Expected the Segments to be unique but got %q unique segments and %d total segments", len(uniqueNames), len(segments)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /resourcemanager/commonids/subscription.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonids 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 11 | ) 12 | 13 | var _ resourceids.ResourceId = &SubscriptionId{} 14 | 15 | // SubscriptionId is a struct representing the Resource ID for a Subscription 16 | type SubscriptionId struct { 17 | SubscriptionId string 18 | } 19 | 20 | // NewSubscriptionID returns a new SubscriptionId struct 21 | func NewSubscriptionID(subscriptionId string) SubscriptionId { 22 | return SubscriptionId{ 23 | SubscriptionId: subscriptionId, 24 | } 25 | } 26 | 27 | // ParseSubscriptionID parses 'input' into a SubscriptionId 28 | func ParseSubscriptionID(input string) (*SubscriptionId, error) { 29 | parser := resourceids.NewParserFromResourceIdType(&SubscriptionId{}) 30 | parsed, err := parser.Parse(input, false) 31 | if err != nil { 32 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 33 | } 34 | 35 | id := SubscriptionId{} 36 | if err := id.FromParseResult(*parsed); err != nil { 37 | return nil, err 38 | } 39 | 40 | return &id, nil 41 | } 42 | 43 | // ParseSubscriptionIDInsensitively parses 'input' case-insensitively into a SubscriptionId 44 | // note: this method should only be used for API response data and not user input 45 | func ParseSubscriptionIDInsensitively(input string) (*SubscriptionId, error) { 46 | parser := resourceids.NewParserFromResourceIdType(&SubscriptionId{}) 47 | parsed, err := parser.Parse(input, true) 48 | if err != nil { 49 | return nil, fmt.Errorf("parsing %q: %+v", input, err) 50 | } 51 | 52 | id := SubscriptionId{} 53 | if err = id.FromParseResult(*parsed); err != nil { 54 | return nil, err 55 | } 56 | 57 | return &id, nil 58 | } 59 | 60 | func (id *SubscriptionId) FromParseResult(input resourceids.ParseResult) error { 61 | var ok bool 62 | 63 | if id.SubscriptionId, ok = input.Parsed["subscriptionId"]; !ok { 64 | return resourceids.NewSegmentNotSpecifiedError(id, "subscriptionId", input) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // ValidateSubscriptionID checks that 'input' can be parsed as a Subscription ID 71 | func ValidateSubscriptionID(input interface{}, key string) (warnings []string, errors []error) { 72 | v, ok := input.(string) 73 | if !ok { 74 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 75 | return 76 | } 77 | 78 | if _, err := ParseSubscriptionID(v); err != nil { 79 | errors = append(errors, err) 80 | } 81 | 82 | return 83 | } 84 | 85 | // ID returns the formatted Subscription ID 86 | func (id SubscriptionId) ID() string { 87 | fmtString := "/subscriptions/%s" 88 | return fmt.Sprintf(fmtString, id.SubscriptionId) 89 | } 90 | 91 | // Segments returns a slice of Resource ID Segments which comprise this Subscription ID 92 | func (id SubscriptionId) Segments() []resourceids.Segment { 93 | return []resourceids.Segment{ 94 | resourceids.StaticSegment("subscriptions", "subscriptions", "subscriptions"), 95 | resourceids.SubscriptionIdSegment("subscriptionId", "12345678-1234-9876-4563-123456789012"), 96 | } 97 | } 98 | 99 | // String returns a human-readable description of this Subscription ID 100 | func (id SubscriptionId) String() string { 101 | components := []string{ 102 | fmt.Sprintf("Subscription: %q", id.SubscriptionId), 103 | } 104 | return fmt.Sprintf("Subscription (%s)", strings.Join(components, "\n")) 105 | } 106 | -------------------------------------------------------------------------------- /resourcemanager/commonschema/edge_zone.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "github.com/hashicorp/go-azure-helpers/resourcemanager/edgezones" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 10 | ) 11 | 12 | // EdgeZoneComputed returns the schema for an Edge Zone which is Computed 13 | func EdgeZoneComputed() *schema.Schema { 14 | return &schema.Schema{ 15 | Type: schema.TypeString, 16 | Computed: true, 17 | } 18 | } 19 | 20 | // EdgeZoneOptional returns the schema for an Edge Zone which is Optional 21 | func EdgeZoneOptional() *schema.Schema { 22 | return &schema.Schema{ 23 | Type: schema.TypeString, 24 | Optional: true, 25 | ValidateFunc: validation.StringIsNotEmpty, 26 | StateFunc: edgezones.StateFunc, 27 | DiffSuppressFunc: edgezones.DiffSuppressFunc, 28 | } 29 | } 30 | 31 | // EdgeZoneOptionalForceNew returns the schema for an Edge Zone which is both Optional and ForceNew 32 | func EdgeZoneOptionalForceNew() *schema.Schema { 33 | return &schema.Schema{ 34 | Type: schema.TypeString, 35 | Optional: true, 36 | ForceNew: true, 37 | ValidateFunc: validation.StringIsNotEmpty, 38 | StateFunc: edgezones.StateFunc, 39 | DiffSuppressFunc: edgezones.DiffSuppressFunc, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /resourcemanager/commonschema/location.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "github.com/hashicorp/go-azure-helpers/resourcemanager/location" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | ) 10 | 11 | func Location() *schema.Schema { 12 | return &schema.Schema{ 13 | Type: schema.TypeString, 14 | Required: true, 15 | ForceNew: true, 16 | ValidateFunc: location.EnhancedValidate, 17 | StateFunc: location.StateFunc, 18 | DiffSuppressFunc: location.DiffSuppressFunc, 19 | } 20 | } 21 | 22 | func LocationOptional() *schema.Schema { 23 | return &schema.Schema{ 24 | Type: schema.TypeString, 25 | Optional: true, 26 | ForceNew: true, 27 | StateFunc: location.StateFunc, 28 | DiffSuppressFunc: location.DiffSuppressFunc, 29 | } 30 | } 31 | 32 | func LocationComputed() *schema.Schema { 33 | return &schema.Schema{ 34 | Type: schema.TypeString, 35 | Computed: true, 36 | } 37 | } 38 | 39 | func LocationWithoutForceNew() *schema.Schema { 40 | return &schema.Schema{ 41 | Type: schema.TypeString, 42 | Required: true, 43 | ValidateFunc: location.EnhancedValidate, 44 | StateFunc: location.StateFunc, 45 | DiffSuppressFunc: location.DiffSuppressFunc, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /resourcemanager/commonschema/resource_group_name.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourcegroups" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | ) 10 | 11 | func ResourceGroupName() *schema.Schema { 12 | return &schema.Schema{ 13 | Type: schema.TypeString, 14 | Required: true, 15 | ForceNew: true, 16 | ValidateFunc: resourcegroups.ValidateName, 17 | } 18 | } 19 | 20 | func ResourceGroupNameDeprecated() *schema.Schema { 21 | return &schema.Schema{ 22 | Type: schema.TypeString, 23 | Optional: true, 24 | ValidateFunc: resourcegroups.ValidateName, 25 | Deprecated: "This field is no longer used and will be removed in the next major version of the Azure Provider", 26 | } 27 | } 28 | 29 | func ResourceGroupNameDeprecatedComputed() *schema.Schema { 30 | return &schema.Schema{ 31 | Type: schema.TypeString, 32 | Optional: true, 33 | Computed: true, 34 | ValidateFunc: resourcegroups.ValidateName, 35 | Deprecated: "This field is no longer used and will be removed in the next major version of the Azure Provider", 36 | } 37 | } 38 | 39 | func ResourceGroupNameForDataSource() *schema.Schema { 40 | return &schema.Schema{ 41 | Type: schema.TypeString, 42 | Required: true, 43 | ValidateFunc: resourcegroups.ValidateName, 44 | } 45 | } 46 | 47 | func ResourceGroupNameOptionalComputed() *schema.Schema { 48 | return &schema.Schema{ 49 | Type: schema.TypeString, 50 | ForceNew: true, 51 | Optional: true, 52 | Computed: true, 53 | ValidateFunc: resourcegroups.ValidateName, 54 | } 55 | } 56 | 57 | func ResourceGroupNameOptional() *schema.Schema { 58 | return &schema.Schema{ 59 | Type: schema.TypeString, 60 | Optional: true, 61 | ValidateFunc: resourcegroups.ValidateName, 62 | } 63 | } 64 | 65 | func ResourceGroupNameSetOptional() *schema.Schema { 66 | return &schema.Schema{ 67 | Type: schema.TypeSet, 68 | Optional: true, 69 | Elem: &schema.Schema{ 70 | Type: schema.TypeString, 71 | ValidateFunc: resourcegroups.ValidateName, 72 | }, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /resourcemanager/commonschema/resource_id_reference.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | // ResourceIDReferenceOptional returns the schema for a Resource ID Reference which is Optional. 14 | func ResourceIDReferenceOptional(id resourceids.ResourceId) *schema.Schema { 15 | return &schema.Schema{ 16 | Type: schema.TypeString, 17 | Optional: true, 18 | ValidateFunc: validationFunctionForResourceID(id), 19 | } 20 | } 21 | 22 | // ResourceIDReferenceElem returns the schema for a Resource ID Reference which is compatible with the Elem of lists and sets. 23 | func ResourceIDReferenceElem(id resourceids.ResourceId) *schema.Schema { 24 | return &schema.Schema{ 25 | Type: schema.TypeString, 26 | ValidateFunc: validationFunctionForResourceID(id), 27 | } 28 | } 29 | 30 | // ResourceIDReferenceOptionalForceNew returns the schema for a Resource ID Reference 31 | // which is both Optional and ForceNew. 32 | func ResourceIDReferenceOptionalForceNew(id resourceids.ResourceId) *schema.Schema { 33 | return &schema.Schema{ 34 | Type: schema.TypeString, 35 | Optional: true, 36 | ForceNew: true, 37 | ValidateFunc: validationFunctionForResourceID(id), 38 | } 39 | } 40 | 41 | // ResourceIDReferenceRequired returns the schema for a Resource ID Reference which is Required. 42 | func ResourceIDReferenceRequired(id resourceids.ResourceId) *schema.Schema { 43 | return &schema.Schema{ 44 | Type: schema.TypeString, 45 | Required: true, 46 | ValidateFunc: validationFunctionForResourceID(id), 47 | } 48 | } 49 | 50 | // ResourceIDReferenceRequiredForceNew returns the schema for a Resource ID Reference 51 | // which is both Required and ForceNew. 52 | func ResourceIDReferenceRequiredForceNew(id resourceids.ResourceId) *schema.Schema { 53 | return &schema.Schema{ 54 | Type: schema.TypeString, 55 | Required: true, 56 | ForceNew: true, 57 | ValidateFunc: validationFunctionForResourceID(id), 58 | } 59 | } 60 | 61 | func validationFunctionForResourceID(id resourceids.ResourceId) schema.SchemaValidateFunc { //nolint:staticcheck 62 | return func(input interface{}, key string) (warnings []string, errors []error) { 63 | v, ok := input.(string) 64 | if !ok { 65 | errors = append(errors, fmt.Errorf("expected %q to be a string", key)) 66 | return 67 | } 68 | 69 | if err := tryParsingResourceID(v, id); err != nil { 70 | errors = append(errors, fmt.Errorf("parsing %q: %+v", v, err)) 71 | } 72 | 73 | return 74 | } 75 | } 76 | 77 | func tryParsingResourceID(value string, resourceId resourceids.ResourceId) error { 78 | parser := resourceids.NewParserFromResourceIdType(resourceId) 79 | parsed, err := parser.Parse(value, false) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | for i, segment := range resourceId.Segments() { 85 | if _, ok := parsed.Parsed[segment.Name]; !ok { 86 | return fmt.Errorf("expected the segment %d (type %q / name %q) to have a value but it didn't", i, segment.Type, segment.Name) 87 | } 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /resourcemanager/commonschema/tags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "github.com/hashicorp/go-azure-helpers/resourcemanager/tags" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | ) 10 | 11 | func TagsDataSource() *schema.Schema { 12 | return &schema.Schema{ 13 | Type: schema.TypeMap, 14 | Computed: true, 15 | Elem: &schema.Schema{ 16 | Type: schema.TypeString, 17 | }, 18 | } 19 | } 20 | 21 | func TagsForceNew() *schema.Schema { 22 | return &schema.Schema{ 23 | Type: schema.TypeMap, 24 | Optional: true, 25 | ForceNew: true, 26 | ValidateFunc: tags.Validate, 27 | Elem: &schema.Schema{ 28 | Type: schema.TypeString, 29 | }, 30 | } 31 | } 32 | 33 | func Tags() *schema.Schema { 34 | return &schema.Schema{ 35 | Type: schema.TypeMap, 36 | Optional: true, 37 | ValidateFunc: tags.Validate, 38 | Elem: &schema.Schema{ 39 | Type: schema.TypeString, 40 | }, 41 | } 42 | } 43 | 44 | func TagsWithLowerCaseKeys() *schema.Schema { 45 | return &schema.Schema{ 46 | Type: schema.TypeMap, 47 | Optional: true, 48 | ValidateFunc: tags.ValidateHasLowerCaseKeys, 49 | Elem: &schema.Schema{ 50 | Type: schema.TypeString, 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resourcemanager/commonschema/zone.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 9 | ) 10 | 11 | // ZoneSingleRequired returns the schema used when a single Zone must be specified 12 | func ZoneSingleRequired() *schema.Schema { 13 | return &schema.Schema{ 14 | Type: schema.TypeString, 15 | Required: true, 16 | ValidateFunc: validation.StringIsNotEmpty, 17 | } 18 | } 19 | 20 | // ZoneSingleRequiredForceNew returns the schema used when a single Zone must be specified but cannot be changed 21 | func ZoneSingleRequiredForceNew() *schema.Schema { 22 | return &schema.Schema{ 23 | Type: schema.TypeString, 24 | Required: true, 25 | ForceNew: true, 26 | ValidateFunc: validation.StringIsNotEmpty, 27 | } 28 | } 29 | 30 | // ZoneSingleOptional returns the schema used when a single Zone can be specified 31 | func ZoneSingleOptional() *schema.Schema { 32 | return &schema.Schema{ 33 | Type: schema.TypeString, 34 | Optional: true, 35 | ValidateFunc: validation.StringIsNotEmpty, 36 | } 37 | } 38 | 39 | // ZoneSingleOptionalForceNew returns the schema used when a single Zone can be specified but cannot be changed 40 | func ZoneSingleOptionalForceNew() *schema.Schema { 41 | return &schema.Schema{ 42 | Type: schema.TypeString, 43 | Optional: true, 44 | ForceNew: true, 45 | ValidateFunc: validation.StringIsNotEmpty, 46 | } 47 | } 48 | 49 | // ZoneSingleComputed returns the schema used when a single Zones can be returned 50 | func ZoneSingleComputed() *schema.Schema { 51 | return &schema.Schema{ 52 | Type: schema.TypeString, 53 | Computed: true, 54 | } 55 | } 56 | 57 | // ZoneSingleOptionalComputed returns the schema used when a single Zone can be specified or a single Zone is returned when omitted 58 | func ZoneSingleOptionalComputed() *schema.Schema { 59 | return &schema.Schema{ 60 | Type: schema.TypeString, 61 | Optional: true, 62 | Computed: true, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /resourcemanager/commonschema/zones.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package commonschema 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 9 | ) 10 | 11 | // NOTE: we intentionally don't have an Optional & Computed here for behavioural consistency. 12 | 13 | // ZonesMultipleRequired returns the schema used when multiple Zones must be specified 14 | func ZonesMultipleRequired() *schema.Schema { 15 | return &schema.Schema{ 16 | Type: schema.TypeSet, 17 | Required: true, 18 | Elem: &schema.Schema{ 19 | Type: schema.TypeString, 20 | ValidateFunc: validation.StringIsNotEmpty, 21 | }, 22 | } 23 | } 24 | 25 | // ZonesMultipleRequiredForceNew returns the schema used when multiple Zones must be specified but cannot be changed 26 | func ZonesMultipleRequiredForceNew() *schema.Schema { 27 | return &schema.Schema{ 28 | Type: schema.TypeSet, 29 | Required: true, 30 | ForceNew: true, 31 | Elem: &schema.Schema{ 32 | Type: schema.TypeString, 33 | ValidateFunc: validation.StringIsNotEmpty, 34 | }, 35 | } 36 | } 37 | 38 | // ZonesMultipleOptional returns the schema used when multiple Zones can be specified 39 | func ZonesMultipleOptional() *schema.Schema { 40 | return &schema.Schema{ 41 | Type: schema.TypeSet, 42 | Optional: true, 43 | Elem: &schema.Schema{ 44 | Type: schema.TypeString, 45 | ValidateFunc: validation.StringIsNotEmpty, 46 | }, 47 | } 48 | } 49 | 50 | // ZonesMultipleOptionalForceNew returns the schema used when multiple Zones can be specified but cannot be changed 51 | func ZonesMultipleOptionalForceNew() *schema.Schema { 52 | return &schema.Schema{ 53 | Type: schema.TypeSet, 54 | Optional: true, 55 | ForceNew: true, 56 | Elem: &schema.Schema{ 57 | Type: schema.TypeString, 58 | ValidateFunc: validation.StringIsNotEmpty, 59 | }, 60 | } 61 | } 62 | 63 | // ZonesMultipleComputed returns the schema used when multiple Zones can be returned 64 | func ZonesMultipleComputed() *schema.Schema { 65 | return &schema.Schema{ 66 | Type: schema.TypeList, 67 | Computed: true, 68 | Elem: &schema.Schema{ 69 | Type: schema.TypeString, 70 | ValidateFunc: validation.StringIsNotEmpty, 71 | }, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /resourcemanager/edgezones/model.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package edgezones 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | _ json.Marshaler = &Model{} 14 | _ json.Unmarshaler = &Model{} 15 | ) 16 | 17 | type Model struct { 18 | Name string 19 | } 20 | 21 | func (m *Model) MarshalJSON() ([]byte, error) { 22 | out := map[string]interface{}{} 23 | 24 | if m.Name != "" { 25 | out["name"] = m.Name 26 | out["type"] = "EdgeZone" 27 | } 28 | 29 | return json.Marshal(out) 30 | } 31 | 32 | func (m *Model) UnmarshalJSON(bytes []byte) error { 33 | var decoded struct { 34 | Name *string `json:"name"` 35 | Type *string `json:"type"` 36 | } 37 | if err := json.Unmarshal(bytes, &decoded); err != nil { 38 | return fmt.Errorf("decoding: %+v", err) 39 | } 40 | 41 | if decoded.Name == nil || decoded.Type == nil || !strings.EqualFold(*decoded.Type, "EdgeZone") { 42 | return nil 43 | } 44 | 45 | m.Name = *decoded.Name 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /resourcemanager/edgezones/model_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package edgezones 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | ) 10 | 11 | func TestMarshalModel(t *testing.T) { 12 | testData := []struct { 13 | Input *Model 14 | ExpectedName *string 15 | }{ 16 | { 17 | // empty name would be bad data, so this can be cleared? 18 | Input: &Model{ 19 | Name: "", 20 | }, 21 | ExpectedName: nil, 22 | }, 23 | { 24 | Input: &Model{ 25 | Name: "Locutus", 26 | }, 27 | ExpectedName: ptr("Locutus"), 28 | }, 29 | } 30 | for i, v := range testData { 31 | t.Logf("item %d", i) 32 | 33 | out, err := json.Marshal(v.Input) 34 | if err != nil { 35 | t.Fatalf("marshaling: %+v", err) 36 | } 37 | 38 | var decoded map[string]interface{} 39 | if err := json.Unmarshal(out, &decoded); err != nil { 40 | t.Fatalf("unmarshaling test output: %+v", err) 41 | } 42 | 43 | name, ok := decoded["name"].(string) 44 | if !ok { 45 | if v.ExpectedName == nil { 46 | continue 47 | } 48 | 49 | t.Fatalf("expected the encoded name to be %q but it was nil", *v.ExpectedName) 50 | } 51 | 52 | if v.ExpectedName == nil { 53 | t.Fatalf("expected there to be no encoded name but got %q", name) 54 | } 55 | 56 | if *v.ExpectedName != name { 57 | t.Fatalf("expected the encoded name to be %q but got %q", *v.ExpectedName, name) 58 | } 59 | } 60 | } 61 | 62 | func TestUnmarshalModel(t *testing.T) { 63 | testData := []struct { 64 | ExpectedName *string 65 | Payload string 66 | }{ 67 | { 68 | // Invalid 69 | ExpectedName: nil, 70 | Payload: `{}`, 71 | }, 72 | { 73 | // Invalid 74 | ExpectedName: nil, 75 | Payload: `{"name": "Locutus"}`, 76 | }, 77 | { 78 | // Invalid 79 | ExpectedName: nil, 80 | Payload: `{"name": "Locutus", "type": "Borg"}`, 81 | }, 82 | { 83 | // Valid 84 | ExpectedName: ptr("Bob"), 85 | Payload: `{"name": "Bob", "type": "EdgeZone"}`, 86 | }, 87 | } 88 | for i, v := range testData { 89 | t.Logf("item %d", i) 90 | 91 | var model Model 92 | if err := json.Unmarshal([]byte(v.Payload), &model); err != nil { 93 | t.Fatalf("unmarshaling: %+v", err) 94 | } 95 | 96 | if model.Name == "" && v.ExpectedName == nil { 97 | // expected failure 98 | continue 99 | } 100 | 101 | if model.Name != "" && v.ExpectedName == nil { 102 | t.Fatalf("expected model to be nil but got %q", model.Name) 103 | } 104 | if model.Name == "" && v.ExpectedName != nil { 105 | t.Fatalf("expected to get a model named %q but didn't", *v.ExpectedName) 106 | } 107 | 108 | if model.Name != *v.ExpectedName { 109 | t.Fatalf("expected the deserialized name to be %q but got %q", *v.ExpectedName, model.Name) 110 | } 111 | } 112 | } 113 | 114 | func ptr(in string) *string { 115 | return &in 116 | } 117 | -------------------------------------------------------------------------------- /resourcemanager/edgezones/normalize.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package edgezones 5 | 6 | import ( 7 | "github.com/hashicorp/go-azure-helpers/resourcemanager/location" 8 | ) 9 | 10 | // Normalize transforms the specified user input into a canonical form 11 | func Normalize(input string) string { 12 | // we're intentionally passing through to Locations today since this is sufficient 13 | // but it's helpful to have a specific endpoint for this should this need to change 14 | // in the future 15 | return location.Normalize(input) 16 | } 17 | 18 | // NormalizeNilable normalizes the specified user input into a canonical form 19 | func NormalizeNilable(input *string) string { 20 | // we're intentionally passing through to Locations today since this is sufficient 21 | // but it's helpful to have a specific endpoint for this should this need to change 22 | // in the future 23 | return location.NormalizeNilable(input) 24 | } 25 | -------------------------------------------------------------------------------- /resourcemanager/edgezones/normalize_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package edgezones 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/go-azure-helpers/lang/pointer" 10 | ) 11 | 12 | func TestNormalizeLocation(t *testing.T) { 13 | cases := []struct { 14 | input string 15 | expected string 16 | }{ 17 | { 18 | input: "MicrosoftLosAngeles1", 19 | expected: "microsoftlosangeles1", 20 | }, 21 | { 22 | input: "Microsoft Los Angeles 1", 23 | expected: "microsoftlosangeles1", 24 | }, 25 | } 26 | 27 | for _, v := range cases { 28 | actual := Normalize(v.input) 29 | if v.expected != actual { 30 | t.Fatalf("Expected %q but got %q", v.expected, actual) 31 | } 32 | } 33 | } 34 | 35 | func TestNormalizeNilableLocation(t *testing.T) { 36 | cases := []struct { 37 | input *string 38 | expected string 39 | }{ 40 | { 41 | input: pointer.FromString("MicrosoftLosAngeles1"), 42 | expected: "microsoftlosangeles1", 43 | }, 44 | { 45 | input: pointer.FromString("Microsoft Los Angeles 1"), 46 | expected: "microsoftlosangeles1", 47 | }, 48 | { 49 | input: nil, 50 | expected: "", 51 | }, 52 | } 53 | 54 | for _, v := range cases { 55 | actual := NormalizeNilable(v.input) 56 | if v.expected != actual { 57 | t.Fatalf("Expected %q but got %q", v.expected, actual) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /resourcemanager/edgezones/schema.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package edgezones 5 | 6 | import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | 8 | func DiffSuppressFunc(_, old, new string, _ *schema.ResourceData) bool { 9 | return Normalize(old) == Normalize(new) 10 | } 11 | 12 | func StateFunc(location interface{}) string { 13 | input := location.(string) 14 | return Normalize(input) 15 | } 16 | -------------------------------------------------------------------------------- /resourcemanager/features/user_specified_segments.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package features 5 | 6 | // TreatUserSpecifiedSegmentsAsCaseInsensitive is a feature-toggle which specifies whether User Specified 7 | // Resource ID Segments should be compared case-insensitively as required. 8 | // 9 | // @tombuildsstuff: whilst this IS EXPOSED in the public interface - this is NOT READY FOR USE and should 10 | // not be exposed to users (i.e. as a feature-toggle) until this work is completed - as this'll become a source of knock-on problems 11 | // rather than being useful. 12 | // 13 | // There are a number of dependencies to enabling this, including completing the standardiation on the 14 | // `ResourceId` interface and the `ResourceIDReference` schema types - and surrounding updates. 15 | var TreatUserSpecifiedSegmentsAsCaseInsensitive = false 16 | -------------------------------------------------------------------------------- /resourcemanager/identity/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import "strings" 7 | 8 | type Type string 9 | 10 | const ( 11 | TypeNone Type = "None" 12 | TypeSystemAssigned Type = "SystemAssigned" 13 | TypeUserAssigned Type = "UserAssigned" 14 | TypeSystemAssignedUserAssigned Type = "SystemAssigned, UserAssigned" 15 | 16 | // this is an internal-only type to transform the legacy API value to the type we want to expose 17 | typeLegacySystemAssignedUserAssigned Type = "SystemAssigned,UserAssigned" 18 | ) 19 | 20 | func normalizeType(input Type) Type { 21 | // switch out the legacy API value (no space) for the value used in the Schema (w/space for consistency) 22 | if strings.EqualFold(string(input), string(typeLegacySystemAssignedUserAssigned)) { 23 | return TypeSystemAssignedUserAssigned 24 | } 25 | 26 | vals := []Type{ 27 | TypeNone, 28 | TypeSystemAssigned, 29 | TypeUserAssigned, 30 | TypeSystemAssignedUserAssigned, 31 | } 32 | for _, v := range vals { 33 | if strings.EqualFold(string(input), string(v)) { 34 | return v 35 | } 36 | } 37 | return input 38 | } 39 | -------------------------------------------------------------------------------- /resourcemanager/identity/model.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import "encoding/json" 7 | 8 | var _ json.Marshaler = UserAssignedIdentityDetails{} 9 | 10 | type UserAssignedIdentityDetails struct { 11 | ClientId *string `json:"clientId,omitempty"` 12 | PrincipalId *string `json:"principalId,omitempty"` 13 | } 14 | 15 | func (u UserAssignedIdentityDetails) MarshalJSON() ([]byte, error) { 16 | // none of these properties can be set, so we'll just flatten an empty struct 17 | return json.Marshal(map[string]interface{}{}) 18 | } 19 | -------------------------------------------------------------------------------- /resourcemanager/identity/system_and_user_assigned_list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "encoding/json" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestSystemUserAssignedListMarshal(t *testing.T) { 14 | testData := []struct { 15 | input *SystemAndUserAssignedList 16 | expectedIdentityType string 17 | expectedUserAssignedIdentityIds []string 18 | }{ 19 | { 20 | input: nil, 21 | expectedIdentityType: "None", 22 | expectedUserAssignedIdentityIds: []string{}, 23 | }, 24 | { 25 | input: &SystemAndUserAssignedList{}, 26 | expectedIdentityType: "None", 27 | expectedUserAssignedIdentityIds: []string{}, 28 | }, 29 | { 30 | input: &SystemAndUserAssignedList{ 31 | Type: TypeNone, 32 | }, 33 | expectedIdentityType: "None", 34 | expectedUserAssignedIdentityIds: []string{}, 35 | }, 36 | { 37 | input: &SystemAndUserAssignedList{ 38 | Type: TypeNone, 39 | IdentityIds: []string{ 40 | "first", 41 | }, 42 | }, 43 | expectedIdentityType: "None", 44 | expectedUserAssignedIdentityIds: []string{ 45 | // intentionally empty since this is bad data 46 | }, 47 | }, 48 | { 49 | input: &SystemAndUserAssignedList{ 50 | Type: TypeSystemAssigned, 51 | IdentityIds: []string{}, 52 | }, 53 | expectedIdentityType: "SystemAssigned", 54 | expectedUserAssignedIdentityIds: []string{}, 55 | }, 56 | { 57 | input: &SystemAndUserAssignedList{ 58 | Type: TypeSystemAssignedUserAssigned, 59 | IdentityIds: []string{}, 60 | }, 61 | expectedIdentityType: "SystemAssigned, UserAssigned", 62 | expectedUserAssignedIdentityIds: []string{}, 63 | }, 64 | { 65 | input: &SystemAndUserAssignedList{ 66 | Type: TypeUserAssigned, 67 | IdentityIds: []string{}, 68 | }, 69 | expectedIdentityType: "UserAssigned", 70 | expectedUserAssignedIdentityIds: []string{}, 71 | }, 72 | 73 | { 74 | input: &SystemAndUserAssignedList{ 75 | Type: TypeSystemAssignedUserAssigned, 76 | IdentityIds: []string{ 77 | "first", 78 | "second", 79 | }, 80 | }, 81 | expectedIdentityType: "SystemAssigned, UserAssigned", 82 | expectedUserAssignedIdentityIds: []string{ 83 | "first", 84 | "second", 85 | }, 86 | }, 87 | { 88 | input: &SystemAndUserAssignedList{ 89 | Type: TypeUserAssigned, 90 | IdentityIds: []string{ 91 | "first", 92 | "second", 93 | }, 94 | }, 95 | expectedIdentityType: "UserAssigned", 96 | expectedUserAssignedIdentityIds: []string{ 97 | "first", 98 | "second", 99 | }, 100 | }, 101 | } 102 | for i, v := range testData { 103 | t.Logf("step %d..", i) 104 | 105 | encoded, err := v.input.MarshalJSON() 106 | if err != nil { 107 | t.Fatalf("marshaling: %+v", err) 108 | } 109 | 110 | var out map[string]interface{} 111 | if err := json.Unmarshal(encoded, &out); err != nil { 112 | t.Fatalf("decoding: %+v", err) 113 | } 114 | 115 | actualIdentityValue := out["type"].(string) 116 | if v.expectedIdentityType != actualIdentityValue { 117 | t.Fatalf("expected %q but got %q", v.expectedIdentityType, actualIdentityValue) 118 | } 119 | 120 | actualUserAssignedIdentityIdsRaw, ok := out["userAssignedIdentities"].([]interface{}) 121 | if !ok { 122 | if len(v.expectedUserAssignedIdentityIds) == 0 { 123 | continue 124 | } 125 | 126 | t.Fatalf("`userAssignedIdentities` was nil") 127 | } 128 | actualUserAssignedIdentityIds := make([]string, 0) 129 | for _, v := range actualUserAssignedIdentityIdsRaw { 130 | actualUserAssignedIdentityIds = append(actualUserAssignedIdentityIds, v.(string)) 131 | } 132 | if !reflect.DeepEqual(v.expectedUserAssignedIdentityIds, actualUserAssignedIdentityIds) { 133 | t.Fatalf("expected %q but got %q", strings.Join(v.expectedUserAssignedIdentityIds, ", "), strings.Join(actualUserAssignedIdentityIds, ", ")) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /resourcemanager/identity/system_assigned.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "encoding/json" 8 | ) 9 | 10 | var _ json.Marshaler = &SystemAssigned{} 11 | 12 | type SystemAssigned struct { 13 | Type Type `json:"type" tfschema:"type"` 14 | PrincipalId string `json:"principalId" tfschema:"principal_id"` 15 | TenantId string `json:"tenantId" tfschema:"tenant_id"` 16 | } 17 | 18 | func (s *SystemAssigned) MarshalJSON() ([]byte, error) { 19 | // we use a custom marshal function here since we can only send the Type field 20 | out := map[string]interface{}{ 21 | "type": string(TypeNone), 22 | } 23 | if s != nil && s.Type == TypeSystemAssigned { 24 | out["type"] = string(TypeSystemAssigned) 25 | } 26 | return json.Marshal(out) 27 | } 28 | 29 | func ExpandSystemAssigned(input []interface{}) (*SystemAssigned, error) { 30 | if len(input) == 0 || input[0] == nil { 31 | return &SystemAssigned{ 32 | Type: TypeNone, 33 | }, nil 34 | } 35 | 36 | return &SystemAssigned{ 37 | Type: TypeSystemAssigned, 38 | }, nil 39 | } 40 | 41 | func FlattenSystemAssigned(input *SystemAssigned) []interface{} { 42 | if input == nil { 43 | return []interface{}{} 44 | } 45 | 46 | input.Type = normalizeType(input.Type) 47 | 48 | if input.Type == TypeNone { 49 | return []interface{}{} 50 | } 51 | 52 | return []interface{}{ 53 | map[string]interface{}{ 54 | "type": input.Type, 55 | "principal_id": input.PrincipalId, 56 | "tenant_id": input.TenantId, 57 | }, 58 | } 59 | } 60 | 61 | func ExpandSystemAssignedFromModel(input []ModelSystemAssigned) (*SystemAssigned, error) { 62 | if len(input) == 0 { 63 | return &SystemAssigned{ 64 | Type: TypeNone, 65 | }, nil 66 | } 67 | 68 | return &SystemAssigned{ 69 | Type: TypeSystemAssigned, 70 | }, nil 71 | } 72 | 73 | func FlattenSystemAssignedToModel(input *SystemAssigned) []ModelSystemAssigned { 74 | if input == nil { 75 | return []ModelSystemAssigned{} 76 | } 77 | 78 | input.Type = normalizeType(input.Type) 79 | 80 | if input.Type == TypeNone { 81 | return []ModelSystemAssigned{} 82 | } 83 | 84 | return []ModelSystemAssigned{ 85 | { 86 | Type: input.Type, 87 | PrincipalId: input.PrincipalId, 88 | TenantId: input.TenantId, 89 | }, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /resourcemanager/identity/system_assigned_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | ) 10 | 11 | func TestSystemAssignedMarshal(t *testing.T) { 12 | testData := []struct { 13 | input *SystemAssigned 14 | expectedValue string 15 | }{ 16 | { 17 | input: nil, 18 | expectedValue: "None", 19 | }, 20 | { 21 | input: &SystemAssigned{}, 22 | expectedValue: "None", 23 | }, 24 | { 25 | input: &SystemAssigned{ 26 | Type: TypeNone, 27 | }, 28 | expectedValue: "None", 29 | }, 30 | { 31 | input: &SystemAssigned{ 32 | Type: TypeSystemAssignedUserAssigned, 33 | }, 34 | expectedValue: "None", 35 | }, 36 | { 37 | input: &SystemAssigned{ 38 | Type: TypeUserAssigned, 39 | }, 40 | expectedValue: "None", 41 | }, 42 | { 43 | input: &SystemAssigned{ 44 | Type: TypeSystemAssigned, 45 | }, 46 | expectedValue: "SystemAssigned", 47 | }, 48 | } 49 | for i, v := range testData { 50 | t.Logf("step %d..", i) 51 | 52 | encoded, err := v.input.MarshalJSON() 53 | if err != nil { 54 | t.Fatalf("marshaling: %+v", err) 55 | } 56 | 57 | var out map[string]interface{} 58 | if err := json.Unmarshal(encoded, &out); err != nil { 59 | t.Fatalf("decoding: %+v", err) 60 | } 61 | 62 | actualValue := out["type"].(string) 63 | if v.expectedValue != actualValue { 64 | t.Fatalf("expected %q but got %q", v.expectedValue, actualValue) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /resourcemanager/identity/system_or_user_assigned_list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "encoding/json" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestSystemOrUserAssignedListMarshal(t *testing.T) { 14 | testData := []struct { 15 | input *SystemOrUserAssignedList 16 | expectedIdentityType string 17 | expectedUserAssignedIdentityIds []string 18 | }{ 19 | { 20 | input: nil, 21 | expectedIdentityType: "None", 22 | expectedUserAssignedIdentityIds: []string{}, 23 | }, 24 | { 25 | input: &SystemOrUserAssignedList{}, 26 | expectedIdentityType: "None", 27 | expectedUserAssignedIdentityIds: []string{}, 28 | }, 29 | { 30 | input: &SystemOrUserAssignedList{ 31 | Type: TypeNone, 32 | }, 33 | expectedIdentityType: "None", 34 | expectedUserAssignedIdentityIds: []string{}, 35 | }, 36 | { 37 | input: &SystemOrUserAssignedList{ 38 | Type: TypeNone, 39 | IdentityIds: []string{ 40 | "first", 41 | }, 42 | }, 43 | expectedIdentityType: "None", 44 | expectedUserAssignedIdentityIds: []string{ 45 | // intentionally empty since this is bad data 46 | }, 47 | }, 48 | { 49 | input: &SystemOrUserAssignedList{ 50 | Type: TypeSystemAssigned, 51 | IdentityIds: []string{}, 52 | }, 53 | expectedIdentityType: "SystemAssigned", 54 | expectedUserAssignedIdentityIds: []string{}, 55 | }, 56 | { 57 | input: &SystemOrUserAssignedList{ 58 | Type: TypeSystemAssignedUserAssigned, 59 | IdentityIds: []string{}, 60 | }, 61 | expectedIdentityType: "None", 62 | expectedUserAssignedIdentityIds: []string{}, 63 | }, 64 | { 65 | input: &SystemOrUserAssignedList{ 66 | Type: TypeUserAssigned, 67 | IdentityIds: []string{}, 68 | }, 69 | expectedIdentityType: "UserAssigned", 70 | expectedUserAssignedIdentityIds: []string{}, 71 | }, 72 | 73 | { 74 | input: &SystemOrUserAssignedList{ 75 | Type: TypeSystemAssignedUserAssigned, 76 | IdentityIds: []string{ 77 | "first", 78 | "second", 79 | }, 80 | }, 81 | expectedIdentityType: "None", 82 | expectedUserAssignedIdentityIds: []string{ 83 | // bad data 84 | }, 85 | }, 86 | { 87 | input: &SystemOrUserAssignedList{ 88 | Type: TypeUserAssigned, 89 | IdentityIds: []string{ 90 | "first", 91 | "second", 92 | }, 93 | }, 94 | expectedIdentityType: "UserAssigned", 95 | expectedUserAssignedIdentityIds: []string{ 96 | "first", 97 | "second", 98 | }, 99 | }, 100 | } 101 | for i, v := range testData { 102 | t.Logf("step %d..", i) 103 | 104 | encoded, err := v.input.MarshalJSON() 105 | if err != nil { 106 | t.Fatalf("marshaling: %+v", err) 107 | } 108 | 109 | var out map[string]interface{} 110 | if err := json.Unmarshal(encoded, &out); err != nil { 111 | t.Fatalf("decoding: %+v", err) 112 | } 113 | 114 | actualIdentityValue := out["type"].(string) 115 | if v.expectedIdentityType != actualIdentityValue { 116 | t.Fatalf("expected %q but got %q", v.expectedIdentityType, actualIdentityValue) 117 | } 118 | 119 | actualUserAssignedIdentityIdsRaw, ok := out["userAssignedIdentities"].([]interface{}) 120 | if !ok { 121 | if len(v.expectedUserAssignedIdentityIds) == 0 { 122 | continue 123 | } 124 | 125 | t.Fatalf("`userAssignedIdentities` was nil") 126 | } 127 | actualUserAssignedIdentityIds := make([]string, 0) 128 | for _, v := range actualUserAssignedIdentityIdsRaw { 129 | actualUserAssignedIdentityIds = append(actualUserAssignedIdentityIds, v.(string)) 130 | } 131 | if !reflect.DeepEqual(v.expectedUserAssignedIdentityIds, actualUserAssignedIdentityIds) { 132 | t.Fatalf("expected %q but got %q", strings.Join(v.expectedUserAssignedIdentityIds, ", "), strings.Join(actualUserAssignedIdentityIds, ", ")) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /resourcemanager/identity/system_or_user_assigned_map_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "encoding/json" 8 | "reflect" 9 | "sort" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestSystemOrUserAssignedMapMarshal(t *testing.T) { 15 | testData := []struct { 16 | input *SystemOrUserAssignedMap 17 | expectedIdentityType string 18 | expectedUserAssignedIdentityIds []string 19 | }{ 20 | { 21 | input: nil, 22 | expectedIdentityType: "None", 23 | expectedUserAssignedIdentityIds: []string{}, 24 | }, 25 | { 26 | input: &SystemOrUserAssignedMap{}, 27 | expectedIdentityType: "None", 28 | expectedUserAssignedIdentityIds: []string{}, 29 | }, 30 | { 31 | input: &SystemOrUserAssignedMap{ 32 | Type: TypeNone, 33 | }, 34 | expectedIdentityType: "None", 35 | expectedUserAssignedIdentityIds: []string{}, 36 | }, 37 | { 38 | input: &SystemOrUserAssignedMap{ 39 | Type: TypeNone, 40 | IdentityIds: map[string]UserAssignedIdentityDetails{ 41 | "first": {}, 42 | }, 43 | }, 44 | expectedIdentityType: "None", 45 | expectedUserAssignedIdentityIds: []string{ 46 | // intentionally empty since this is bad data 47 | }, 48 | }, 49 | { 50 | input: &SystemOrUserAssignedMap{ 51 | Type: TypeSystemAssigned, 52 | IdentityIds: map[string]UserAssignedIdentityDetails{}, 53 | }, 54 | expectedIdentityType: "SystemAssigned", 55 | expectedUserAssignedIdentityIds: []string{}, 56 | }, 57 | { 58 | input: &SystemOrUserAssignedMap{ 59 | Type: TypeSystemAssignedUserAssigned, 60 | IdentityIds: map[string]UserAssignedIdentityDetails{}, 61 | }, 62 | expectedIdentityType: "None", 63 | expectedUserAssignedIdentityIds: []string{}, 64 | }, 65 | { 66 | input: &SystemOrUserAssignedMap{ 67 | Type: TypeUserAssigned, 68 | IdentityIds: map[string]UserAssignedIdentityDetails{}, 69 | }, 70 | expectedIdentityType: "UserAssigned", 71 | expectedUserAssignedIdentityIds: []string{}, 72 | }, 73 | 74 | { 75 | input: &SystemOrUserAssignedMap{ 76 | Type: TypeSystemAssignedUserAssigned, 77 | IdentityIds: map[string]UserAssignedIdentityDetails{ 78 | "first": {}, 79 | "second": {}, 80 | }, 81 | }, 82 | expectedIdentityType: "None", 83 | expectedUserAssignedIdentityIds: []string{ 84 | // bad data 85 | }, 86 | }, 87 | { 88 | input: &SystemOrUserAssignedMap{ 89 | Type: TypeUserAssigned, 90 | IdentityIds: map[string]UserAssignedIdentityDetails{ 91 | "first": {}, 92 | "second": {}, 93 | }, 94 | }, 95 | expectedIdentityType: "UserAssigned", 96 | expectedUserAssignedIdentityIds: []string{ 97 | "first", 98 | "second", 99 | }, 100 | }, 101 | } 102 | for i, v := range testData { 103 | t.Logf("step %d..", i) 104 | 105 | encoded, err := v.input.MarshalJSON() 106 | if err != nil { 107 | t.Fatalf("marshaling: %+v", err) 108 | } 109 | 110 | var out map[string]interface{} 111 | if err := json.Unmarshal(encoded, &out); err != nil { 112 | t.Fatalf("decoding: %+v", err) 113 | } 114 | 115 | actualIdentityValue := out["type"].(string) 116 | if v.expectedIdentityType != actualIdentityValue { 117 | t.Fatalf("expected %q but got %q", v.expectedIdentityType, actualIdentityValue) 118 | } 119 | 120 | actualUserAssignedIdentityIdsRaw, ok := out["userAssignedIdentities"].(map[string]interface{}) 121 | if !ok { 122 | if len(v.expectedUserAssignedIdentityIds) == 0 { 123 | continue 124 | } 125 | 126 | t.Fatalf("`userAssignedIdentities` was nil") 127 | } 128 | actualUserAssignedIdentityIds := make([]string, 0) 129 | for k := range actualUserAssignedIdentityIdsRaw { 130 | actualUserAssignedIdentityIds = append(actualUserAssignedIdentityIds, k) 131 | } 132 | sort.Strings(v.expectedUserAssignedIdentityIds) 133 | sort.Strings(actualUserAssignedIdentityIds) 134 | 135 | if !reflect.DeepEqual(v.expectedUserAssignedIdentityIds, actualUserAssignedIdentityIds) { 136 | t.Fatalf("expected %q but got %q", strings.Join(v.expectedUserAssignedIdentityIds, ", "), strings.Join(actualUserAssignedIdentityIds, ", ")) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /resourcemanager/identity/tfschema_model.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | type ModelUserAssigned struct { 7 | Type Type `tfschema:"type"` 8 | IdentityIds []string `tfschema:"identity_ids"` 9 | } 10 | 11 | type ModelSystemAssigned struct { 12 | Type Type `tfschema:"type"` 13 | PrincipalId string `tfschema:"principal_id"` 14 | TenantId string `tfschema:"tenant_id"` 15 | } 16 | 17 | type ModelSystemAssignedUserAssigned struct { 18 | Type Type `tfschema:"type"` 19 | PrincipalId string `tfschema:"principal_id"` 20 | TenantId string `tfschema:"tenant_id"` 21 | IdentityIds []string `tfschema:"identity_ids"` 22 | } 23 | -------------------------------------------------------------------------------- /resourcemanager/identity/user_assigned_list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "encoding/json" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestUserAssignedListMarshal(t *testing.T) { 14 | testData := []struct { 15 | input *UserAssignedList 16 | expectedIdentityType string 17 | expectedUserAssignedIdentityIds []string 18 | }{ 19 | { 20 | input: nil, 21 | expectedIdentityType: "None", 22 | expectedUserAssignedIdentityIds: []string{}, 23 | }, 24 | { 25 | input: &UserAssignedList{}, 26 | expectedIdentityType: "None", 27 | expectedUserAssignedIdentityIds: []string{}, 28 | }, 29 | { 30 | input: &UserAssignedList{ 31 | Type: TypeNone, 32 | }, 33 | expectedIdentityType: "None", 34 | expectedUserAssignedIdentityIds: []string{}, 35 | }, 36 | { 37 | input: &UserAssignedList{ 38 | Type: TypeNone, 39 | IdentityIds: []string{ 40 | "first", 41 | }, 42 | }, 43 | expectedIdentityType: "None", 44 | expectedUserAssignedIdentityIds: []string{ 45 | // intentionally empty since this is bad data 46 | }, 47 | }, 48 | { 49 | input: &UserAssignedList{ 50 | Type: TypeSystemAssigned, 51 | IdentityIds: []string{}, 52 | }, 53 | expectedIdentityType: "None", 54 | expectedUserAssignedIdentityIds: []string{}, 55 | }, 56 | { 57 | input: &UserAssignedList{ 58 | Type: TypeSystemAssignedUserAssigned, 59 | IdentityIds: []string{}, 60 | }, 61 | expectedIdentityType: "None", 62 | expectedUserAssignedIdentityIds: []string{}, 63 | }, 64 | { 65 | input: &UserAssignedList{ 66 | Type: TypeUserAssigned, 67 | IdentityIds: []string{}, 68 | }, 69 | expectedIdentityType: "UserAssigned", 70 | expectedUserAssignedIdentityIds: []string{}, 71 | }, 72 | 73 | { 74 | input: &UserAssignedList{ 75 | Type: TypeSystemAssignedUserAssigned, 76 | IdentityIds: []string{ 77 | "first", 78 | "second", 79 | }, 80 | }, 81 | expectedIdentityType: "None", 82 | expectedUserAssignedIdentityIds: []string{ 83 | // intentionally empty as this is bad data 84 | }, 85 | }, 86 | { 87 | input: &UserAssignedList{ 88 | Type: TypeUserAssigned, 89 | IdentityIds: []string{ 90 | "first", 91 | "second", 92 | }, 93 | }, 94 | expectedIdentityType: "UserAssigned", 95 | expectedUserAssignedIdentityIds: []string{ 96 | "first", 97 | "second", 98 | }, 99 | }, 100 | } 101 | for i, v := range testData { 102 | t.Logf("step %d..", i) 103 | 104 | encoded, err := v.input.MarshalJSON() 105 | if err != nil { 106 | t.Fatalf("marshaling: %+v", err) 107 | } 108 | 109 | var out map[string]interface{} 110 | if err := json.Unmarshal(encoded, &out); err != nil { 111 | t.Fatalf("decoding: %+v", err) 112 | } 113 | 114 | actualIdentityValue := out["type"].(string) 115 | if v.expectedIdentityType != actualIdentityValue { 116 | t.Fatalf("expected %q but got %q", v.expectedIdentityType, actualIdentityValue) 117 | } 118 | 119 | actualUserAssignedIdentityIdsRaw, ok := out["userAssignedIdentities"].([]interface{}) 120 | if !ok { 121 | if len(v.expectedUserAssignedIdentityIds) == 0 { 122 | continue 123 | } 124 | 125 | t.Fatalf("`userAssignedIdentities` was nil") 126 | } 127 | actualUserAssignedIdentityIds := make([]string, 0) 128 | for _, v := range actualUserAssignedIdentityIdsRaw { 129 | actualUserAssignedIdentityIds = append(actualUserAssignedIdentityIds, v.(string)) 130 | } 131 | if !reflect.DeepEqual(v.expectedUserAssignedIdentityIds, actualUserAssignedIdentityIds) { 132 | t.Fatalf("expected %q but got %q", strings.Join(v.expectedUserAssignedIdentityIds, ", "), strings.Join(actualUserAssignedIdentityIds, ", ")) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /resourcemanager/identity/user_assigned_map_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package identity 5 | 6 | import ( 7 | "encoding/json" 8 | "reflect" 9 | "sort" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestUserAssignedMapMarshal(t *testing.T) { 15 | testData := []struct { 16 | input *UserAssignedMap 17 | expectedIdentityType string 18 | expectedUserAssignedIdentityIds []string 19 | }{ 20 | { 21 | input: nil, 22 | expectedIdentityType: "None", 23 | expectedUserAssignedIdentityIds: []string{}, 24 | }, 25 | { 26 | input: &UserAssignedMap{}, 27 | expectedIdentityType: "None", 28 | expectedUserAssignedIdentityIds: []string{}, 29 | }, 30 | { 31 | input: &UserAssignedMap{ 32 | Type: TypeNone, 33 | }, 34 | expectedIdentityType: "None", 35 | expectedUserAssignedIdentityIds: []string{}, 36 | }, 37 | { 38 | input: &UserAssignedMap{ 39 | Type: TypeNone, 40 | IdentityIds: map[string]UserAssignedIdentityDetails{ 41 | "first": {}, 42 | }, 43 | }, 44 | expectedIdentityType: "None", 45 | expectedUserAssignedIdentityIds: []string{ 46 | // intentionally empty since this is bad data 47 | }, 48 | }, 49 | { 50 | input: &UserAssignedMap{ 51 | Type: TypeSystemAssigned, 52 | IdentityIds: map[string]UserAssignedIdentityDetails{}, 53 | }, 54 | expectedIdentityType: "None", 55 | expectedUserAssignedIdentityIds: []string{}, 56 | }, 57 | { 58 | input: &UserAssignedMap{ 59 | Type: TypeSystemAssignedUserAssigned, 60 | IdentityIds: map[string]UserAssignedIdentityDetails{}, 61 | }, 62 | expectedIdentityType: "None", 63 | expectedUserAssignedIdentityIds: []string{}, 64 | }, 65 | { 66 | input: &UserAssignedMap{ 67 | Type: TypeUserAssigned, 68 | IdentityIds: map[string]UserAssignedIdentityDetails{}, 69 | }, 70 | expectedIdentityType: "UserAssigned", 71 | expectedUserAssignedIdentityIds: []string{}, 72 | }, 73 | 74 | { 75 | input: &UserAssignedMap{ 76 | Type: TypeSystemAssignedUserAssigned, 77 | IdentityIds: map[string]UserAssignedIdentityDetails{ 78 | "first": {}, 79 | "second": {}, 80 | }, 81 | }, 82 | expectedIdentityType: "None", 83 | expectedUserAssignedIdentityIds: []string{ 84 | // intentionally empty as this is bad data 85 | }, 86 | }, 87 | { 88 | input: &UserAssignedMap{ 89 | Type: TypeUserAssigned, 90 | IdentityIds: map[string]UserAssignedIdentityDetails{ 91 | "first": {}, 92 | "second": {}, 93 | }, 94 | }, 95 | expectedIdentityType: "UserAssigned", 96 | expectedUserAssignedIdentityIds: []string{ 97 | "first", 98 | "second", 99 | }, 100 | }, 101 | } 102 | for i, v := range testData { 103 | t.Logf("step %d..", i) 104 | 105 | encoded, err := v.input.MarshalJSON() 106 | if err != nil { 107 | t.Fatalf("marshaling: %+v", err) 108 | } 109 | 110 | var out map[string]interface{} 111 | if err := json.Unmarshal(encoded, &out); err != nil { 112 | t.Fatalf("decoding: %+v", err) 113 | } 114 | 115 | actualIdentityValue := out["type"].(string) 116 | if v.expectedIdentityType != actualIdentityValue { 117 | t.Fatalf("expected %q but got %q", v.expectedIdentityType, actualIdentityValue) 118 | } 119 | 120 | actualUserAssignedIdentityIdsRaw, ok := out["userAssignedIdentities"].(map[string]interface{}) 121 | if !ok { 122 | if len(v.expectedUserAssignedIdentityIds) == 0 { 123 | continue 124 | } 125 | 126 | t.Fatalf("`userAssignedIdentities` was nil") 127 | } 128 | actualUserAssignedIdentityIds := make([]string, 0) 129 | for k := range actualUserAssignedIdentityIdsRaw { 130 | actualUserAssignedIdentityIds = append(actualUserAssignedIdentityIds, k) 131 | } 132 | sort.Strings(v.expectedUserAssignedIdentityIds) 133 | sort.Strings(actualUserAssignedIdentityIds) 134 | 135 | if !reflect.DeepEqual(v.expectedUserAssignedIdentityIds, actualUserAssignedIdentityIds) { 136 | t.Fatalf("expected %q but got %q", strings.Join(v.expectedUserAssignedIdentityIds, ", "), strings.Join(actualUserAssignedIdentityIds, ", ")) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /resourcemanager/location/normalize.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import "strings" 7 | 8 | // Normalize transforms the human readable Azure Region/Location names (e.g. `West US`) 9 | // into the canonical value to allow comparisons between user-code and API Responses 10 | func Normalize(input string) string { 11 | return strings.ReplaceAll(strings.ToLower(input), " ", "") 12 | } 13 | 14 | // NormalizeNilable normalizes the Location field even if it's nil to ensure this field 15 | // can always have a value 16 | func NormalizeNilable(input *string) string { 17 | if input == nil { 18 | return "" 19 | } 20 | 21 | return Normalize(*input) 22 | } 23 | -------------------------------------------------------------------------------- /resourcemanager/location/normalize_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/go-azure-helpers/lang/pointer" 10 | ) 11 | 12 | func TestNormalizeLocation(t *testing.T) { 13 | cases := []struct { 14 | input string 15 | expected string 16 | }{ 17 | { 18 | input: "West US", 19 | expected: "westus", 20 | }, 21 | { 22 | input: "South East Asia", 23 | expected: "southeastasia", 24 | }, 25 | { 26 | input: "southeastasia", 27 | expected: "southeastasia", 28 | }, 29 | } 30 | 31 | for _, v := range cases { 32 | actual := Normalize(v.input) 33 | if v.expected != actual { 34 | t.Fatalf("Expected %q but got %q", v.expected, actual) 35 | } 36 | } 37 | } 38 | 39 | func TestNormalizeNilableLocation(t *testing.T) { 40 | cases := []struct { 41 | input *string 42 | expected string 43 | }{ 44 | { 45 | input: pointer.FromString("West US"), 46 | expected: "westus", 47 | }, 48 | { 49 | input: pointer.FromString("South East Asia"), 50 | expected: "southeastasia", 51 | }, 52 | { 53 | input: pointer.FromString("southeastasia"), 54 | expected: "southeastasia", 55 | }, 56 | { 57 | input: nil, 58 | expected: "", 59 | }, 60 | } 61 | 62 | for _, v := range cases { 63 | actual := NormalizeNilable(v.input) 64 | if v.expected != actual { 65 | t.Fatalf("Expected %q but got %q", v.expected, actual) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /resourcemanager/location/schema.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | 8 | func DiffSuppressFunc(_, old, new string, _ *schema.ResourceData) bool { 9 | return Normalize(old) == Normalize(new) 10 | } 11 | 12 | func StateFunc(location interface{}) string { 13 | input := location.(string) 14 | return Normalize(input) 15 | } 16 | -------------------------------------------------------------------------------- /resourcemanager/location/supported.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import ( 7 | "context" 8 | "log" 9 | ) 10 | 11 | // supportedLocations can be (validly) nil - as such this shouldn't be relied on 12 | var supportedLocations *[]string 13 | 14 | // CacheSupportedLocations attempts to retrieve the supported locations from the Azure MetaData Service 15 | // and caches them, for used in enhanced validation 16 | func CacheSupportedLocations(ctx context.Context, resourceManagerEndpoint string) { 17 | locs, err := availableAzureLocations(ctx, resourceManagerEndpoint) 18 | if err != nil { 19 | log.Printf("[DEBUG] error retrieving locations: %s. Enhanced validation will be unavailable", err) 20 | return 21 | } 22 | 23 | supportedLocations = locs.Locations 24 | } 25 | -------------------------------------------------------------------------------- /resourcemanager/location/supported_azure.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | type SupportedLocations struct { 15 | // Locations is a list of Locations which are supported on this Azure Endpoint. 16 | // This could be nil when the user is offline, or the Azure MetaData Service does not have this 17 | // information and as such this should be used as best-effort, rather than guaranteed 18 | Locations *[]string 19 | } 20 | 21 | type cloudEndpoint struct { 22 | Endpoint string `json:"endpoint"` 23 | Locations *[]string `json:"locations"` 24 | } 25 | 26 | type metaDataResponse struct { 27 | CloudEndpoint map[string]cloudEndpoint `json:"cloudEndpoint"` 28 | } 29 | 30 | // availableAzureLocations returns a list of the Azure Locations which are available on the specified endpoint 31 | func availableAzureLocations(ctx context.Context, resourceManagerEndpoint string) (*SupportedLocations, error) { 32 | // e.g. https://management.azure.com/ but we need management.azure.com 33 | endpoint := strings.TrimPrefix(resourceManagerEndpoint, "https://") 34 | endpoint = strings.TrimSuffix(endpoint, "/") 35 | 36 | uri := fmt.Sprintf("https://%s//metadata/endpoints?api-version=2018-01-01", endpoint) 37 | client := http.Client{ 38 | Transport: &http.Transport{ 39 | Proxy: http.ProxyFromEnvironment, 40 | }, 41 | } 42 | req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) 43 | if err != nil { 44 | return nil, err 45 | } 46 | resp, err := client.Do(req) 47 | if err != nil { 48 | return nil, fmt.Errorf("retrieving supported locations from Azure MetaData service: %+v", err) 49 | } 50 | var out metaDataResponse 51 | if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { 52 | return nil, fmt.Errorf("deserializing JSON from Azure MetaData service: %+v", err) 53 | } 54 | 55 | var locations *[]string 56 | for _, v := range out.CloudEndpoint { 57 | // one of the endpoints on this endpoint should reference itself 58 | // however this is best-effort, so if it doesn't, it's not the end of the world 59 | if strings.EqualFold(v.Endpoint, endpoint) { 60 | locations = v.Locations 61 | } 62 | } 63 | 64 | // TODO: remove this once Microsoft fixes the API 65 | // the Azure API returns the india locations the wrong way around 66 | // e.g. 'southindia' is returned as 'indiasouth' 67 | // so we need to conditionally switch these out until Microsoft fixes the API 68 | // $ az account list-locations -o table | grep india 69 | // Central India centralindia (Asia Pacific) Central India 70 | // South India southindia (Asia Pacific) South India 71 | // West India westindia (Asia Pacific) West India 72 | if locations != nil { 73 | out := *locations 74 | out = switchLocationIfExists("indiacentral", "centralindia", out) 75 | out = switchLocationIfExists("indiasouth", "southindia", out) 76 | out = switchLocationIfExists("indiawest", "westindia", out) 77 | locations = &out 78 | } 79 | 80 | return &SupportedLocations{ 81 | Locations: locations, 82 | }, nil 83 | } 84 | 85 | func switchLocationIfExists(find, replace string, locations []string) []string { 86 | out := locations 87 | 88 | for i, v := range out { 89 | if v == find { 90 | out[i] = replace 91 | } 92 | } 93 | 94 | return locations 95 | } 96 | -------------------------------------------------------------------------------- /resourcemanager/location/validation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 11 | ) 12 | 13 | // EnhancedValidate returns a validation function which attempts to validate the location 14 | // against the list of Locations supported by this Azure Location. 15 | // 16 | // NOTE: this is best-effort - if the users offline, or the API doesn't return it we'll 17 | // fall back to the original approach 18 | func EnhancedValidate(i interface{}, k string) ([]string, []error) { 19 | if supportedLocations == nil { 20 | return validation.StringIsNotEmpty(i, k) 21 | } 22 | 23 | return enhancedValidation(i, k) 24 | } 25 | 26 | func enhancedValidation(i interface{}, k string) ([]string, []error) { 27 | v, ok := i.(string) 28 | if !ok { 29 | return nil, []error{fmt.Errorf("expected type of %q to be string", k)} 30 | } 31 | 32 | normalizedUserInput := Normalize(v) 33 | if normalizedUserInput == "" { 34 | return nil, []error{fmt.Errorf("%q must not be empty", k)} 35 | } 36 | 37 | // supportedLocations can be nil if the users offline 38 | if supportedLocations != nil { 39 | found := false 40 | for _, loc := range *supportedLocations { 41 | if normalizedUserInput == Normalize(loc) { 42 | found = true 43 | break 44 | } 45 | } 46 | 47 | if !found { 48 | // Some resources use a location named "global". 49 | if normalizedUserInput == "global" { 50 | return nil, nil 51 | } 52 | 53 | locations := strings.Join(*supportedLocations, ",") 54 | return nil, []error{ 55 | fmt.Errorf("%q was not found in the list of supported Azure Locations: %q", normalizedUserInput, locations), 56 | } 57 | } 58 | } 59 | 60 | return nil, nil 61 | } 62 | -------------------------------------------------------------------------------- /resourcemanager/location/validation_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package location 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestEnhancedValidationDisabled(t *testing.T) { 11 | testCases := []struct { 12 | input string 13 | valid bool 14 | }{ 15 | { 16 | input: "", 17 | valid: false, 18 | }, 19 | { 20 | input: "chinanorth", 21 | valid: true, 22 | }, 23 | { 24 | input: "China North", 25 | valid: true, 26 | }, 27 | { 28 | input: "westeurope", 29 | valid: true, 30 | }, 31 | { 32 | input: "West Europe", 33 | valid: true, 34 | }, 35 | { 36 | input: "global", 37 | valid: true, 38 | }, 39 | } 40 | 41 | for _, testCase := range testCases { 42 | t.Logf("Testing %q..", testCase.input) 43 | 44 | supportedLocations = nil 45 | warnings, errors := EnhancedValidate(testCase.input, "location") 46 | valid := len(warnings) == 0 && len(errors) == 0 47 | if testCase.valid != valid { 48 | t.Errorf("Expected %t but got %t", testCase.valid, valid) 49 | } 50 | } 51 | } 52 | 53 | func TestEnhancedValidationEnabled(t *testing.T) { 54 | testCases := []struct { 55 | availableLocations []string 56 | input string 57 | valid bool 58 | }{ 59 | { 60 | availableLocations: publicLocations, 61 | input: "", 62 | valid: false, 63 | }, 64 | { 65 | availableLocations: publicLocations, 66 | input: "chinanorth", 67 | valid: false, 68 | }, 69 | { 70 | availableLocations: publicLocations, 71 | input: "China North", 72 | valid: false, 73 | }, 74 | { 75 | availableLocations: publicLocations, 76 | input: "westeurope", 77 | valid: true, 78 | }, 79 | { 80 | availableLocations: publicLocations, 81 | input: "West Europe", 82 | valid: true, 83 | }, 84 | { 85 | availableLocations: chinaLocations, 86 | input: "chinanorth", 87 | valid: true, 88 | }, 89 | { 90 | availableLocations: chinaLocations, 91 | input: "China North", 92 | valid: true, 93 | }, 94 | { 95 | availableLocations: chinaLocations, 96 | input: "westeurope", 97 | valid: false, 98 | }, 99 | { 100 | availableLocations: chinaLocations, 101 | input: "West Europe", 102 | valid: false, 103 | }, 104 | { 105 | availableLocations: publicLocations, 106 | input: "global", 107 | valid: true, 108 | }, 109 | } 110 | defer func() { 111 | supportedLocations = nil 112 | }() 113 | 114 | for _, testCase := range testCases { 115 | t.Logf("Testing %q..", testCase.input) 116 | supportedLocations = &testCase.availableLocations 117 | 118 | warnings, errors := EnhancedValidate(testCase.input, "location") 119 | valid := len(warnings) == 0 && len(errors) == 0 120 | if testCase.valid != valid { 121 | t.Logf("Expected %t but got %t", testCase.valid, valid) 122 | t.Fail() 123 | } 124 | } 125 | } 126 | 127 | var ( 128 | chinaLocations = []string{"chinaeast", "chinanorth", "chinanorth2", "chinaeast2"} 129 | publicLocations = []string{ 130 | "westus", 131 | "westus2", 132 | "eastus", 133 | "centralus", 134 | "southcentralus", 135 | "northcentralus", 136 | "westcentralus", 137 | "eastus2", 138 | "brazilsouth", 139 | "brazilus", 140 | "northeurope", 141 | "westeurope", 142 | "eastasia", 143 | "southeastasia", 144 | "japanwest", 145 | "japaneast", 146 | "koreacentral", 147 | "koreasouth", 148 | "indiasouth", 149 | "indiawest", 150 | "indiacentral", 151 | "australiaeast", 152 | "australiasoutheast", 153 | "canadacentral", 154 | "canadaeast", 155 | "uknorth", 156 | "uksouth2", 157 | "uksouth", 158 | "ukwest", 159 | "francecentral", 160 | "francesouth", 161 | "australiacentral", 162 | "australiacentral2", 163 | "uaecentral", 164 | "uaenorth", 165 | "southafricanorth", 166 | "southafricawest", 167 | "switzerlandnorth", 168 | "switzerlandwest", 169 | "germanynorth", 170 | "germanywestcentral", 171 | "norwayeast", 172 | "norwaywest", 173 | } 174 | ) 175 | -------------------------------------------------------------------------------- /resourcemanager/recaser/registration.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package recaser 5 | 6 | import ( 7 | "strings" 8 | "sync" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" 11 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 12 | ) 13 | 14 | var knownResourceIds = make(map[string]resourceids.ResourceId) 15 | 16 | // KnownResourceIds returns the map of resource IDs that have been registered by each API imported via the 17 | // RegisterResourceId function. This is the case for all APIs generated via the Pandora project via init(). 18 | // The keys for the map are the lower-cased ID strings with the user-specified segments 19 | // stripped out, leaving the path intact. Example: 20 | // "/subscriptions//resourceGroups//providers/Microsoft.BotService/botServices/" 21 | func KnownResourceIds() map[string]resourceids.ResourceId { 22 | return knownResourceIds 23 | } 24 | 25 | var resourceIdsWriteLock = &sync.Mutex{} 26 | 27 | func init() { 28 | // register common ids 29 | for _, id := range commonids.CommonIds() { 30 | RegisterResourceId(id) 31 | } 32 | } 33 | 34 | // RegisterResourceId adds ResourceIds to a list of known ids 35 | func RegisterResourceId(id resourceids.ResourceId) { 36 | key := strings.ToLower(id.ID()) 37 | 38 | resourceIdsWriteLock.Lock() 39 | if _, ok := knownResourceIds[key]; !ok { 40 | knownResourceIds[key] = id 41 | } 42 | resourceIdsWriteLock.Unlock() 43 | } 44 | -------------------------------------------------------------------------------- /resourcemanager/resourcegroups/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resourcegroups 5 | 6 | import ( 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | func ValidateName(v interface{}, k string) (warnings []string, errors []error) { 13 | value := v.(string) 14 | 15 | if len(value) > 90 { 16 | errors = append(errors, fmt.Errorf("%q may not exceed 90 characters in length", k)) 17 | } 18 | 19 | if strings.HasSuffix(value, ".") { 20 | errors = append(errors, fmt.Errorf("%q may not end with a period", k)) 21 | } 22 | 23 | if len(value) == 0 { 24 | errors = append(errors, fmt.Errorf("%q cannot be blank", k)) 25 | } else if matched := regexp.MustCompile(`^[-\w._()]+$`).Match([]byte(value)); !matched { 26 | // regex pulled from https://docs.microsoft.com/en-us/rest/api/resources/resourcegroups/createorupdate 27 | errors = append(errors, fmt.Errorf("%q may only contain alphanumeric characters, dash, underscores, parentheses and periods", k)) 28 | } 29 | 30 | return warnings, errors 31 | } 32 | -------------------------------------------------------------------------------- /resourcemanager/resourcegroups/validate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resourcegroups 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" 11 | ) 12 | 13 | func TestValidateResourceGroupName(t *testing.T) { 14 | cases := []struct { 15 | Value string 16 | ErrCount int 17 | Message string 18 | }{ 19 | { 20 | Value: "", 21 | ErrCount: 1, 22 | Message: "cannot be blank", 23 | }, 24 | { 25 | Value: "hello", 26 | ErrCount: 0, 27 | }, 28 | { 29 | Value: "Hello", 30 | ErrCount: 0, 31 | }, 32 | { 33 | Value: "hello-world", 34 | ErrCount: 0, 35 | }, 36 | { 37 | Value: "Hello_World", 38 | ErrCount: 0, 39 | }, 40 | { 41 | Value: "HelloWithNumbers12345", 42 | ErrCount: 0, 43 | }, 44 | { 45 | Value: "(Did)You(Know)That(Brackets)Are(Allowed)In(Resource)Group(Names)", 46 | ErrCount: 0, 47 | }, 48 | { 49 | Value: "EndingWithAPeriod.", 50 | ErrCount: 1, 51 | }, 52 | { 53 | Value: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo", 54 | ErrCount: 1, 55 | }, 56 | { 57 | Value: acctest.RandString(90), 58 | ErrCount: 0, 59 | }, 60 | { 61 | Value: acctest.RandString(91), 62 | ErrCount: 1, 63 | }, 64 | } 65 | 66 | for _, tc := range cases { 67 | _, errors := ValidateName(tc.Value, "azurerm_resource_group") 68 | 69 | if len(errors) != tc.ErrCount { 70 | t.Fatalf("Expected ValidateName to trigger '%d' errors for '%s' - got '%d'", tc.ErrCount, tc.Value, len(errors)) 71 | } else if len(errors) == 1 && tc.Message != "" { 72 | errorMessage := errors[0].Error() 73 | 74 | if !strings.Contains(errorMessage, tc.Message) { 75 | t.Fatalf("Expected ValidateName to report an error including '%s' for '%s' - got '%s'", tc.Message, tc.Value, errorMessage) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /resourcemanager/resourceids/error_number_of_segments_didnt_match.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resourceids 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | var _ error = NumberOfSegmentsDidntMatchError{} 13 | 14 | type NumberOfSegmentsDidntMatchError struct { 15 | parseResult ParseResult 16 | resourceId ResourceId 17 | resourceIdName string 18 | } 19 | 20 | func NewNumberOfSegmentsDidntMatchError(id ResourceId, parseResult ParseResult) NumberOfSegmentsDidntMatchError { 21 | // Resource ID types must be in the format {Name}Id 22 | resourceIdTypeName := reflect.ValueOf(id).Type().Name() 23 | if resourceIdTypeName == "" { 24 | resourceIdTypeName = reflect.ValueOf(id).Elem().Type().Name() 25 | } 26 | resourceIdName := strings.TrimSuffix(resourceIdTypeName, "Id") 27 | return NumberOfSegmentsDidntMatchError{ 28 | parseResult: parseResult, 29 | resourceId: id, 30 | resourceIdName: resourceIdName, 31 | } 32 | } 33 | 34 | // Error returns a detailed error message highlighting the issues found when parsing this Resource ID Segment. 35 | func (e NumberOfSegmentsDidntMatchError) Error() string { 36 | expectedId := buildExpectedResourceId(e.resourceId.Segments()) 37 | 38 | description, err := descriptionForSegments(e.resourceId.Segments()) 39 | if err != nil { 40 | return fmt.Sprintf("internal-error: building description for segments: %+v", err) 41 | } 42 | 43 | parsedSegments := summaryOfParsedSegments(e.parseResult, e.resourceId.Segments()) 44 | 45 | return fmt.Sprintf(`parsing the %[1]s ID: the number of segments didn't match 46 | 47 | Expected a %[1]s ID that matched (containing %[2]d segments): 48 | 49 | > %[3]s 50 | 51 | However this value was provided (which was parsed into %[4]d segments): 52 | 53 | > %[5]s 54 | 55 | The following Segments are expected: 56 | 57 | %[6]s 58 | 59 | The following Segments were parsed: 60 | 61 | %[7]s 62 | `, e.resourceIdName, len(e.resourceId.Segments()), expectedId, len(e.parseResult.Parsed), e.parseResult.RawInput, *description, parsedSegments) 63 | } 64 | -------------------------------------------------------------------------------- /resourcemanager/resourceids/error_number_of_segments_didnt_match_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resourceids_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" 11 | ) 12 | 13 | func TestNumberOfSegmentsDidntMatchError_CommonIdAllMissing(t *testing.T) { 14 | id := commonids.ResourceGroupId{} 15 | parseResult := resourceids.ParseResult{ 16 | Parsed: map[string]string{}, 17 | RawInput: "/some-value", 18 | } 19 | expected := ` 20 | parsing the ResourceGroup ID: the number of segments didn't match 21 | 22 | Expected a ResourceGroup ID that matched (containing 4 segments): 23 | 24 | > /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group 25 | 26 | However this value was provided (which was parsed into 0 segments): 27 | 28 | > /some-value 29 | 30 | The following Segments are expected: 31 | 32 | * Segment 0 - this should be the literal value "subscriptions" 33 | * Segment 1 - this should be the UUID of the Azure Subscription 34 | * Segment 2 - this should be the literal value "resourceGroups" 35 | * Segment 3 - this should be the name of the Resource Group 36 | 37 | The following Segments were parsed: 38 | 39 | * Segment 0 - not found 40 | * Segment 1 - not found 41 | * Segment 2 - not found 42 | * Segment 3 - not found 43 | ` 44 | actual := resourceids.NewNumberOfSegmentsDidntMatchError(&id, parseResult).Error() 45 | assertTemplatedCodeMatches(t, expected, actual) 46 | } 47 | 48 | func TestNumberOfSegmentsDidntMatchError_CommonIdMissingSegment(t *testing.T) { 49 | id := commonids.ResourceGroupId{} 50 | parseResult := resourceids.ParseResult{ 51 | Parsed: map[string]string{ 52 | "subscriptions": "subscriptions", 53 | "subscriptionId": "1234", 54 | }, 55 | RawInput: "/subscriptions/1234/resourcegroups", 56 | } 57 | expected := ` 58 | parsing the ResourceGroup ID: the number of segments didn't match 59 | 60 | Expected a ResourceGroup ID that matched (containing 4 segments): 61 | 62 | > /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group 63 | 64 | However this value was provided (which was parsed into 2 segments): 65 | 66 | > /subscriptions/1234/resourcegroups 67 | 68 | The following Segments are expected: 69 | 70 | * Segment 0 - this should be the literal value "subscriptions" 71 | * Segment 1 - this should be the UUID of the Azure Subscription 72 | * Segment 2 - this should be the literal value "resourceGroups" 73 | * Segment 3 - this should be the name of the Resource Group 74 | 75 | The following Segments were parsed: 76 | 77 | * Segment 0 - parsed as "subscriptions" 78 | * Segment 1 - parsed as "1234" 79 | * Segment 2 - not found 80 | * Segment 3 - not found 81 | ` 82 | actual := resourceids.NewNumberOfSegmentsDidntMatchError(&id, parseResult).Error() 83 | assertTemplatedCodeMatches(t, expected, actual) 84 | } 85 | -------------------------------------------------------------------------------- /resourcemanager/resourceids/error_segment_not_specified.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resourceids 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | var _ error = SegmentNotSpecifiedError{} 13 | 14 | type SegmentNotSpecifiedError struct { 15 | parseResult ParseResult 16 | resourceId ResourceId 17 | resourceIdName string 18 | segmentName string 19 | } 20 | 21 | // NewSegmentNotSpecifiedError returns a SegmentNotSpecifiedError for the provided Resource ID, segment and parseResult combination 22 | func NewSegmentNotSpecifiedError(id ResourceId, segmentName string, parseResult ParseResult) SegmentNotSpecifiedError { 23 | // Resource ID types must be in the format {Name}Id 24 | resourceIdTypeName := reflect.ValueOf(id).Type().Name() 25 | if resourceIdTypeName == "" { 26 | resourceIdTypeName = reflect.ValueOf(id).Elem().Type().Name() 27 | } 28 | resourceIdName := strings.TrimSuffix(resourceIdTypeName, "Id") 29 | return SegmentNotSpecifiedError{ 30 | resourceIdName: resourceIdName, 31 | resourceId: id, 32 | segmentName: segmentName, 33 | parseResult: parseResult, 34 | } 35 | } 36 | 37 | // Error returns a detailed error message highlighting the issues found when parsing this Resource ID Segment. 38 | func (e SegmentNotSpecifiedError) Error() string { 39 | expectedId := buildExpectedResourceId(e.resourceId.Segments()) 40 | position := findPositionOfSegment(e.segmentName, e.resourceId.Segments()) 41 | if position == nil { 42 | return fmt.Sprintf("internal-error: couldn't determine the position for segment %q", e.segmentName) 43 | } 44 | description, err := descriptionForSegment(e.segmentName, e.resourceId.Segments()) 45 | if err != nil { 46 | return fmt.Sprintf("internal-error: building description for segment: %+v", err) 47 | } 48 | 49 | return fmt.Sprintf(`parsing the %[1]s ID: the segment at position %[2]d didn't match 50 | 51 | Expected a %[1]s ID that matched: 52 | 53 | > %[3]s 54 | 55 | However this value was provided: 56 | 57 | > %[4]s 58 | 59 | The parsed Resource ID was missing a value for the segment at position %[2]d 60 | (which %[5]s). 61 | 62 | `, e.resourceIdName, *position, expectedId, e.parseResult.RawInput, *description) 63 | } 64 | -------------------------------------------------------------------------------- /resourcemanager/resourceids/helpers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resourceids_test 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func assertTemplatedCodeMatches(t *testing.T, expected string, actual string) { 12 | // when generating "for real" we run gofmt after it - whilst we 13 | // could do that here, as the test data won't contain the other files 14 | // as such comparing these line by line is fine for now 15 | actualLines := splitLines(actual) 16 | expectedLines := splitLines(expected) 17 | 18 | normalizedActualValue := strings.Join(actualLines, "\n") 19 | normalizedExpectedValue := strings.Join(expectedLines, "\n") 20 | 21 | if len(actualLines) != len(expectedLines) { 22 | t.Fatalf(`Expected %d lines but got %d lines. 23 | 24 | Expected Value: 25 | --- 26 | %s 27 | --- 28 | 29 | Actual Value: 30 | --- 31 | %s 32 | --- 33 | `, len(expectedLines), len(actualLines), normalizedExpectedValue, normalizedActualValue) 34 | } 35 | 36 | for i := 0; i < len(actualLines); i++ { 37 | actualLine := actualLines[i] 38 | expectedLine := expectedLines[i] 39 | if !strings.EqualFold(strings.TrimSpace(actualLine), strings.TrimSpace(expectedLine)) { 40 | t.Fatalf(`Expected and Actual differ on line %d 41 | 42 | Expected %q but got %q 43 | 44 | Expected Value: 45 | --- 46 | %s 47 | --- 48 | 49 | Actual Value: 50 | --- 51 | %s 52 | --- 53 | `, i, expectedLine, actualLine, normalizedExpectedValue, normalizedActualValue) 54 | } 55 | } 56 | } 57 | 58 | func splitLines(input string) []string { 59 | // normalize the spacing and remove any empty lines, since they don't matter for testing 60 | lines := strings.Split(strings.TrimSpace(input), "\n") 61 | out := make([]string, 0) 62 | for _, line := range lines { 63 | line = strings.TrimSpace(line) 64 | line = strings.ReplaceAll(line, "\t", " ") 65 | if line == "" || line == "\n" { 66 | continue 67 | } 68 | 69 | out = append(out, line) 70 | } 71 | return out 72 | } 73 | -------------------------------------------------------------------------------- /resourcemanager/resourceids/match.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package resourceids 5 | 6 | import ( 7 | "reflect" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-azure-helpers/resourcemanager/features" 11 | ) 12 | 13 | // Match compares two instances of the same ResourceId and determines whether they are a match 14 | // 15 | // Whilst it might seem fine to compare the result of the `.ID()` function, that doesn't account 16 | // for Resource ID Segments which need to be compared as case-insensitive. 17 | // 18 | // As such whilst this function is NOT exposing that functionality right now, it will when the 19 | // centralised feature-flag for this is rolled out. 20 | func Match(first, second ResourceId) bool { 21 | // since we're comparing interface types, ensure the two underlying types are the same 22 | if reflect.TypeOf(first) != reflect.TypeOf(second) { 23 | return false 24 | } 25 | 26 | parser := NewParserFromResourceIdType(first) 27 | firstParsed, err := parser.Parse(first.ID(), true) 28 | if err != nil { 29 | return false 30 | } 31 | secondParsed, err := parser.Parse(second.ID(), true) 32 | if err != nil { 33 | return false 34 | } 35 | firstVal := firstParsed.Parsed 36 | secondVal := secondParsed.Parsed 37 | if len(firstVal) != len(secondVal) { 38 | return false 39 | } 40 | for key, val := range firstVal { 41 | otherVal, ok := secondVal[key] 42 | if !ok { 43 | return false 44 | } 45 | 46 | segment := parser.namedSegment(key) 47 | if segment == nil { 48 | return false 49 | } 50 | 51 | if features.TreatUserSpecifiedSegmentsAsCaseInsensitive && segment.Type == UserSpecifiedSegmentType { 52 | if !strings.EqualFold(val, otherVal) { 53 | return false 54 | } 55 | } else if val != otherVal { 56 | return false 57 | } 58 | } 59 | 60 | return true 61 | } 62 | -------------------------------------------------------------------------------- /resourcemanager/systemdata/model.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package systemdata 5 | 6 | import "encoding/json" 7 | 8 | var _ json.Marshaler = &SystemData{} 9 | 10 | type SystemData struct { 11 | CreatedBy string `json:"createdBy"` 12 | CreatedByType string `json:"createdByType"` 13 | CreatedAt string `json:"createdAt"` 14 | LastModifiedBy string `json:"lastModifiedBy"` 15 | LastModifiedbyType string `json:"lastModifiedbyType"` 16 | LastModifiedAt string `json:"lastModifiedAt"` 17 | } 18 | 19 | func (s *SystemData) MarshalJSON() ([]byte, error) { 20 | // SystemData is a Read Only type. If Systemdata is part of a request some Azure APIs will ignore it, 21 | // others will return HTTP 400. We're returning nothing on purpose to avoid the error. 22 | return json.Marshal(nil) 23 | } 24 | -------------------------------------------------------------------------------- /resourcemanager/tags/expand.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tags 5 | 6 | // Expand transforms the input Tags to a `*map[string]string` 7 | func Expand(input map[string]interface{}) *map[string]string { 8 | output := make(map[string]string) 9 | 10 | for k, v := range input { 11 | tagKey := k 12 | tagValue := v.(string) 13 | output[tagKey] = tagValue 14 | } 15 | 16 | return &output 17 | } 18 | -------------------------------------------------------------------------------- /resourcemanager/tags/flatten.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tags 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | // Flatten transforms the Tags specified via `input` into a map[string]interface{} 13 | // for compatibility with the Schema. 14 | func Flatten(input *map[string]string) map[string]interface{} { 15 | output := make(map[string]interface{}) 16 | if input == nil { 17 | return output 18 | } 19 | 20 | for k, v := range *input { 21 | tagKey := k 22 | tagValue := v 23 | output[tagKey] = tagValue 24 | } 25 | 26 | return output 27 | } 28 | 29 | // FlattenAndSet first Flatten's the Tags and then sets the flattened value into 30 | // the `tags` field in the State. 31 | func FlattenAndSet(d *schema.ResourceData, input *map[string]string) error { 32 | tags := Flatten(input) 33 | 34 | if err := d.Set("tags", tags); err != nil { 35 | return fmt.Errorf("setting `tags`: %+v", err) 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /resourcemanager/tags/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tags 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | func Validate(v interface{}, _ string) (warnings []string, errors []error) { 12 | tagsMap := v.(map[string]interface{}) 13 | 14 | if len(tagsMap) > 50 { 15 | errors = append(errors, fmt.Errorf("a maximum of 50 tags can be applied to each resource")) 16 | } 17 | 18 | for k, v := range tagsMap { 19 | if len(k) > 512 { 20 | errors = append(errors, fmt.Errorf("the maximum length for a tag key is 512 characters: %q is %d characters", k, len(k))) 21 | } 22 | 23 | value, err := tagValueToString(v) 24 | if err != nil { 25 | errors = append(errors, err) 26 | } else if len(value) > 256 { 27 | errors = append(errors, fmt.Errorf("the maximum length for a tag value is 256 characters: the value for %q is %d characters", k, len(value))) 28 | } 29 | } 30 | 31 | return warnings, errors 32 | } 33 | 34 | func ValidateHasLowerCaseKeys(i interface{}, k string) (warnings []string, errors []error) { 35 | tagsMap, ok := i.(map[string]interface{}) 36 | if !ok { 37 | errors = append(errors, fmt.Errorf("expected type of %s to be map", k)) 38 | return warnings, errors 39 | } 40 | 41 | if len(tagsMap) > 50 { 42 | errors = append(errors, fmt.Errorf("a maximum of 50 tags can be applied to each ARM resource")) 43 | } 44 | 45 | for key, value := range tagsMap { 46 | if len(key) > 512 { 47 | errors = append(errors, fmt.Errorf("the maximum length for a tag key is 512 characters: %q has %d characters", key, len(key))) 48 | return warnings, errors 49 | } 50 | 51 | if strings.ToLower(key) != key { 52 | errors = append(errors, fmt.Errorf("a tag key %q expected to be all in lowercase", key)) 53 | return warnings, errors 54 | } 55 | 56 | v, err := tagValueToString(value) 57 | if err != nil { 58 | errors = append(errors, err) 59 | return warnings, errors 60 | } 61 | if len(v) > 256 { 62 | errors = append(errors, fmt.Errorf("the maximum length for a tag value is 256 characters: the value for %q has %d characters", key, len(v))) 63 | return warnings, errors 64 | } 65 | } 66 | 67 | return warnings, errors 68 | } 69 | 70 | func tagValueToString(v interface{}) (string, error) { 71 | switch value := v.(type) { 72 | case string: 73 | return value, nil 74 | case int: 75 | return fmt.Sprintf("%d", value), nil 76 | default: 77 | return "", fmt.Errorf("unknown tag type %T in tag value", value) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /resourcemanager/tags/validate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tags 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestValidateMaximumNumberOfTags(t *testing.T) { 13 | tagsMap := make(map[string]interface{}) 14 | for i := 0; i < 51; i++ { 15 | tagsMap[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i) 16 | } 17 | 18 | _, es := Validate(tagsMap, "tags") 19 | if len(es) != 1 { 20 | t.Fatal("Expected one validation error for too many tags") 21 | } 22 | 23 | if !strings.Contains(es[0].Error(), "a maximum of 50 tags") { 24 | t.Fatal("Wrong validation error message for too many tags") 25 | } 26 | } 27 | 28 | func TestValidateTagMaxKeyLength(t *testing.T) { 29 | tooLongKey := strings.Repeat("long", 128) + "a" 30 | tagsMap := make(map[string]interface{}) 31 | tagsMap[tooLongKey] = "value" 32 | 33 | _, es := Validate(tagsMap, "tags") 34 | if len(es) != 1 { 35 | t.Fatal("Expected one validation error for a key which is > 512 chars") 36 | } 37 | 38 | if !strings.Contains(es[0].Error(), "maximum length for a tag key") { 39 | t.Fatal("Wrong validation error message maximum tag key length") 40 | } 41 | 42 | if !strings.Contains(es[0].Error(), tooLongKey) { 43 | t.Fatal("Expected validated error to contain the key name") 44 | } 45 | 46 | if !strings.Contains(es[0].Error(), "513") { 47 | t.Fatal("Expected the length in the validation error for tag key") 48 | } 49 | } 50 | 51 | func TestValidateTagMaxValueLength(t *testing.T) { 52 | tagsMap := make(map[string]interface{}) 53 | tagsMap["toolong"] = strings.Repeat("long", 64) + "a" 54 | 55 | _, es := Validate(tagsMap, "tags") 56 | if len(es) != 1 { 57 | t.Fatal("Expected one validation error for a value which is > 256 chars") 58 | } 59 | 60 | if !strings.Contains(es[0].Error(), "maximum length for a tag value") { 61 | t.Fatal("Wrong validation error message for maximum tag value length") 62 | } 63 | 64 | if !strings.Contains(es[0].Error(), "toolong") { 65 | t.Fatal("Expected validated error to contain the key name") 66 | } 67 | 68 | if !strings.Contains(es[0].Error(), "257") { 69 | t.Fatal("Expected the length in the validation error for value") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /resourcemanager/zones/expand.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package zones 5 | 6 | func Expand(input []string) Schema { 7 | out := Schema{} 8 | 9 | out = append(out, input...) 10 | 11 | return out 12 | } 13 | 14 | func ExpandUntyped(input []interface{}) []string { 15 | out := make([]string, 0) 16 | 17 | for _, v := range input { 18 | out = append(out, v.(string)) 19 | } 20 | 21 | return out 22 | } 23 | -------------------------------------------------------------------------------- /resourcemanager/zones/flatten.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package zones 5 | 6 | func Flatten(input *Schema) []string { 7 | out := make([]string, 0) 8 | 9 | if input != nil { 10 | out = append(out, *input...) 11 | } 12 | 13 | return out 14 | } 15 | 16 | func FlattenUntyped(input *[]string) []interface{} { 17 | out := make([]interface{}, 0) 18 | 19 | if input != nil { 20 | for _, v := range *input { 21 | out = append(out, v) 22 | } 23 | } 24 | 25 | return out 26 | } 27 | -------------------------------------------------------------------------------- /resourcemanager/zones/schema.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package zones 5 | 6 | type Schema = []string 7 | -------------------------------------------------------------------------------- /scripts/determine-and-publish-git-tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | set -e 7 | 8 | function determineGitTag { 9 | # Whilst we could do something fancy here to determine the version, tbh 10 | # it doesn't matter providing it's: 11 | # a) 0-based (so we don't bump the Go module version/trigger import path changes) 12 | # b) ordered 13 | # c) clear when it was released 14 | # 15 | # whilst an odd choice for an SDK, makes sense insofar as the API Versions 16 | # themselves are versioned too - so we can use a date/time stamp for now: 17 | # e.g.v0.YYYYMMDD.HHMMSS (v0.20220504.115712) 18 | # 19 | # turns out that Go doesn't like the minor/patch to start with a 0, else 20 | # the Go Module Proxy returns: 21 | # > bad request: version "v0.20220622.050833" is not canonical (wanted "") 22 | # as such we'll prefix the Hours-Minutes-Seconds segment with a `1` for now 23 | date '+v0.%Y%m%d.1%H%M%S' 24 | } 25 | 26 | function publish { 27 | local version=$1 28 | 29 | echo "Tagging as '$version'.." 30 | git tag "$version" 31 | 32 | echo "Pushing Tags.." 33 | git push --tags 34 | } 35 | 36 | function main { 37 | local gitTag 38 | gitTag=$(determineGitTag) 39 | publish "$gitTag" 40 | } 41 | 42 | main 43 | -------------------------------------------------------------------------------- /scripts/update-azurerm-provider.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | set -e 6 | 7 | function prepare { 8 | local workingDirectory=$1 9 | 10 | echo "Recreating the working directory at '${workingDirectory}'.." 11 | rm -rf "${workingDirectory}" 12 | mkdir -p "${workingDirectory}" 13 | 14 | local repositoryDirectory="terraform-provider-azurerm" 15 | 16 | echo "Cloning AzureRM.." 17 | pushd "${workingDirectory}" 18 | git clone git@github.com:hashicorp/terraform-provider-azurerm.git "${repositoryDirectory}" 19 | 20 | echo "Returning to the original directory.." 21 | popd 22 | } 23 | 24 | function runUpdaterTool { 25 | local workingDirectory=$1 26 | local newHelpersVersion=$2 27 | local branch="auto-deps-pr/updating-go-azure-helpers-to-${newHelpersVersion}" 28 | 29 | echo "Moving into the AzureRM Provider directory.." 30 | pwd 31 | pushd "${workingDirectory}/terraform-provider-azurerm" 32 | 33 | echo "Checking out a new branch.." 34 | git checkout -b "${branch}" 35 | 36 | echo "Building the updater tool.." 37 | cd ./internal/tools/update-go-azure-sdk 38 | go build . 39 | 40 | echo "Configuring Git in the AzureRM repository.." 41 | git config --global user.name "hc-github-team-tf-azure" 42 | git config --global user.email '<>' 43 | 44 | echo "Running the updater tool.." 45 | ./update-go-azure-helpers --new-helpers-version="${newHelpersVersion}" --azurerm-repo-path=../../../ --azure-helpers-repo-path=../../../../../ 46 | 47 | hasChangesToPush="no" 48 | if [[ $(git diff main --name-only | wc -l) -gt 0 ]]; then 49 | echo "Pushing the branch" 50 | git push origin "$branch" -f 51 | hasChangesToPush="yes" 52 | else 53 | echo "No changes to push - skipping" 54 | fi 55 | 56 | echo "Returning to the original directory" 57 | popd 58 | 59 | echo "Writing has changes to push to file" 60 | echo "${hasChangesToPush}" > "${workingDirectory}/has-changes-to-push.txt" 61 | } 62 | 63 | function main { 64 | local workingDirectory="./tmp" 65 | local newHelpersVersion=$1 66 | 67 | prepare "$workingDirectory" 68 | runUpdaterTool "$workingDirectory" "$newHelpersVersion" 69 | 70 | exit 0 71 | } 72 | 73 | # 1 = go-azure-helpers version 74 | main "$1" 75 | -------------------------------------------------------------------------------- /sender/sender.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package sender 5 | 6 | import ( 7 | "log" 8 | "net/http" 9 | "net/http/httputil" 10 | 11 | "github.com/Azure/go-autorest/autorest" 12 | ) 13 | 14 | func BuildSender(providerName string) autorest.Sender { 15 | return autorest.DecorateSender(&http.Client{ 16 | Transport: &http.Transport{ 17 | Proxy: http.ProxyFromEnvironment, 18 | }, 19 | }, withRequestLogging(providerName)) 20 | } 21 | 22 | func withRequestLogging(providerName string) autorest.SendDecorator { 23 | return func(s autorest.Sender) autorest.Sender { 24 | return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { 25 | // strip the authorization header prior to printing 26 | authHeaderName := "Authorization" 27 | auth := r.Header.Get(authHeaderName) 28 | if auth != "" { 29 | r.Header.Del(authHeaderName) 30 | } 31 | 32 | // dump request to wire format 33 | if dump, err := httputil.DumpRequestOut(r, true); err == nil { 34 | log.Printf("[DEBUG] %s Request: \n%s\n", providerName, dump) 35 | } else { 36 | // fallback to basic message 37 | log.Printf("[DEBUG] %s Request: %s to %s\n", providerName, r.Method, r.URL) 38 | } 39 | 40 | // add the auth header back 41 | if auth != "" { 42 | r.Header.Add(authHeaderName, auth) 43 | } 44 | 45 | resp, err := s.Do(r) 46 | if resp != nil { 47 | // dump response to wire format 48 | if dump, err2 := httputil.DumpResponse(resp, true); err2 == nil { 49 | log.Printf("[DEBUG] %s Response for %s: \n%s\n", providerName, r.URL, dump) 50 | } else { 51 | // fallback to basic message 52 | log.Printf("[DEBUG] %s Response: %s for %s\n", providerName, resp.Status, r.URL) 53 | } 54 | } else if err != nil { 55 | log.Printf("[DEBUG] %s Response Error: %s for %s\n", providerName, err, r.URL) 56 | } else { 57 | log.Printf("[DEBUG] Request to %s completed with no response", r.URL) 58 | } 59 | return resp, err 60 | }) 61 | } 62 | } 63 | --------------------------------------------------------------------------------