internal/terraform/json.go (146 lines of code) (raw):

// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package terraform provides definitions of Terraform resources in JSON format. // This intentially does not define all fields in the plan JSON. // https://www.terraform.io/docs/internals/json-format.html#plan-representation package terraform import ( "encoding/json" "strings" ) // https://www.terraform.io/docs/internals/json-format.html#plan-representation type plan struct { Variables map[string]variable `json:"variables"` PlannedValues values `json:"planned_values"` ResourceChanges []ResourceChange `json:"resource_changes"` Configuration Configuration `json:"configuration"` } type variable struct { Value interface{} `json:"value"` } // https://www.terraform.io/docs/internals/json-format.html#values-representation type values struct { RootModules struct { Resources []Resource `json:"resources"` ChildModules []childModule `json:"child_modules"` } `json:"root_module"` } type childModule struct { Address string `json:"address"` Resources []Resource `json:"resources"` ChildModules []childModule `json:"child_modules"` } // Resource represent single Terraform resource definition. type Resource struct { Name string `json:"name"` Address string `json:"address"` Kind string `json:"type"` Mode string `json:"mode"` // "managed" for resources, or "data" for data resources Values map[string]interface{} `json:"values"` } // ResourceChange represents a Terraform resource change from a Terraform plan. // See "resource_changes" at https://www.terraform.io/docs/internals/json-format.html#plan-representation type ResourceChange struct { Address string `json:"address"` ModuleAddress string `json:"module_address"` Mode string `json:"mode"` // "managed" for resources, or "data" for data resources Kind string `json:"type"` Name string `json:"name"` Change Change `json:"change"` } // Change represents the "Change" element of a Terraform resource change from a Terraform plan. // https://www.terraform.io/docs/internals/json-format.html#change-representation type Change struct { Actions []string `json:"actions"` // These are "value-representation", not "values-representation" and the keys are resource-specific. Before map[string]interface{} `json:"before"` After map[string]interface{} `json:"after"` AfterUnknown map[string]interface{} `json:"after_unknown"` // Undocumented :( See https://github.com/terraform-providers/terraform-provider-aws/issues/11823 } // Configuration represents part of the configuration block of a plan. // https://www.terraform.io/docs/internals/json-format.html#configuration-representation type Configuration struct { ProviderConfig map[string]ProviderConfig `json:"provider_config"` RootModule struct { // Note: This is not the same schema as the planned value resource above. Resources []struct { Address string `json:"address"` Kind string `json:"type"` Name string `json:"name"` ProviderConfigKey string `json:"provider_config_key"` Expressions expressions `json:"expressions"` } `json:"resources"` } `json:"root_module"` } // ProviderConfig represents a single provider configuration from the Configuration block of a Terraform plan. type ProviderConfig struct { Name string `json:"name"` VersionConstraint string `json:"version_constraint,omitempty"` Alias string `json:"alias,omitempty"` Expressions expressions `json:"expressions"` } type expressions map[string]interface{} // ReadPlanChanges unmarshals b into a jsonPlan and returns the array of ResourceChange from it. // If actions is not "", will only return changes where one of the specified actions will be taken. func ReadPlanChanges(data []byte, actions []string) ([]ResourceChange, error) { p := new(plan) if err := json.Unmarshal(data, p); err != nil { return nil, err } var result []ResourceChange for _, rc := range p.ResourceChanges { if len(actions) == 0 || slicesEqual(rc.Change.Actions, actions) { result = append(result, rc) } } return result, nil } func slicesEqual(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // ReadProviderConfigValues returns the values from the expressions block from the provider config for the resource with the given kind and name. // Variable references are resolved, as are constant_value. func ReadProviderConfigValues(data []byte, kind, name string) (map[string]interface{}, error) { p := new(plan) if err := json.Unmarshal(data, p); err != nil { return nil, err } result := make(map[string]interface{}) // Resolve expressions in the provider config. config, ok := resourceProviderConfig(kind, name, p) if !ok { return result, nil } for k, v := range config.Expressions { // Within a provider config, we expect expressions to be maps, but that's not guaranteed, so don't type assert. switch mv := v.(type) { case map[string]interface{}: result[k] = resolveExpression(mv, p) } } return result, nil } func resourceProviderConfig(kind string, name string, plan *plan) (pc ProviderConfig, ok bool) { // Find the provider_config_key for this resource, if it exists. for _, r := range plan.Configuration.RootModule.Resources { if r.Kind == kind && r.Name == name { // Find the right provider config based on the provider_config_key if pc, ok := plan.Configuration.ProviderConfig[r.ProviderConfigKey]; ok { return pc, true } } } return ProviderConfig{}, false } func resolveExpression(expr map[string]interface{}, plan *plan) interface{} { if expr == nil { return nil } if cv, ok := expr["constant_value"]; ok { return cv } if refs, ok := expr["references"]; ok { switch stringRefs := refs.(type) { case []interface{}: for _, ref := range stringRefs { if resolved := resolveReference(ref.(string), plan); resolved != nil { // Take the first one. Not sure if this is 100% correct. return resolved } } } } // Can't resolve, return expression value as-is. return expr } // resolveReference resolves expressions within "references" blocks of a Terraform plan to their specific values. // At the moment, it only handles "var.XXX", for references to variables. // The docs say not to parse the strings, but I don't see anywhere in the plan where these are directly used as keys, so I'm parsing them as strings. // https://www.terraform.io/docs/configuration/expressions.html#references-to-named-values func resolveReference(expr string, plan *plan) interface{} { exprParts := strings.Split(expr, ".") if exprParts[0] == "var" { // Variable. varName := exprParts[1] if v, ok := plan.Variables[varName]; ok { return v.Value } } return nil }