internal/services/preflight/preflight.go (164 lines of code) (raw):
package preflight
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/terraform-provider-azapi/internal/azure/identity"
aztypes "github.com/Azure/terraform-provider-azapi/internal/azure/types"
"github.com/Azure/terraform-provider-azapi/internal/clients"
"github.com/Azure/terraform-provider-azapi/internal/services/dynamic"
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
"github.com/Azure/terraform-provider-azapi/utils"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
)
type RequestBodyModel struct {
Provider string `json:"provider"`
Type string `json:"type"`
Location string `json:"location"`
Scope string `json:"scope"`
Resources []map[string]interface{} `json:"resources"`
}
// ParentIdPlaceholder generates a placeholder for the parentID based on the resource definition and subscription ID
func ParentIdPlaceholder(resourceDef *aztypes.ResourceType, subscriptionId string) (string, error) {
// since the parentID is faked, there should exist only one scope type
if resourceDef == nil || len(resourceDef.ScopeTypes) != 1 {
return "", fmt.Errorf("failed to generate parentID placeholder because the resource definition is invalid")
}
scopeId := ""
switch resourceDef.ScopeTypes[0] {
case aztypes.Tenant:
scopeId = "/"
case aztypes.ManagementGroup:
scopeId = "/providers/Microsoft.Management/managementGroups/" + NamePlaceholder()
case aztypes.Subscription:
scopeId = fmt.Sprintf("/subscriptions/%s", subscriptionId)
case aztypes.ResourceGroup:
scopeId = fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", subscriptionId, NamePlaceholder())
default:
return "", fmt.Errorf("failed to generate parentID placeholder because the scope type is not supported")
}
// for top-level resources, the parent ID is the same as the scope ID
resourceType := strings.Split(resourceDef.Name, "@")[0]
if utils.IsTopLevelResourceType(resourceType) {
return scopeId, nil
}
parts := strings.Split(resourceType, "/")
parentId := fmt.Sprintf("%s/providers/%s", scopeId, parts[0])
for i := 1; i < len(parts)-1; i++ {
parentId += fmt.Sprintf("/%s/%s", parts[i], NamePlaceholder())
}
return parentId, nil
}
// NamePlaceholder generates a random name placeholder
func NamePlaceholder() string {
return acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)
}
// Validate validates the resource using the preflight API
func Validate(ctx context.Context, client *clients.ResourceClient, resourceType string, parentId string, name string, location string, body types.Dynamic, identity types.List) error {
azureResourceType, apiVersion, err := utils.GetAzureResourceTypeApiVersion(resourceType)
if err != nil {
return err
}
payload := RequestBodyModel{}
payload.Provider, payload.Type, _ = strings.Cut(azureResourceType, "/")
id, err := parse.NewResourceIDSkipScopeValidation(name, parentId, resourceType)
if err != nil {
return err
}
scopeId, err := ScopeID(id.ID())
if err != nil {
return err
}
payload.Scope = scopeId
if location != "" {
payload.Location = location
}
resource := make(map[string]interface{})
err = unmarshalPreflightBody(body, identity, &resource)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("Skipping preflight validation for resource %s because the body is invalid: %v", resourceType, err))
return nil
}
resource["name"] = name
resource["apiVersion"] = apiVersion
payload.Resources = []map[string]interface{}{resource}
_, err = client.Action(ctx, "/providers/Microsoft.Resources", "validateResources", "2020-10-01", "POST", payload, clients.DefaultRequestOptions())
return err
}
func ScopeID(resourceId string) (string, error) {
armId, err := arm.ParseResourceID(resourceId)
if err != nil {
return "", err
}
if armId.Parent == nil {
return "/", nil
}
scopeId := armId.Parent
for scopeId.Parent != nil &&
!strings.EqualFold(scopeId.ResourceType.String(), arm.SubscriptionResourceType.String()) &&
!strings.EqualFold(scopeId.ResourceType.String(), arm.ResourceGroupResourceType.String()) &&
!strings.EqualFold(scopeId.ResourceType.String(), arm.TenantResourceType.String()) &&
!strings.EqualFold(scopeId.ResourceType.String(), "Microsoft.Management/managementGroups") {
scopeId = scopeId.Parent
}
if scopeId == nil || scopeId.ResourceType.String() == arm.TenantResourceType.String() {
return "/", nil
}
return scopeId.String(), nil
}
func unmarshalPreflightBody(input types.Dynamic, identityList types.List, out *map[string]interface{}) error {
if input.IsNull() || input.IsUnknown() || input.IsUnderlyingValueUnknown() {
return fmt.Errorf("input is null or unknown")
}
const unknownPlaceholder = "[length('foo')]"
data, err := dynamic.ToJSONWithUnknownValueHandler(input, func(value attr.Value) ([]byte, error) {
return json.Marshal(unknownPlaceholder)
})
if err != nil {
return fmt.Errorf("marshaling failed: %v", err)
}
if err = json.Unmarshal(data, &out); err != nil {
return fmt.Errorf(`unmarshaling failed: value: %s, err: %+v`, string(data), err)
}
if out == nil {
out = &map[string]interface{}{}
}
// make sure that there's no unknown value outside the properties bag
for k, v := range *out {
if k == "properties" {
continue
}
if searchForValue(v, unknownPlaceholder) {
return fmt.Errorf("unknown value found outside the properties bag")
}
}
if (*out)["identity"] == nil && !identityList.IsNull() && !identityList.IsUnknown() {
identityModel := identity.FromList(identityList)
expandedIdentity, err := identity.ExpandIdentity(identityModel)
if err != nil {
return err
}
(*out)["identity"] = expandedIdentity
}
return nil
}
func searchForValue(input interface{}, target string) bool {
switch v := input.(type) {
case map[string]interface{}:
for _, value := range v {
if searchForValue(value, target) {
return true
}
}
case []interface{}:
for _, value := range v {
if searchForValue(value, target) {
return true
}
}
case string:
if v == target {
return true
}
}
return false
}