diff --git a/internal/services/iotcentral/iotcentral_organization_resource.go b/internal/services/iotcentral/iotcentral_organization_resource.go index 48e6f25c30059..2689e2bc435a7 100644 --- a/internal/services/iotcentral/iotcentral_organization_resource.go +++ b/internal/services/iotcentral/iotcentral_organization_resource.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/hashicorp/go-azure-sdk/resource-manager/iotcentral/2021-11-01-preview/apps" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/iotcentral/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/iotcentral/validate" @@ -22,24 +23,25 @@ var ( ) type IotCentralOrganizationModel struct { - SubDomain string `tfschema:"sub_domain"` - OrganizationId string `tfschema:"organization_id"` - DisplayName string `tfschema:"display_name"` - ParentOrganizationId string `tfschema:"parent_organization_id"` + IotCentralApplicationId string `tfschema:"iotcentral_application_id"` + OrganizationId string `tfschema:"organization_id"` + DisplayName string `tfschema:"display_name"` + ParentOrganizationId string `tfschema:"parent_organization_id"` } func (r IotCentralOrganizationResource) Arguments() map[string]*pluginsdk.Schema { return map[string]*pluginsdk.Schema{ - "sub_domain": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, + "iotcentral_application_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: apps.ValidateIotAppID, }, "organization_id": { Type: pluginsdk.TypeString, Required: true, ForceNew: true, - ValidateFunc: validate.OrganizationID, + ValidateFunc: validate.OrganizationOrganizationID, }, "display_name": { Type: pluginsdk.TypeString, @@ -49,7 +51,7 @@ func (r IotCentralOrganizationResource) Arguments() map[string]*pluginsdk.Schema Type: pluginsdk.TypeString, Optional: true, ForceNew: true, - ValidateFunc: validate.OrganizationID, + ValidateFunc: validate.OrganizationOrganizationID, }, } } @@ -67,7 +69,7 @@ func (r IotCentralOrganizationResource) ModelObject() interface{} { } func (r IotCentralOrganizationResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { - return validate.ID + return validate.OrganizationID } func (r IotCentralOrganizationResource) Create() sdk.ResourceFunc { @@ -79,7 +81,17 @@ func (r IotCentralOrganizationResource) Create() sdk.ResourceFunc { return err } - orgClient, err := client.OrganizationsClient(ctx, state.SubDomain) + appId, err := apps.ParseIotAppID(state.IotCentralApplicationId) + if err != nil { + return err + } + + app, err := client.AppsClient.Get(ctx, *appId) + if err != nil || app.Model == nil { + return fmt.Errorf("checking for the presence of existing %q: %+v", appId, err) + } + + orgClient, err := client.OrganizationsClient(ctx, *app.Model.Properties.Subdomain) if err != nil { return fmt.Errorf("creating organization client: %+v", err) } @@ -97,10 +109,7 @@ func (r IotCentralOrganizationResource) Create() sdk.ResourceFunc { return fmt.Errorf("creating %s: %+v", state.OrganizationId, err) } - orgId, err := parse.NewOrganizationID(state.SubDomain, client.Endpoint.Name(), *org.ID) - if err != nil { - return err - } + orgId := parse.NewOrganizationID(appId.SubscriptionId, appId.ResourceGroupName, appId.IotAppName, *org.ID) metadata.SetID(orgId) return nil @@ -113,17 +122,27 @@ func (r IotCentralOrganizationResource) Read() sdk.ResourceFunc { return sdk.ResourceFunc{ Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { client := metadata.Client.IoTCentral - id, err := parse.ParseOrganizationID(metadata.ResourceData.Id(), metadata.ResourceData.Get("sub_domain").(string), client.Endpoint.Name()) + id, err := parse.OrganizationID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + appId := apps.NewIotAppID(id.SubscriptionId, id.ResourceGroup, id.IotAppName) if err != nil { return err } - orgClient, err := client.OrganizationsClient(ctx, id.SubDomain) + app, err := client.AppsClient.Get(ctx, appId) + if err != nil || app.Model == nil { + return metadata.MarkAsGone(id) + } + + orgClient, err := client.OrganizationsClient(ctx, *app.Model.Properties.Subdomain) if err != nil { return fmt.Errorf("creating organization client: %+v", err) } - org, err := orgClient.Get(ctx, id.OrganizationId) + org, err := orgClient.Get(ctx, id.Name) if err != nil { if org.ID == nil || *org.ID == "" { return metadata.MarkAsGone(id) @@ -133,9 +152,9 @@ func (r IotCentralOrganizationResource) Read() sdk.ResourceFunc { } state := IotCentralOrganizationModel{ - SubDomain: id.SubDomain, - OrganizationId: id.OrganizationId, - DisplayName: *org.DisplayName, + IotCentralApplicationId: appId.ID(), + OrganizationId: id.Name, + DisplayName: *org.DisplayName, } if org.Parent != nil { @@ -157,19 +176,33 @@ func (r IotCentralOrganizationResource) Update() sdk.ResourceFunc { return err } - id, err := parse.ParseOrganizationID(metadata.ResourceData.Id(), state.SubDomain, client.Endpoint.Name()) + id, err := parse.OrganizationID(metadata.ResourceData.Id()) if err != nil { return err } - orgClient, err := client.OrganizationsClient(ctx, id.SubDomain) + appId := apps.NewIotAppID(id.SubscriptionId, id.ResourceGroup, id.IotAppName) + if err != nil { + return err + } + + app, err := client.AppsClient.Get(ctx, appId) + if err != nil || app.Model == nil { + return metadata.MarkAsGone(id) + } + + orgClient, err := client.OrganizationsClient(ctx, *app.Model.Properties.Subdomain) if err != nil { return fmt.Errorf("creating organization client: %+v", err) } - existing, err := orgClient.Get(ctx, id.OrganizationId) + existing, err := orgClient.Get(ctx, id.Name) if err != nil { - return fmt.Errorf("retrieving %s: %+v", id, err) + if existing.ID == nil || *existing.ID == "" { + return metadata.MarkAsGone(id) + } + + return fmt.Errorf("retrieving %s: %+v", *id, err) } if metadata.ResourceData.HasChange("display_name") { @@ -196,17 +229,27 @@ func (r IotCentralOrganizationResource) Delete() sdk.ResourceFunc { return err } - id, err := parse.ParseOrganizationID(metadata.ResourceData.Id(), state.SubDomain, client.Endpoint.Name()) + id, err := parse.OrganizationID(metadata.ResourceData.Id()) if err != nil { return err } - orgClient, err := client.OrganizationsClient(ctx, id.SubDomain) + appId := apps.NewIotAppID(id.SubscriptionId, id.ResourceGroup, id.IotAppName) + if err != nil { + return err + } + + app, err := client.AppsClient.Get(ctx, appId) + if err != nil || app.Model == nil { + return metadata.MarkAsGone(id) + } + + orgClient, err := client.OrganizationsClient(ctx, *app.Model.Properties.Subdomain) if err != nil { return fmt.Errorf("creating organization client: %+v", err) } - _, err = orgClient.Remove(ctx, id.OrganizationId) + _, err = orgClient.Remove(ctx, id.Name) if err != nil { return fmt.Errorf("deleting %s: %+v", id, err) } diff --git a/internal/services/iotcentral/iotcentral_organization_resource_test.go b/internal/services/iotcentral/iotcentral_organization_resource_test.go index ba46e816086bd..40d2388649296 100644 --- a/internal/services/iotcentral/iotcentral_organization_resource_test.go +++ b/internal/services/iotcentral/iotcentral_organization_resource_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/hashicorp/go-azure-sdk/resource-manager/iotcentral/2021-11-01-preview/apps" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" @@ -143,17 +144,27 @@ func TestAccIoTCentralOrganization_unsetParent(t *testing.T) { } func (IoTCentralOrganizationResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { - id, err := parse.ParseOrganizationID(state.ID, state.Attributes["sub_domain"], clients.IoTCentral.Endpoint.Name()) + id, err := parse.OrganizationID(state.ID) if err != nil { return nil, err } - orgClient, err := clients.IoTCentral.OrganizationsClient(ctx, id.SubDomain) + appId, err := apps.ParseIotAppID(state.Attributes["iotcentral_application_id"]) + if err != nil { + return nil, err + } + + app, err := clients.IoTCentral.AppsClient.Get(ctx, *appId) + if err != nil || app.Model == nil { + return nil, fmt.Errorf("checking for the presence of existing %q: %+v", appId, err) + } + + orgClient, err := clients.IoTCentral.OrganizationsClient(ctx, *app.Model.Properties.Subdomain) if err != nil { return nil, fmt.Errorf("creating organization client: %+v", err) } - resp, err := orgClient.Get(ctx, id.OrganizationId) + resp, err := orgClient.Get(ctx, id.Name) if err != nil { return nil, fmt.Errorf("retrieving %s: %+v", id, err) } @@ -168,9 +179,9 @@ provider "azurerm" { } %s resource "azurerm_iotcentral_organization" "test" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-id" - display_name = "Org basic" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-id" + display_name = "Org basic" } `, r.template(data)) } @@ -182,14 +193,14 @@ provider "azurerm" { } %s resource "azurerm_iotcentral_organization" "test_parent" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-parent-id" - display_name = "Org parent" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-parent-id" + display_name = "Org parent" } resource "azurerm_iotcentral_organization" "test" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-id" - display_name = "Org child" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-id" + display_name = "Org child" parent_organization_id = azurerm_iotcentral_organization.test_parent.organization_id } @@ -203,9 +214,9 @@ provider "azurerm" { } %s resource "azurerm_iotcentral_organization" "test" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-id" - display_name = "Org basic updated" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-id" + display_name = "Org basic updated" } `, r.template(data)) } @@ -217,19 +228,19 @@ provider "azurerm" { } %s resource "azurerm_iotcentral_organization" "test_parent" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-parent-id" - display_name = "Org parent" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-parent-id" + display_name = "Org parent" } resource "azurerm_iotcentral_organization" "test_parent_2" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-parent-2-id" - display_name = "Org parent 2" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-parent-2-id" + display_name = "Org parent 2" } resource "azurerm_iotcentral_organization" "test" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-id" - display_name = "Org child" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-id" + display_name = "Org child" parent_organization_id = azurerm_iotcentral_organization.test_parent_2.organization_id } @@ -243,14 +254,14 @@ provider "azurerm" { } %s resource "azurerm_iotcentral_organization" "test_parent" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-parent-id" - display_name = "Org parent" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-parent-id" + display_name = "Org parent" } resource "azurerm_iotcentral_organization" "test" { - sub_domain = azurerm_iotcentral_application.test.sub_domain - organization_id = "org-test-id" - display_name = "Org child" + iotcentral_application_id = azurerm_iotcentral_application.test.id + organization_id = "org-test-id" + display_name = "Org child" } `, r.template(data)) } diff --git a/internal/services/iotcentral/parse/organization.go b/internal/services/iotcentral/parse/organization.go index d4ae10c9e4bb5..bdf8290386b16 100644 --- a/internal/services/iotcentral/parse/organization.go +++ b/internal/services/iotcentral/parse/organization.go @@ -1,90 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package parse +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + import ( "fmt" - "net/url" "strings" "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" ) -var _ resourceids.Id = OrganizationId{} - type OrganizationId struct { - DomainSuffix string - SubDomain string - OrganizationId string -} - -func NewOrganizationID(subDomain string, domainSuffix string, id string) (*OrganizationId, error) { - return &OrganizationId{ - DomainSuffix: domainSuffix, - SubDomain: subDomain, - OrganizationId: id, - }, nil + SubscriptionId string + ResourceGroup string + IotAppName string + Name string } -func (id OrganizationId) ID() string { - return fmt.Sprintf("https://%s.%s/api/organizations/%s", id.SubDomain, id.DomainSuffix, id.OrganizationId) +func NewOrganizationID(subscriptionId, resourceGroup, iotAppName, name string) OrganizationId { + return OrganizationId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + IotAppName: iotAppName, + Name: name, + } } func (id OrganizationId) String() string { - components := []string{ - fmt.Sprintf("DomainSuffix: %q", id.DomainSuffix), - fmt.Sprintf("SubDomain: %q", id.SubDomain), - fmt.Sprintf("Path: %q", "/api/organizations/"), - fmt.Sprintf("OrganizationId: %q", id.OrganizationId), + segments := []string{ + fmt.Sprintf("Name %q", id.Name), + fmt.Sprintf("Iot App Name %q", id.IotAppName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), } - return fmt.Sprintf("Iot Central OrganizationId %s", strings.Join(components, "\n")) + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Organization", segmentsStr) +} + +func (id OrganizationId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.IoTCentral/iotApps/%s/organizations/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.IotAppName, id.Name) } -func ParseOrganizationID(id string, subDomain string, domainSuffix string) (*OrganizationId, error) { - idURL, err := url.ParseRequestURI(id) +// OrganizationID parses a Organization ID into an OrganizationId struct +func OrganizationID(input string) (*OrganizationId, error) { + id, err := resourceids.ParseAzureResourceID(input) if err != nil { - return nil, fmt.Errorf("cannot parse azure iot central organization ID: %s", err) + return nil, fmt.Errorf("parsing %q as an Organization ID: %+v", input, err) } - path := idURL.Path - - path = strings.TrimPrefix(path, "/") - path = strings.TrimSuffix(path, "/") - - components := strings.Split(path, "/") - - if len(components) != 3 { - return nil, fmt.Errorf("iot central organization should have 3 segments, found %d segment(s) in %q", len(components), id) + resourceId := OrganizationId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, } - apiString := components[0] - if apiString != "api" { - return nil, fmt.Errorf("iot central organization should have api as first segment, found %q", apiString) + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") } - organizationsString := components[1] - if organizationsString != "organizations" { - return nil, fmt.Errorf("iot central organization should have organizations as second segment, found %q", organizationsString) + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") } - parsedOrganizationId := components[2] - - parsedSubDomain := strings.Split(idURL.Host, ".")[0] - parsedDomainSuffix := strings.Split(idURL.Host, ".")[1] - - if subDomain != "" { // subDomain is empty when importing - if parsedSubDomain != subDomain { - return nil, fmt.Errorf("iot central organization subdomain should be %q, got %q", subDomain, parsedSubDomain) - } + if resourceId.IotAppName, err = id.PopSegment("iotApps"); err != nil { + return nil, err } - - if parsedDomainSuffix != domainSuffix { - return nil, fmt.Errorf("iot central organization domain suffix should be %q, got %q", domainSuffix, parsedDomainSuffix) + if resourceId.Name, err = id.PopSegment("organizations"); err != nil { + return nil, err } - organizationId := OrganizationId{ - DomainSuffix: parsedDomainSuffix, - SubDomain: parsedSubDomain, - OrganizationId: parsedOrganizationId, + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err } - return &organizationId, nil + return &resourceId, nil } diff --git a/internal/services/iotcentral/parse/organization_test.go b/internal/services/iotcentral/parse/organization_test.go index 43b86ff01884e..4d1e9d36ad4b1 100644 --- a/internal/services/iotcentral/parse/organization_test.go +++ b/internal/services/iotcentral/parse/organization_test.go @@ -1,100 +1,131 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package parse -import "testing" +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten -func TestNewOrganizationID(t *testing.T) { - id, err := NewOrganizationID("subdomain", "domainSuffix", "organizationId") - if err != nil { - t.Fatalf("Got error for New Organization ID: %+v", err) - } - actual := id.ID() - expected := "https://subdomain.domainSuffix/api/organizations/organizationId" +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = OrganizationId{} + +func TestOrganizationIDFormatter(t *testing.T) { + actual := NewOrganizationID("12345678-1234-9876-4563-123456789012", "resGroup1", "application1", "organization1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/application1/organizations/organization1" if actual != expected { t.Fatalf("Expected %q but got %q", expected, actual) } } -func TestParseNestedItemID(t *testing.T) { - cases := []struct { - Input string - Expected OrganizationId - ExpectError bool +func TestOrganizationID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *OrganizationId }{ + { - Input: "", - ExpectError: true, + // empty + Input: "", + Error: true, }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + { - Input: "https", - ExpectError: true, + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, }, + { - Input: "https://", - ExpectError: true, + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, }, + { - Input: "https://subdomain.domainSuffix", - ExpectError: true, + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, }, + { - Input: "https://subdomain.domainSuffix/", - ExpectError: true, + // missing IotAppName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/", + Error: true, }, + { - Input: "https://subdomain.domainSuffix/api", - ExpectError: true, + // missing value for IotAppName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/", + Error: true, }, + { - Input: "https://subdomain.domainSuffix/api/organizations", - ExpectError: true, + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/application1/", + Error: true, }, + { - Input: "https://subdomain.domainSuffix/api/domains/fdf067c93bbb4b22bff4d8b7a9a56217", - ExpectError: true, + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/application1/organizations/", + Error: true, }, + { - Input: "https://subdomain.domainSuffix/api/organizations/fdf067c93bbb4b22bff4d8b7a9a56217", - ExpectError: false, - Expected: OrganizationId{ - OrganizationId: "fdf067c93bbb4b22bff4d8b7a9a56217", - DomainSuffix: "domainSuffix", - SubDomain: "subdomain", + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/application1/organizations/organization1", + Expected: &OrganizationId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + IotAppName: "application1", + Name: "organization1", }, }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.IOTCENTRAL/IOTAPPS/APPLICATION1/ORGANIZATIONS/ORGANIZATION1", + Error: true, + }, } - for _, tc := range cases { - t.Logf("[DEBUG] Testing %q", tc.Input) + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) - orgId, err := ParseOrganizationID(tc.Input, "subdomain", "domainSuffix") + actual, err := OrganizationID(v.Input) if err != nil { - if tc.ExpectError { - t.Logf("[DEBUG] --> [Received Expected Error]: %+v", err) + if v.Error { continue } - t.Fatalf("Got error for ID '%s': %+v", tc.Input, err) + t.Fatalf("Expect a value but got an error: %s", err) } - - if orgId == nil { - t.Fatalf("Expected a SecretID to be parsed for ID '%s', got nil.", tc.Input) + if v.Error { + t.Fatal("Expect an error but didn't get one") } - if tc.Expected.SubDomain != orgId.SubDomain { - t.Fatalf("Expected 'SubDomain' to be '%s', got '%s' for ID '%s'", tc.Expected.SubDomain, orgId.SubDomain, tc.Input) + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) } - - if tc.Expected.DomainSuffix != orgId.DomainSuffix { - t.Fatalf("Expected 'DomainSuffix' to be '%s', got '%s' for ID '%s'", tc.Expected.DomainSuffix, orgId.DomainSuffix, tc.Input) + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) } - - if tc.Expected.OrganizationId != orgId.OrganizationId { - t.Fatalf("Expected 'OrganizationId' to be '%s', got '%s' for ID '%s'", tc.Expected.OrganizationId, orgId.OrganizationId, tc.Input) + if actual.IotAppName != v.Expected.IotAppName { + t.Fatalf("Expected %q but got %q for IotAppName", v.Expected.IotAppName, actual.IotAppName) } - - if tc.Input != orgId.ID() { - t.Fatalf("Expected 'ID()' to be '%s', got '%s'", tc.Input, orgId.ID()) + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) } - t.Logf("[DEBUG] --> [Valid Value]: %+v", tc.Input) } } diff --git a/internal/services/iotcentral/resourceids.go b/internal/services/iotcentral/resourceids.go new file mode 100644 index 0000000000000..da20cc45366b2 --- /dev/null +++ b/internal/services/iotcentral/resourceids.go @@ -0,0 +1,3 @@ +package iotcentral + +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Organization -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/application1/organizations/organization1 diff --git a/internal/services/iotcentral/validate/organization.go b/internal/services/iotcentral/validate/organization.go deleted file mode 100644 index c883800c22a6c..0000000000000 --- a/internal/services/iotcentral/validate/organization.go +++ /dev/null @@ -1,106 +0,0 @@ -package validate - -import ( - "fmt" - "net/url" - "regexp" - "strings" - - "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" -) - -func ID(i interface{}, k string) (warnings []string, errors []error) { - if warnings, errors = validation.StringIsNotEmpty(i, k); len(errors) > 0 { - return warnings, errors - } - - id, ok := i.(string) - if !ok { - errors = append(errors, fmt.Errorf("expected %s to be a string", k)) - return warnings, errors - } - - idURL, err := url.ParseRequestURI(id) - if err != nil { - errors = append(errors, fmt.Errorf("cannot parse azure iot central organization ID as URI: %s", err)) - return warnings, errors - } - - path := idURL.Path - - path = strings.TrimPrefix(path, "/") - path = strings.TrimSuffix(path, "/") - - components := strings.Split(path, "/") - - if len(components) != 3 { - errors = append(errors, fmt.Errorf("iot central organization should have 3 segments, found %d segment(s) in %q", len(components), id)) - return warnings, errors - } - - apiString := components[0] - if apiString != "api" { - errors = append(errors, fmt.Errorf("iot central organization should have api as first segment, found %q", apiString)) - return warnings, errors - } - - organizationsString := components[1] - if organizationsString != "organizations" { - errors = append(errors, fmt.Errorf("iot central organization should have organizations as second segment, found %q", organizationsString)) - return warnings, errors - } - - organizationIdString := components[2] - err = validateOrganizationId(organizationIdString) - if err != nil { - errors = append(errors, err) - return warnings, errors - } - - return warnings, errors -} - -func OrganizationID(i interface{}, k string) (warnings []string, errors []error) { - id, ok := i.(string) - if !ok { - errors = append(errors, fmt.Errorf("expected %s to be a string", k)) - return warnings, errors - } - - err := validateOrganizationId(id) - if err != nil { - errors = append(errors, err) - return warnings, errors - } - - return warnings, errors -} - -func validateOrganizationId(id string) error { - // Ensure the string follows the desired format. - // Regex pattern: ^(?!-)[a-z0-9-]{1,48}[a-z0-9]$ - // The negative lookahead (?!-) is not supported in Go's standard regexp package - formatPattern := `^[a-z0-9-]{1,48}[a-z0-9]$` - formatRegex, err := regexp.Compile(formatPattern) - if err != nil { - return fmt.Errorf("error compiling format regex: %s error: %+v", formatPattern, err) - } - - if !formatRegex.MatchString(id) { - return fmt.Errorf("iot central organizationId %q is invalid, regex pattern: ^(?!-)[a-z0-9-]{1,48}[a-z0-9]$", id) - } - - // Ensure the string does not start with a hyphen. - // Solves for (?!-) - startHyphenPattern := `^-` - startHyphenRegex, err := regexp.Compile(startHyphenPattern) - if err != nil { - return fmt.Errorf("error compiling start hyphen regex: %s error: %+v", startHyphenPattern, err) - } - - if startHyphenRegex.MatchString(id) { - return fmt.Errorf("iot central organizationId %q is invalid, regex pattern: ^(?!-)[a-z0-9-]{1,48}[a-z0-9]$", id) - } - - return nil -} diff --git a/internal/services/iotcentral/validate/organization_id.go b/internal/services/iotcentral/validate/organization_id.go new file mode 100644 index 0000000000000..bd1f23e0a8a70 --- /dev/null +++ b/internal/services/iotcentral/validate/organization_id.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/iotcentral/parse" +) + +func OrganizationID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.OrganizationID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/iotcentral/validate/organization_id_test.go b/internal/services/iotcentral/validate/organization_id_test.go new file mode 100644 index 0000000000000..8dcdfb9e65f18 --- /dev/null +++ b/internal/services/iotcentral/validate/organization_id_test.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestOrganizationID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing IotAppName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/", + Valid: false, + }, + + { + // missing value for IotAppName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/", + Valid: false, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/application1/", + Valid: false, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/application1/organizations/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.IoTCentral/iotApps/application1/organizations/organization1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.IOTCENTRAL/IOTAPPS/APPLICATION1/ORGANIZATIONS/ORGANIZATION1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := OrganizationID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/internal/services/iotcentral/validate/organization_organization_id.go b/internal/services/iotcentral/validate/organization_organization_id.go new file mode 100644 index 0000000000000..d0fa5c860f1ed --- /dev/null +++ b/internal/services/iotcentral/validate/organization_organization_id.go @@ -0,0 +1,51 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func OrganizationOrganizationID(i interface{}, k string) (warnings []string, errors []error) { + id, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %s to be a string", k)) + return warnings, errors + } + + err := validateOrganizationId(id) + if err != nil { + errors = append(errors, err) + return warnings, errors + } + + return warnings, errors +} + +func validateOrganizationId(id string) error { + // Ensure the string follows the desired format. + // Regex pattern: ^(?!-)[a-z0-9-]{1,48}[a-z0-9]$ + // The negative lookahead (?!-) is not supported in Go's standard regexp package + formatPattern := `^[a-z0-9-]{1,48}[a-z0-9]$` + formatRegex, err := regexp.Compile(formatPattern) + if err != nil { + return fmt.Errorf("error compiling format regex: %s error: %+v", formatPattern, err) + } + + if !formatRegex.MatchString(id) { + return fmt.Errorf("iot central organizationId %q is invalid, regex pattern: ^(?!-)[a-z0-9-]{1,48}[a-z0-9]$", id) + } + + // Ensure the string does not start with a hyphen. + // Solves for (?!-) + startHyphenPattern := `^-` + startHyphenRegex, err := regexp.Compile(startHyphenPattern) + if err != nil { + return fmt.Errorf("error compiling start hyphen regex: %s error: %+v", startHyphenPattern, err) + } + + if startHyphenRegex.MatchString(id) { + return fmt.Errorf("iot central organizationId %q is invalid, regex pattern: ^(?!-)[a-z0-9-]{1,48}[a-z0-9]$", id) + } + + return nil +} diff --git a/internal/services/iotcentral/validate/organization_organization_id_test.go b/internal/services/iotcentral/validate/organization_organization_id_test.go new file mode 100644 index 0000000000000..aca0b9d53fa82 --- /dev/null +++ b/internal/services/iotcentral/validate/organization_organization_id_test.go @@ -0,0 +1,58 @@ +package validate + +import ( + "testing" +) + +func TestOrganizationOrganizationID(t *testing.T) { + cases := []struct { + Input string + ExpectError bool + }{ + { + Input: "-invalid-start", + ExpectError: true, + }, + { + Input: "invalid--hyphen", + ExpectError: true, + }, + { + Input: "1234567890123456789012345678901234567890123456789", + ExpectError: true, + }, + { + Input: "valid-string1", + ExpectError: false, + }, + { + Input: "validstring2", + ExpectError: false, + }, + { + Input: "v", + ExpectError: false, + }, + { + Input: "1", + ExpectError: true, + }, + } + + for _, tc := range cases { + warnings, err := OrganizationID(tc.Input, "example") + if err != nil { + if !tc.ExpectError { + t.Fatalf("Got error for input %q: %+v", tc.Input, err) + } + + return + } + + if tc.ExpectError && len(warnings) == 0 { + t.Fatalf("Got no errors for input %q but expected some", tc.Input) + } else if !tc.ExpectError && len(warnings) > 0 { + t.Fatalf("Got %d errors for input %q when didn't expect any", len(warnings), tc.Input) + } + } +} diff --git a/internal/services/iotcentral/validate/organization_test.go b/internal/services/iotcentral/validate/organization_test.go deleted file mode 100644 index 739a2209bc7d1..0000000000000 --- a/internal/services/iotcentral/validate/organization_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package validate - -import ( - "testing" -) - -func TestID(t *testing.T) { - cases := []struct { - Input string - ExpectError bool - }{ - { - Input: "", - ExpectError: true, - }, - { - Input: "https://subdomain.baseDomain/api", - ExpectError: true, - }, - { - Input: "https://subdomain.baseDomain/api/organizations", - ExpectError: true, - }, - { - Input: "https://subdomain.baseDomain/api/organizations/fdf067c93bbb4b22bff4d8b7a9a56217", - ExpectError: false, - }, - { - Input: "https://my-keyvault.vault.azure.net/certificates/hello/world", - ExpectError: false, - }, - { - Input: "https://my-keyvault.vault.azure.net/keys/castle/1492", - ExpectError: false, - }, - { - Input: "https://subdomain.baseDomain/api/organizations/fdf067c93bbb4b22bff4d8b7a9a56217/XXX", - ExpectError: true, - }, - } - - for _, tc := range cases { - warnings, err := ID(tc.Input, "example") - if err != nil { - if !tc.ExpectError { - t.Fatalf("Got error for input %q: %+v", tc.Input, err) - } - - return - } - - if tc.ExpectError && len(warnings) == 0 { - t.Fatalf("Got no errors for input %q but expected some", tc.Input) - } else if !tc.ExpectError && len(warnings) > 0 { - t.Fatalf("Got %d errors for input %q when didn't expect any", len(warnings), tc.Input) - } - } -} - -func TestOrganizationID(t *testing.T) { - cases := []struct { - Input string - ExpectError bool - }{ - { - Input: "-invalid-start", - ExpectError: true, - }, - { - Input: "invalid--hyphen", - ExpectError: true, - }, - { - Input: "1234567890123456789012345678901234567890123456789", - ExpectError: true, - }, - { - Input: "valid-string1", - ExpectError: false, - }, - { - Input: "validstring2", - ExpectError: false, - }, - { - Input: "v", - ExpectError: false, - }, - { - Input: "1", - ExpectError: true, - }, - } - - for _, tc := range cases { - warnings, err := OrganizationID(tc.Input, "example") - if err != nil { - if !tc.ExpectError { - t.Fatalf("Got error for input %q: %+v", tc.Input, err) - } - - return - } - - if tc.ExpectError && len(warnings) == 0 { - t.Fatalf("Got no errors for input %q but expected some", tc.Input) - } else if !tc.ExpectError && len(warnings) > 0 { - t.Fatalf("Got %d errors for input %q when didn't expect any", len(warnings), tc.Input) - } - } -} diff --git a/website/docs/r/iotcentral_organization.html.markdown b/website/docs/r/iotcentral_organization.html.markdown index 326743bc75677..e83b2595dd15b 100644 --- a/website/docs/r/iotcentral_organization.html.markdown +++ b/website/docs/r/iotcentral_organization.html.markdown @@ -32,16 +32,16 @@ resource "azurerm_iotcentral_application" "example" { } resource "azurerm_iotcentral_organization" "example_parent" { - sub_domain = azurerm_iotcentral_application.example.sub_domain - organization_id = "example-parent-organization-id" - display_name = "Org example parent" + iotcentral_application_id = azurerm_iotcentral_application.example.id + organization_id = "example-parent-organization-id" + display_name = "Org example parent" } resource "azurerm_iotcentral_organization" "example" { - sub_domain = azurerm_iotcentral_application.example.sub_domain - organization_id = "example-child-organization-id" - display_name = "Org example" - parent = azurerm_iotcentral_organization.example_parent.organization_id + iotcentral_application_id = azurerm_iotcentral_application.example.id + organization_id = "example-child-organization-id" + display_name = "Org example" + parent_organization_id = azurerm_iotcentral_organization.example_parent.organization_id } ``` @@ -49,19 +49,19 @@ resource "azurerm_iotcentral_organization" "example" { The following arguments are supported: -* `sub_domain` - (Required) The application `sub_domain`. Changing this forces a new resource to be created. +* `iotcentral_application_id` - (Required) The application `id`. Changing this forces a new resource to be created. * `organization_id` - The ID of the organization. Changing this forces a new resource to be created. * `display_name` - (Required) Custom `display_name` for the organization. -* `parent` - (Optional) The `organization_id` of the parent organization. +* `parent_organization_id` - (Optional) The `organization_id` of the parent organization. Changing this forces a new resource to be created. ## Attributes Reference In addition to the Arguments listed above - the following Attributes are exported: -* `id` - The ID reference of the organization, formated as `{subdomain}.{baseDomain}/api/organizations/{organizationId}`. +* `id` - The ID reference of the organization, formated as `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.IoTCentral/iotApps/{application}/organizations/{organizationId}`. ## Timeouts @@ -77,5 +77,5 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/l The IoT Central Organization can be imported using the `id`, e.g. ```shell -terraform import azurerm_iotcentral_organization.example https://example.azureiotcentral.com/api/organizations/abc123 +terraform import azurerm_iotcentral_organization.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.IoTCentral/iotApps/example/organizations/example ```