From e20b9ea23fccb376eaaab2a75408402ce5fe7f62 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 30 Nov 2022 16:14:29 -0800 Subject: [PATCH] fhir - a state migration to work around the previously incorrect id casing --- .../healthcare/healthcare_fhir_resource.go | 6 + .../healthcare_fhir_migration_v0_to_v1.go | 178 ++++++++++++++++++ .../services/healthcare/parse/fhir_service.go | 60 +++++- .../healthcare/parse/fhir_service_test.go | 142 +++++++++++++- internal/services/healthcare/resourceids.go | 2 +- .../validate/fhir_service_id_test.go | 4 +- .../r/healthcare_fhir_service.html.markdown | 2 +- .../healthcare_medtech_service.html.markdown | 2 +- ...ech_service_fhir_destination.html.markdown | 2 +- 9 files changed, 387 insertions(+), 11 deletions(-) create mode 100644 internal/services/healthcare/migration/healthcare_fhir_migration_v0_to_v1.go diff --git a/internal/services/healthcare/healthcare_fhir_resource.go b/internal/services/healthcare/healthcare_fhir_resource.go index f3225388a397..85d273a71052 100644 --- a/internal/services/healthcare/healthcare_fhir_resource.go +++ b/internal/services/healthcare/healthcare_fhir_resource.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/healthcare/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/healthcare/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/healthcare/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tags" @@ -37,6 +38,11 @@ func resourceHealthcareApisFhirService() *pluginsdk.Resource { Delete: pluginsdk.DefaultTimeout(30 * time.Minute), }, + SchemaVersion: 1, + StateUpgraders: pluginsdk.StateUpgrades(map[int]pluginsdk.StateUpgrade{ + 0: migration.HealthCareFhirV0ToV1{}, + }), + Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { _, err := parse.FhirServiceID(id) return err diff --git a/internal/services/healthcare/migration/healthcare_fhir_migration_v0_to_v1.go b/internal/services/healthcare/migration/healthcare_fhir_migration_v0_to_v1.go new file mode 100644 index 000000000000..e60c4a6ed67e --- /dev/null +++ b/internal/services/healthcare/migration/healthcare_fhir_migration_v0_to_v1.go @@ -0,0 +1,178 @@ +package migration + +import ( + "context" + "log" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/healthcare/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type HealthCareFhirV0ToV1 struct{} + +func (s HealthCareFhirV0ToV1) Schema() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + + "resource_group_name": commonschema.ResourceGroupName(), + + "workspace_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + + "location": commonschema.Location(), + + "kind": { + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + }, + + "access_policy_object_ids": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "authentication": { + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "authority": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "audience": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "smart_proxy_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + }, + }, + }, + }, + + "identity": commonschema.SystemAssignedIdentityOptional(), + + // can't use the registry ID due to the ID cannot be obtained when setting the property in state file + "container_registry_login_server_url": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "oci_artifact": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "login_server": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "image_name": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "digest": { + Type: pluginsdk.TypeString, + Optional: true, + }, + }, + }, + }, + + "cors": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "allowed_origins": { + Type: pluginsdk.TypeSet, + Required: true, + MaxItems: 64, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "allowed_headers": { + Type: pluginsdk.TypeSet, + Required: true, + MaxItems: 64, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "allowed_methods": { + Type: pluginsdk.TypeSet, + Required: true, + MaxItems: 64, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "max_age_in_seconds": { + Type: pluginsdk.TypeInt, + Optional: true, + }, + + "credentials_allowed": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + + "configuration_export_storage_account_name": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "public_network_access_enabled": { + Type: pluginsdk.TypeBool, + Computed: true, + }, + + "tags": commonschema.Tags(), + } +} + +func (s HealthCareFhirV0ToV1) UpgradeFunc() pluginsdk.StateUpgraderFunc { + return func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + oldId := rawState["id"].(string) + newId, err := parse.FhirServiceIDInsensitively(oldId) + if err != nil { + return nil, err + } + + log.Printf("[DEBUG] Updating ID from %q to %q", oldId, newId) + + rawState["id"] = newId.ID() + return rawState, nil + } +} diff --git a/internal/services/healthcare/parse/fhir_service.go b/internal/services/healthcare/parse/fhir_service.go index b28fa20a9244..e4e8745e7728 100644 --- a/internal/services/healthcare/parse/fhir_service.go +++ b/internal/services/healthcare/parse/fhir_service.go @@ -36,7 +36,7 @@ func (id FhirServiceId) String() string { } func (id FhirServiceId) ID() string { - fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.HealthcareApis/workspaces/%s/fhirservices/%s" + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.HealthcareApis/workspaces/%s/fhirServices/%s" return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.WorkspaceName, id.Name) } @@ -63,7 +63,63 @@ func FhirServiceID(input string) (*FhirServiceId, error) { if resourceId.WorkspaceName, err = id.PopSegment("workspaces"); err != nil { return nil, err } - if resourceId.Name, err = id.PopSegment("fhirservices"); err != nil { + if resourceId.Name, err = id.PopSegment("fhirServices"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} + +// FhirServiceIDInsensitively parses an FhirService ID into an FhirServiceId struct, insensitively +// This should only be used to parse an ID for rewriting, the FhirServiceID +// method should be used instead for validation etc. +// +// Whilst this may seem strange, this enables Terraform have consistent casing +// which works around issues in Core, whilst handling broken API responses. +func FhirServiceIDInsensitively(input string) (*FhirServiceId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := FhirServiceId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + // find the correct casing for the 'workspaces' segment + workspacesKey := "workspaces" + for key := range id.Path { + if strings.EqualFold(key, workspacesKey) { + workspacesKey = key + break + } + } + if resourceId.WorkspaceName, err = id.PopSegment(workspacesKey); err != nil { + return nil, err + } + + // find the correct casing for the 'fhirServices' segment + fhirServicesKey := "fhirServices" + for key := range id.Path { + if strings.EqualFold(key, fhirServicesKey) { + fhirServicesKey = key + break + } + } + if resourceId.Name, err = id.PopSegment(fhirServicesKey); err != nil { return nil, err } diff --git a/internal/services/healthcare/parse/fhir_service_test.go b/internal/services/healthcare/parse/fhir_service_test.go index 45e3c7919c90..ecb21fcebc6a 100644 --- a/internal/services/healthcare/parse/fhir_service_test.go +++ b/internal/services/healthcare/parse/fhir_service_test.go @@ -12,7 +12,7 @@ var _ resourceids.Id = FhirServiceId{} func TestFhirServiceIDFormatter(t *testing.T) { actual := NewFhirServiceID("12345678-1234-9876-4563-123456789012", "group1", "workspace1", "service1").ID() - expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/service1" + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/service1" if actual != expected { t.Fatalf("Expected %q but got %q", expected, actual) } @@ -75,13 +75,13 @@ func TestFhirServiceID(t *testing.T) { { // missing value for Name - Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/", Error: true, }, { // valid - Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/service1", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/service1", Expected: &FhirServiceId{ SubscriptionId: "12345678-1234-9876-4563-123456789012", ResourceGroup: "group1", @@ -126,3 +126,139 @@ func TestFhirServiceID(t *testing.T) { } } } + +func TestFhirServiceIDInsensitively(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *FhirServiceId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing WorkspaceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/", + Error: true, + }, + + { + // missing value for WorkspaceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/", + Error: true, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/service1", + Expected: &FhirServiceId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "group1", + WorkspaceName: "workspace1", + Name: "service1", + }, + }, + + { + // lower-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/service1", + Expected: &FhirServiceId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "group1", + WorkspaceName: "workspace1", + Name: "service1", + }, + }, + + { + // upper-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/WORKSPACES/workspace1/FHIRSERVICES/service1", + Expected: &FhirServiceId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "group1", + WorkspaceName: "workspace1", + Name: "service1", + }, + }, + + { + // mixed-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/WoRkSpAcEs/workspace1/FhIrSeRvIcEs/service1", + Expected: &FhirServiceId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "group1", + WorkspaceName: "workspace1", + Name: "service1", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := FhirServiceIDInsensitively(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.WorkspaceName != v.Expected.WorkspaceName { + t.Fatalf("Expected %q but got %q for WorkspaceName", v.Expected.WorkspaceName, actual.WorkspaceName) + } + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} diff --git a/internal/services/healthcare/resourceids.go b/internal/services/healthcare/resourceids.go index 3800c7884de4..77071444dd91 100644 --- a/internal/services/healthcare/resourceids.go +++ b/internal/services/healthcare/resourceids.go @@ -4,6 +4,6 @@ package healthcare //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Workspace -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=FhirService -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/service1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=DicomService -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/dicomServices/service1 -rewrite=true -//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=FhirService -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/service1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=FhirService -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/service1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=MedTechService -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/iotconnectors/iotconnector1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=MedTechServiceFhirDestination -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/iotconnectors/iotconnector1/fhirdestinations/destination1 diff --git a/internal/services/healthcare/validate/fhir_service_id_test.go b/internal/services/healthcare/validate/fhir_service_id_test.go index 511a3576807e..b03f3cbe5833 100644 --- a/internal/services/healthcare/validate/fhir_service_id_test.go +++ b/internal/services/healthcare/validate/fhir_service_id_test.go @@ -60,13 +60,13 @@ func TestFhirServiceID(t *testing.T) { { // missing value for Name - Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/", Valid: false, }, { // valid - Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/service1", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/service1", Valid: true, }, diff --git a/website/docs/r/healthcare_fhir_service.html.markdown b/website/docs/r/healthcare_fhir_service.html.markdown index bc3236ed7e90..e030ee58ce72 100644 --- a/website/docs/r/healthcare_fhir_service.html.markdown +++ b/website/docs/r/healthcare_fhir_service.html.markdown @@ -140,5 +140,5 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/l Healthcare FHIR Service can be imported using the resource`id`, e.g. ```shell -terraform import azurerm_healthcare_fhir_service.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirservices/service1 +terraform import azurerm_healthcare_fhir_service.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/fhirServices/service1 ``` diff --git a/website/docs/r/healthcare_medtech_service.html.markdown b/website/docs/r/healthcare_medtech_service.html.markdown index 275fe479f76b..c0366939f43e 100644 --- a/website/docs/r/healthcare_medtech_service.html.markdown +++ b/website/docs/r/healthcare_medtech_service.html.markdown @@ -108,5 +108,5 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/d Healthcare Med Tech Service can be imported using the resource`id`, e.g. ```shell -terraform import azurerm_healthcare_medtech_service.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/iotconnectors/iotconnector1 +terraform import azurerm_healthcare_medtech_service.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/iotConnectors/iotconnector1 ``` diff --git a/website/docs/r/healthcare_medtech_service_fhir_destination.html.markdown b/website/docs/r/healthcare_medtech_service_fhir_destination.html.markdown index aedceaeed2ee..483d9418b852 100644 --- a/website/docs/r/healthcare_medtech_service_fhir_destination.html.markdown +++ b/website/docs/r/healthcare_medtech_service_fhir_destination.html.markdown @@ -82,5 +82,5 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/d Healthcare Med Tech Service Fhir Destination can be imported using the resource`id`, e.g. ```shell -terraform import azurerm_healthcare_medtech_service_fhir_destination.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/iotconnectors/iotconnector1/fhirdestinations/destination1 +terraform import azurerm_healthcare_medtech_service_fhir_destination.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.HealthcareApis/workspaces/workspace1/iotConnectors/iotconnector1/fhirDestinations/destination1 ```