resource/context.go (446 lines of code) (raw):

package resource import ( "fmt" "math/rand" "strconv" "strings" "time" "github.com/azure/armstrong/dependency" "github.com/azure/armstrong/resource/resolver" "github.com/azure/armstrong/resource/types" "github.com/azure/armstrong/utils" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/sirupsen/logrus" "github.com/zclconf/go-cty/cty" ) type Context struct { File *hclwrite.File locationVarBlock *hclwrite.Block terraformBlock *hclwrite.Block KnownPatternMap map[string]types.Reference ReferenceResolvers []resolver.ReferenceResolver azapiAddingMap map[string]bool } var DefaultProviderConfig string func init() { DefaultProviderConfig = fmt.Sprintf(`terraform { required_providers { azapi = { source = "Azure/azapi" } } } provider "azurerm" { features { resource_group { prevent_deletion_if_contains_resources = false } key_vault { purge_soft_delete_on_destroy = false purge_soft_deleted_keys_on_destroy = false } } skip_provider_registration = true } provider "azapi" { skip_provider_registration = false } variable "resource_name" { type = string default = "acctest%04d" } variable "location" { type = string default = "westeurope" } `, rand.New(rand.NewSource(time.Now().UnixNano())).Intn(10000)) } func NewContext(referenceResolvers []resolver.ReferenceResolver) *Context { knownPatternMap := make(map[string]types.Reference) referenceResolvers = append([]resolver.ReferenceResolver{resolver.NewKnownReferenceResolver(knownPatternMap)}, referenceResolvers...) c := Context{ KnownPatternMap: knownPatternMap, ReferenceResolvers: referenceResolvers, azapiAddingMap: make(map[string]bool), } err := c.InitFile(DefaultProviderConfig) if err != nil { logrus.Errorf("failed to init context: %v", err) } return &c } var R = rand.New(rand.NewSource(time.Now().UnixNano())) func (c *Context) InitFile(content string) error { file, diags := hclwrite.ParseConfig([]byte(content), "", hcl.InitialPos) if diags.HasErrors() { logrus.Errorf("failed to parse input:\n%v", content) return diags } var locationVarBlock, terraformBlock *hclwrite.Block for _, block := range file.Body().Blocks() { switch block.Type() { case "variable": switch block.Labels()[0] { case "location": locationVarBlock = block case "resource_name": block.Body().SetAttributeValue("default", cty.StringVal(fmt.Sprintf("acctest%04d", R.Intn(10000)))) } case "terraform": terraformBlock = block } } if terraformBlock == nil { logrus.Warnf("terraform block not found in the input.") } if locationVarBlock == nil { logrus.Warnf("location variable block not found in the input.") } c.File = file c.terraformBlock = terraformBlock c.locationVarBlock = locationVarBlock return nil } func (c *Context) AddAzapiDefinition(input types.AzapiDefinition) error { logrus.Debugf("adding azapi definition: \n%v", input) if c.azapiAddingMap[input.Identifier()] { return fmt.Errorf("azapi definition already added: %v", input.Identifier()) } c.azapiAddingMap[input.Identifier()] = true defer func() { c.azapiAddingMap[input.Identifier()] = false }() def := input.DeepCopy() // find all id placeholders from def placeHolders := make([]types.PropertyDependencyMapping, 0) rootFields := []string{"parent_id", "resource_id"} for _, field := range rootFields { if value, ok := def.AdditionalFields[field]; ok { if literalValue, ok := value.(types.StringLiteralValue); ok { placeHolders = append(placeHolders, types.PropertyDependencyMapping{ ValuePath: field, LiteralValue: literalValue.Literal, }) } } } if def.Body != nil { mappings := GetKeyValueMappings(def.Body, "") for _, mapping := range mappings { if utils.IsResourceId(mapping.LiteralValue) { placeHolders = append(placeHolders, mapping) } } } logrus.Debugf("found %d id placeholders", len(placeHolders)) // find all dependencies that match the id placeholders for i, placeHolder := range placeHolders { logrus.Debugf("processing id placeholder: %s", placeHolder.LiteralValue) if utils.IsAction(placeHolder.LiteralValue) { logrus.Debugf("skip action: %s", placeHolder.LiteralValue) continue } pattern := dependency.NewPattern(placeHolder.LiteralValue) for _, resolver := range c.ReferenceResolvers { result, err := resolver.Resolve(pattern) if err != nil { return err } if result == nil { continue } logrus.Debugf("found dependency by resolver: %T", resolver) switch { case result.Reference.IsKnown(): placeHolders[i].Reference = result.Reference logrus.Debugf("dependency resolved, ref: %v", result.Reference) case result.HclToAdd != "": logrus.Debugf("found dependency:\n %v", result.HclToAdd) ref, err := c.AddHcl(result.HclToAdd, true) if err != nil { logrus.Warnf("failed to add hcl as a dependency, will continue to try other dependency resolvers: %v", err) continue } c.KnownPatternMap[pattern.String()] = *ref placeHolders[i].Reference = ref logrus.Debugf("dependency resolved, ref: %v", ref) case result.AzapiDefinitionToAdd != nil: logrus.Debugf("found dependency:\n %v", result.AzapiDefinitionToAdd) err = c.AddAzapiDefinition(*result.AzapiDefinitionToAdd) if err != nil { logrus.Warnf("failed to add azapi definition as a dependency, will continue to try other dependency resolvers: %v", err) continue } ref := c.KnownPatternMap[pattern.String()] if !ref.IsKnown() { return fmt.Errorf("resource type address not found: %v after adding azapi definition to the context, azapi def: %v", pattern, result.AzapiDefinitionToAdd) } placeHolders[i].Reference = &ref logrus.Debugf("dependency resolved, ref: %v", ref) } break } } // replace the id placeholders with the dependency address logrus.Debugf("replacing id placeholders with dependency address...") for _, filed := range rootFields { for _, placeHolder := range placeHolders { if placeHolder.ValuePath == filed && placeHolder.Reference.IsKnown() { def.AdditionalFields[filed] = types.NewReferenceValue(placeHolder.Reference.String()) break } } } if def.Body != nil { replacements := make(map[string]string) for _, placeHolder := range placeHolders { if !placeHolder.Reference.IsKnown() { continue } valuePath := placeHolder.ValuePath if placeHolder.IsKey { valuePath = fmt.Sprintf("key:%s", placeHolder.ValuePath) } replacements[valuePath] = fmt.Sprintf(`${%s}`, placeHolder.Reference) } def.Body = utils.UpdatedBody(def.Body, replacements, "") } // add extra dependencies if def.ResourceName == "azapi_resource_list" { logrus.Debugf("adding extra dependencies for azapi_resource_list...") var ref *types.Reference for pattern, r := range c.KnownPatternMap { if strings.HasSuffix(pattern, strings.ToLower(":"+def.AzureResourceType)) { ref = &r break } } if ref.IsKnown() { addr := fmt.Sprintf(`%s.%s`, ref.Name, ref.Label) if ref.Kind == "data" { addr = fmt.Sprintf(`data.%s.%s`, ref.Name, ref.Label) } def.AdditionalFields["depends_on"] = types.NewRawValue(fmt.Sprintf(`[%s]`, addr)) } else { logrus.Debugf("no `depends_on` dependency found for azapi_resource_list: %v", def) } } ref, err := c.AddHcl(def.String(), false) if err != nil { return err } if def.AdditionalFields["action"] == nil && def.ResourceName != "azapi_resource_list" { pattern := dependency.NewPattern(def.Id) c.KnownPatternMap[pattern.String()] = *ref logrus.Debugf("adding known pattern: %s, ref: %s", pattern, *ref) } return nil } func (c *Context) AddHcl(input string, skipWhenDuplicate bool) (*types.Reference, error) { logrus.Debugf("adding hcl:\n%v, skipWhenDuplicate: %v", input, skipWhenDuplicate) inputFile, diags := hclwrite.ParseConfig([]byte(input), "", hcl.InitialPos) if diags.HasErrors() { logrus.Warnf("failed to parse input:\n%v", input) return nil, diags } labelsMap := make(map[string]map[string]*hclwrite.Block) for _, block := range c.File.Body().Blocks() { if block.Type() != "data" && block.Type() != "resource" { continue } labels := block.Labels() if len(labels) != 2 { return nil, fmt.Errorf("label is invalid: %v, input:\n%v", labels, input) } key := fmt.Sprintf("%s.%s", block.Type(), labels[0]) if labelsMap[key] == nil { labelsMap[key] = make(map[string]*hclwrite.Block) } labelsMap[key][labels[1]] = block } // resource/data blocks with same resource name and labels will have conflicts, // merge them if their resource types are the same, // otherwise, rename the labels for _, block := range inputFile.Body().Blocks() { if block.Type() != "data" && block.Type() != "resource" { continue } labels := block.Labels() if len(labels) != 2 { return nil, fmt.Errorf("label is invalid: %v, input:\n%v", labels, input) } key := fmt.Sprintf("%s.%s", block.Type(), labels[0]) // no conflict conflictBlock := labelsMap[key][labels[1]] if conflictBlock == nil { continue } logrus.Debugf("found conflict: %s %v", conflictBlock.Type(), conflictBlock.Labels()) // if the resource types are the same, there is no conflict if utils.TypeValue(block) == utils.TypeValue(conflictBlock) && skipWhenDuplicate { continue } // conflict, rename the labels newLabel := labels[1] for i := 1; i < 100; i++ { newLabel = fmt.Sprintf("%s_%d", labels[1], i) if labelsMap[key][newLabel] == nil { break } } logrus.Debugf("renaming labels: %v -> %v", labels[1], newLabel) block.SetLabels([]string{labels[0], newLabel}) input = string(inputFile.BuildTokens(nil).Bytes()) // TODO: improve the following renaming labels logic oldAddressPrefix := fmt.Sprintf("%s.", strings.Join(labels, ".")) if block.Type() == "data" { oldAddressPrefix = "data." + oldAddressPrefix } newAddressPrefix := fmt.Sprintf("%s.", strings.Join(block.Labels(), ".")) if block.Type() == "data" { newAddressPrefix = "data." + newAddressPrefix } input = strings.ReplaceAll(input, oldAddressPrefix, newAddressPrefix) } inputFile, diags = hclwrite.ParseConfig([]byte(input), "", hcl.InitialPos) if diags.HasErrors() { logrus.Warnf("failed to parse input:\n%v", input) return nil, diags } // update the location and name fields for _, block := range inputFile.Body().Blocks() { if block.Type() != "data" && block.Type() != "resource" { continue } locationAttr := block.Body().GetAttribute("location") if locationAttr != nil { defaultLocation := utils.AttributeValue(c.locationVarBlock.Body().GetAttribute("default")) currentLocation := utils.AttributeValue(locationAttr) if currentLocation != defaultLocation && !strings.Contains(currentLocation, "var.") { c.locationVarBlock.Body().SetAttributeValue("default", cty.StringVal(currentLocation)) block.Body().SetAttributeTraversal("location", hcl.Traversal{hcl.TraverseRoot{Name: "var"}, hcl.TraverseAttr{Name: "location"}}) } } //TODO: replace location value in the body payload nameAttr := block.Body().GetAttribute("name") if nameAttr != nil { currentName := utils.AttributeValue(nameAttr) if isRandomName(currentName) { block.Body().SetAttributeTraversal("name", hcl.Traversal{hcl.TraverseRoot{Name: "var"}, hcl.TraverseAttr{Name: "resource_name"}}) } } } varMap := make(map[string]bool) providerMap := make(map[string]bool) for _, block := range c.File.Body().Blocks() { switch block.Type() { case "provider": providerMap[strings.Join(block.Labels(), ".")] = true case "variable": varMap[strings.Join(block.Labels(), ".")] = true } } var lastBlock *hclwrite.Block for _, block := range inputFile.Body().Blocks() { switch block.Type() { case "terraform": newProvidersBlock := block.Body().FirstMatchingBlock("required_providers", []string{}) if newProvidersBlock == nil { continue } oldProvidersBlock := c.terraformBlock.Body().FirstMatchingBlock("required_providers", []string{}) if oldProvidersBlock != nil { for attrName, attr := range newProvidersBlock.Body().Attributes() { if oldProvidersBlock.Body().GetAttribute(attrName) == nil { oldProvidersBlock.Body().SetAttributeRaw(attrName, attr.Expr().BuildTokens(nil)) } } } else { logrus.Errorf("required_providers block not found in the input.") } continue case "variable": label := strings.Join(block.Labels(), ".") if varMap[label] { continue } c.File.Body().AppendBlock(block) case "provider": label := strings.Join(block.Labels(), ".") if providerMap[label] { continue } c.File.Body().AppendBlock(block) case "output": continue case "locals": c.File.Body().AppendBlock(block) c.File.Body().AppendNewline() case "data", "resource": labels := block.Labels() if len(labels) != 2 { return nil, fmt.Errorf("label is invalid: %v, input:\n%v", labels, input) } key := fmt.Sprintf("%s.%s", block.Type(), labels[0]) conflictBlock := labelsMap[key][labels[1]] if conflictBlock != nil { lastBlock = conflictBlock continue } c.File.Body().AppendBlock(block) c.File.Body().AppendNewline() lastBlock = block default: c.File.Body().AppendBlock(block) c.File.Body().AppendNewline() } } if lastBlock != nil { labels := lastBlock.Labels() if len(labels) != 2 { return nil, fmt.Errorf("label is invalid: %v, input:\n%v", labels, input) } return &types.Reference{ Label: labels[1], Kind: lastBlock.Type(), Name: labels[0], Property: "id", }, nil } return nil, fmt.Errorf("no resource or data block found in the input, input:\n%v", input) } func (c *Context) String() string { return string(hclwrite.Format(c.File.Bytes())) } // GetKeyValueMappings returns a list of key and value of input func GetKeyValueMappings(parameters interface{}, path string) []types.PropertyDependencyMapping { if parameters == nil { return []types.PropertyDependencyMapping{} } results := make([]types.PropertyDependencyMapping, 0) switch param := parameters.(type) { case map[string]interface{}: for key, value := range param { results = append(results, GetKeyValueMappings(value, path+"."+key)...) results = append(results, types.PropertyDependencyMapping{ ValuePath: path + "." + key, LiteralValue: key, IsKey: true, }) } case []interface{}: for index, value := range param { results = append(results, GetKeyValueMappings(value, path+"."+strconv.Itoa(index))...) } case string: results = append(results, types.PropertyDependencyMapping{ ValuePath: path, LiteralValue: param, IsKey: false, }) default: } return results } func isRandomName(input string) bool { if input == "default" { return false } if input == "current" { return false } if strings.Contains(input, "Microsoft.") { return false } if strings.Contains(input, "var.") { return false } return true }