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
}