diff --git a/internal/services/securitycenter/iot_security_solution_resource.go b/internal/services/securitycenter/iot_security_solution_resource.go index a20a97b888c7..8386fe8da445 100644 --- a/internal/services/securitycenter/iot_security_solution_resource.go +++ b/internal/services/securitycenter/iot_security_solution_resource.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" iothubValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/iothub/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/securitycenter/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/securitycenter/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/securitycenter/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tags" @@ -35,6 +36,11 @@ func resourceIotSecuritySolution() *pluginsdk.Resource { return err }), + SchemaVersion: 1, + StateUpgraders: pluginsdk.StateUpgrades(map[int]pluginsdk.StateUpgrade{ + 0: migration.SecurityCenterIotSecuritySolutionV0ToV1{}, + }), + Timeouts: &pluginsdk.ResourceTimeout{ Create: pluginsdk.DefaultTimeout(30 * time.Minute), Read: pluginsdk.DefaultTimeout(5 * time.Minute), diff --git a/internal/services/securitycenter/migration/iot_security_solution_v0_to_v1.go b/internal/services/securitycenter/migration/iot_security_solution_v0_to_v1.go new file mode 100644 index 000000000000..a8e8dee933ac --- /dev/null +++ b/internal/services/securitycenter/migration/iot_security_solution_v0_to_v1.go @@ -0,0 +1,234 @@ +package migration + +import ( + "context" + "log" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/securitycenter/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tags" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/set" +) + +type SecurityCenterIotSecuritySolutionV0ToV1 struct{} + +func (s SecurityCenterIotSecuritySolutionV0ToV1) Schema() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + }, + + "resource_group_name": commonschema.ResourceGroupName(), + + "location": commonschema.Location(), + + "display_name": { + Type: pluginsdk.TypeString, + Required: true, + }, + + "iothub_ids": { + Type: pluginsdk.TypeSet, + Required: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + Set: set.HashStringIgnoreCase, + }, + + "additional_workspace": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "data_types": { + Type: pluginsdk.TypeSet, + Required: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "workspace_id": { + Type: pluginsdk.TypeString, + Required: true, + }, + }, + }, + }, + + "disabled_data_sources": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "log_analytics_workspace_id": { + Type: pluginsdk.TypeString, + Optional: true, + }, + + "enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "log_unmasked_ips_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + + "events_to_export": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "recommendations_enabled": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "acr_authentication": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "agent_send_unutilized_msg": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "baseline": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "edge_hub_mem_optimize": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "edge_logging_option": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "inconsistent_module_settings": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "install_agent": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "ip_filter_deny_all": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "ip_filter_permissive_rule": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "open_ports": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "permissive_firewall_policy": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "permissive_input_firewall_rules": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "permissive_output_firewall_rules": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "privileged_docker_options": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "shared_credentials": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "vulnerable_tls_cipher_suite": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + }, + }, + }, + + "query_for_resources": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + }, + + "query_subscription_ids": { + Type: pluginsdk.TypeSet, + Optional: true, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "tags": tags.Schema(), + } +} + +func (s SecurityCenterIotSecuritySolutionV0ToV1) 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.IotSecuritySolutionIDInsensitively(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/securitycenter/parse/iot_security_solution.go b/internal/services/securitycenter/parse/iot_security_solution.go index e032f0a4c02b..aeada8ec3589 100644 --- a/internal/services/securitycenter/parse/iot_security_solution.go +++ b/internal/services/securitycenter/parse/iot_security_solution.go @@ -33,7 +33,7 @@ func (id IotSecuritySolutionId) String() string { } func (id IotSecuritySolutionId) ID() string { - fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Security/IoTSecuritySolutions/%s" + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Security/iotSecuritySolutions/%s" return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.Name) } @@ -57,7 +57,51 @@ func IotSecuritySolutionID(input string) (*IotSecuritySolutionId, error) { return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") } - if resourceId.Name, err = id.PopSegment("IoTSecuritySolutions"); err != nil { + if resourceId.Name, err = id.PopSegment("iotSecuritySolutions"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} + +// IotSecuritySolutionIDInsensitively parses an IotSecuritySolution ID into an IotSecuritySolutionId struct, insensitively +// This should only be used to parse an ID for rewriting, the IotSecuritySolutionID +// 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 IotSecuritySolutionIDInsensitively(input string) (*IotSecuritySolutionId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := IotSecuritySolutionId{ + 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 'iotSecuritySolutions' segment + iotSecuritySolutionsKey := "iotSecuritySolutions" + for key := range id.Path { + if strings.EqualFold(key, iotSecuritySolutionsKey) { + iotSecuritySolutionsKey = key + break + } + } + if resourceId.Name, err = id.PopSegment(iotSecuritySolutionsKey); err != nil { return nil, err } diff --git a/internal/services/securitycenter/parse/iot_security_solution_test.go b/internal/services/securitycenter/parse/iot_security_solution_test.go index dc94d2777fdd..c6e666bb28db 100644 --- a/internal/services/securitycenter/parse/iot_security_solution_test.go +++ b/internal/services/securitycenter/parse/iot_security_solution_test.go @@ -12,7 +12,7 @@ var _ resourceids.Id = IotSecuritySolutionId{} func TestIotSecuritySolutionIDFormatter(t *testing.T) { actual := NewIotSecuritySolutionID("12345678-1234-9876-4563-123456789012", "resGroup1", "solution1").ID() - expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1" + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/solution1" if actual != expected { t.Fatalf("Expected %q but got %q", expected, actual) } @@ -63,13 +63,13 @@ func TestIotSecuritySolutionID(t *testing.T) { { // missing value for Name - Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/", Error: true, }, { // valid - Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/solution1", Expected: &IotSecuritySolutionId{ SubscriptionId: "12345678-1234-9876-4563-123456789012", ResourceGroup: "resGroup1", @@ -110,3 +110,120 @@ func TestIotSecuritySolutionID(t *testing.T) { } } } + +func TestIotSecuritySolutionIDInsensitively(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *IotSecuritySolutionId + }{ + + { + // 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 Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/solution1", + Expected: &IotSecuritySolutionId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + Name: "solution1", + }, + }, + + { + // lower-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotsecuritysolutions/solution1", + Expected: &IotSecuritySolutionId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + Name: "solution1", + }, + }, + + { + // upper-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IOTSECURITYSOLUTIONS/solution1", + Expected: &IotSecuritySolutionId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + Name: "solution1", + }, + }, + + { + // mixed-cased segment names + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTsEcUrItYsOlUtIoNs/solution1", + Expected: &IotSecuritySolutionId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + Name: "solution1", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := IotSecuritySolutionIDInsensitively(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.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} diff --git a/internal/services/securitycenter/resourceids.go b/internal/services/securitycenter/resourceids.go index 934b581a6c8c..35c488bcaf9f 100644 --- a/internal/services/securitycenter/resourceids.go +++ b/internal/services/securitycenter/resourceids.go @@ -8,6 +8,6 @@ package securitycenter //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=AssessmentMetadata -id=/subscriptions/12345678-1234-9876-4563-123456789012/providers/Microsoft.Security/assessmentMetadata/metadata1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=AutoProvisioningSetting -id=/subscriptions/12345678-1234-9876-4563-123456789012/providers/Microsoft.Security/autoProvisioningSettings/default -rewrite=true -//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=IotSecuritySolution -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=IotSecuritySolution -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/solution1 -rewrite=true //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=VulnerabilityAssessmentVm -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Compute/virtualMachines/vm-name1/providers/Microsoft.Security/serverVulnerabilityAssessments/default1 diff --git a/internal/services/securitycenter/validate/iot_security_solution_id_test.go b/internal/services/securitycenter/validate/iot_security_solution_id_test.go index 8bfcdb64f56d..d2b30ab4bb20 100644 --- a/internal/services/securitycenter/validate/iot_security_solution_id_test.go +++ b/internal/services/securitycenter/validate/iot_security_solution_id_test.go @@ -48,13 +48,13 @@ func TestIotSecuritySolutionID(t *testing.T) { { // missing value for Name - Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/", Valid: false, }, { // valid - Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/solution1", Valid: true, }, diff --git a/website/docs/r/iot_security_solution.html.markdown b/website/docs/r/iot_security_solution.html.markdown index f3acd36f496b..6d2e23d44fb0 100644 --- a/website/docs/r/iot_security_solution.html.markdown +++ b/website/docs/r/iot_security_solution.html.markdown @@ -136,5 +136,5 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/l Iot Security Solution can be imported using the `resource id`, e.g. ```shell -terraform import azurerm_iot_security_solution.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1 +terraform import azurerm_iot_security_solution.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Security/iotSecuritySolutions/solution1 ```