internal/services/parse/resource.go (223 lines of code) (raw):

package parse import ( "fmt" "log" "net/url" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/terraform-provider-azapi/internal/azure" "github.com/Azure/terraform-provider-azapi/internal/azure/types" "github.com/Azure/terraform-provider-azapi/utils" ) type ResourceId struct { AzureResourceId string ApiVersion string AzureResourceType string Name string ParentId string ResourceDef *types.ResourceType } // NewResourceIDWithNestedResourceNames constructs a nested resource ID from the given resource names, parent ID, and resource type. func NewResourceIDWithNestedResourceNames(resourceNames []string, parentId, resourceType string) (ResourceId, error) { if len(resourceNames) == 0 { return ResourceId{}, fmt.Errorf("resource names cannot be empty") } // Append default "@latest" api version if not provided resourceType = utils.TryAppendDefaultApiVersion(resourceType) azureResourceType, apiVersion, err := utils.GetAzureResourceTypeApiVersion(resourceType) if err != nil { return ResourceId{}, err } resourceProvider, resourceTypeParts, err := utils.GetAzureResourceTypeParts(azureResourceType) if err != nil { return ResourceId{}, err } // Ensure the number of resource names matches the number of resource type parts if len(resourceNames) != len(resourceTypeParts) { return ResourceId{}, fmt.Errorf("number of resource names does not match the number of resource type parts, expected %d, got %d", len(resourceTypeParts), len(resourceNames)) } currentResourceType := resourceProvider // Build resource ID for each nested resource for i, resourceTypePart := range resourceTypeParts { // Final resource type part if i == len(resourceTypeParts)-1 { return NewResourceID(resourceNames[i], parentId, resourceType) } // Intermediate resource type part currentResourceType += "/" + resourceTypePart parentResourceID, err := NewResourceIDSkipScopeValidation(resourceNames[i], parentId, utils.GetAzureResourceType(currentResourceType, apiVersion)) if err != nil { return ResourceId{}, err } parentId = parentResourceID.AzureResourceId } return ResourceId{}, fmt.Errorf("failed to build resource id for nested resources") } func NewResourceID(name, parentId, resourceType string) (ResourceId, error) { return newResourceID(name, parentId, resourceType, false) } func NewResourceIDSkipScopeValidation(name, parentId, resourceType string) (ResourceId, error) { return newResourceID(name, parentId, resourceType, true) } func newResourceID(name, parentId, resourceType string, skipScopeValidation bool) (ResourceId, error) { azureResourceType, apiVersion, err := utils.GetAzureResourceTypeApiVersion(resourceType) if err != nil { return ResourceId{}, err } resourceDef, err := azure.GetResourceDefinition(azureResourceType, apiVersion) if err != nil { log.Printf("[WARN] load embedded schema: %+v\n", err) } azureResourceId := "" switch { case !strings.Contains(azureResourceType, "/"): // case 0: resource type is a provider type // avoid duplicated `/` if parent_id is tenant scope scopeId := parentId if parentId == "/" { scopeId = "" } azureResourceId = fmt.Sprintf("%s/providers/%s", scopeId, name) case utils.IsTopLevelResourceType(azureResourceType): // case 1: top level resource, verify parent_id providers correct scope if !skipScopeValidation { if err = validateParentIdScope(resourceDef, parentId); err != nil { return ResourceId{}, fmt.Errorf("`parent_id is invalid`: %+v", err) } } // build azure resource id switch azureResourceType { case arm.ResourceGroupResourceType.String(): azureResourceId = fmt.Sprintf("%s/resourceGroups/%s", parentId, name) case arm.SubscriptionResourceType.String(): azureResourceId = fmt.Sprintf("/subscriptions/%s", name) case arm.TenantResourceType.String(): azureResourceId = "/" case arm.ProviderResourceType.String(): // avoid duplicated `/` if parent_id is tenant scope scopeId := parentId if parentId == "/" { scopeId = "" } azureResourceId = fmt.Sprintf("%s/providers/%s", scopeId, name) default: // avoid duplicated `/` if parent_id is tenant scope scopeId := parentId if parentId == "/" { scopeId = "" } azureResourceId = fmt.Sprintf("%s/providers/%s/%s", scopeId, azureResourceType, name) } default: // case 2: child resource, verify parent_id's type matches with resource type's parent type if err = validateParentIdType(azureResourceType, parentId); err != nil { return ResourceId{}, fmt.Errorf("`parent_id is invalid`: %+v", err) } // build azure resource id lastType := azureResourceType[strings.LastIndex(azureResourceType, "/")+1:] azureResourceId = fmt.Sprintf("%s/%s/%s", parentId, lastType, name) } return ResourceId{ AzureResourceId: azureResourceId, ApiVersion: apiVersion, AzureResourceType: azureResourceType, Name: name, ParentId: parentId, ResourceDef: resourceDef, }, nil } // ResourceIDWithResourceType parses a Resource ID and resource type into an ResourceId struct func ResourceIDWithResourceType(azureResourceId, resourceType string) (ResourceId, error) { azureResourceType, _, err := utils.GetAzureResourceTypeApiVersion(resourceType) if err != nil { return ResourceId{}, err } resourceTypeFromId := utils.GetResourceType(azureResourceId) // if resource type is a provider type, then `type` should be either `Microsoft.Foo` or `Microsoft.Resources/providers` if strings.EqualFold(arm.ProviderResourceType.String(), resourceTypeFromId) { if strings.Contains(azureResourceType, "/") && !strings.EqualFold(azureResourceType, arm.ProviderResourceType.String()) { return ResourceId{}, fmt.Errorf("`resource_id` and `type` are not matched, expect `type` to be a provider type, but got %s", azureResourceType) } } else { if !strings.EqualFold(azureResourceType, resourceTypeFromId) { return ResourceId{}, fmt.Errorf("`resource_id` and `type` are not matched, expect `type` to be %s, but got %s", resourceTypeFromId, azureResourceType) } } name := utils.GetName(azureResourceId) parentId := utils.GetParentId(azureResourceId) id, err := NewResourceID(name, parentId, resourceType) if err != nil { return ResourceId{}, err } // The generated resource id is based on the resource type whose case might be different from the input resource id. // So we set the generated resource id to the input value. id.AzureResourceId = azureResourceId return id, nil } // ResourceIDContainsApiVersion parses a Resource ID which contains api-version into an ResourceId struct func ResourceIDContainsApiVersion(input string) (ResourceId, error) { idUrl, err := url.Parse(input) if err != nil { return ResourceId{}, err } azureResourceId := idUrl.Path apiVersion := idUrl.Query().Get("api-version") if azureResourceId == "" { return ResourceId{}, fmt.Errorf("ID was missing the 'azure resource id' element") } if apiVersion == "" { return ResourceId{}, fmt.Errorf("ID was missing the 'api-version' element") } azureResourceType := utils.GetResourceType(azureResourceId) id, err := ResourceIDWithResourceType(azureResourceId, fmt.Sprintf("%s@%s", azureResourceType, apiVersion)) if err != nil { return ResourceId{}, err } return id, nil } // ResourceID parses a Resource ID which might not contain api-version into an ResourceId struct, it will append the latest api-version if not provided func ResourceID(input string) (ResourceId, error) { idUrl, err := url.Parse(input) if err != nil { return ResourceId{}, err } apiVersion := idUrl.Query().Get("api-version") if apiVersion == "" { resourceType := utils.GetResourceType(input) apiVersions := azure.GetApiVersions(resourceType) if len(apiVersions) != 0 { input = fmt.Sprintf("%s?api-version=%s", input, apiVersions[len(apiVersions)-1]) } else { return ResourceId{}, fmt.Errorf("ID was missing the `api-version` element") } } return ResourceIDContainsApiVersion(input) } func (id ResourceId) String() string { segments := []string{ fmt.Sprintf("ResourceId %q", id.AzureResourceId), fmt.Sprintf("Api Version %q", id.ApiVersion), } segmentsStr := strings.Join(segments, " / ") return fmt.Sprintf("%s: (%s)", "Resource", segmentsStr) } func (id ResourceId) ID() string { return id.AzureResourceId } func validateParentIdScope(resourceDef *types.ResourceType, parentId string) error { if resourceDef != nil { scopeTypes := make([]types.ScopeType, 0) for _, scope := range resourceDef.ScopeTypes { if scope != types.Unknown { scopeTypes = append(scopeTypes, scope) } } parentIdScope := utils.GetScopeType(parentId) // known scope, use `type` to verify `parent_id` if len(scopeTypes) != 0 { // check parent_id's scope matchedScope := types.Unknown for _, scope := range scopeTypes { switch scope { case types.Tenant, types.ManagementGroup, types.Subscription, types.ResourceGroup: if parentIdScope == scope { matchedScope = scope } case types.Extension: // skip checking the parent Id's scope because extension resource could be applied to any scope matchedScope = scope case types.Unknown: } } if matchedScope == types.Unknown { return fmt.Errorf("expect ID of resource whose scope is %v, but got scope %v", scopeTypes, parentIdScope) } } } return nil } func validateParentIdType(azureResourceType string, parentId string) error { parentIdExpectedType := utils.GetParentType(azureResourceType) parentIdType := utils.GetResourceType(parentId) if !strings.EqualFold(parentIdExpectedType, parentIdType) { return fmt.Errorf("expect ID of `%s`", parentIdExpectedType) } return nil }