tf/utils.go (391 lines of code) (raw):
package tf
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/azure/armstrong/coverage"
"github.com/azure/armstrong/types"
"github.com/azure/armstrong/utils"
tfjson "github.com/hashicorp/terraform-json"
paltypes "github.com/ms-henglu/pal/types"
"github.com/sirupsen/logrus"
)
type Action string
const (
ActionCreate Action = "create"
ActionReplace Action = "replace"
ActionUpdate Action = "update"
ActionDelete Action = "delete"
)
// Actions denotes a valid change type.
type Actions []Action
func GetChanges(plan *tfjson.Plan) []Action {
if plan == nil {
return []Action{}
}
actions := make([]Action, 0)
for _, change := range plan.ResourceChanges {
if change.Change != nil {
if len(change.Change.Actions) == 0 {
continue
}
if len(change.Change.Actions) == 1 {
switch change.Change.Actions[0] {
case tfjson.ActionCreate:
actions = append(actions, ActionCreate)
case tfjson.ActionDelete:
actions = append(actions, ActionDelete)
case tfjson.ActionUpdate:
actions = append(actions, ActionUpdate)
case tfjson.ActionNoop:
case tfjson.ActionRead:
}
} else {
actions = append(actions, ActionReplace)
}
}
}
return actions
}
func NewDiffReport(plan *tfjson.Plan, logs []paltypes.RequestTrace) types.DiffReport {
out := types.DiffReport{
Diffs: make([]types.Diff, 0),
Logs: logs,
}
if plan == nil {
return out
}
for _, resourceChange := range plan.ResourceChanges {
if resourceChange == nil || resourceChange.Change == nil || resourceChange.Change.Before == nil || resourceChange.Change.After == nil {
continue
}
if !strings.HasPrefix(resourceChange.Address, "azapi_") {
continue
}
if len(resourceChange.Change.Actions) == 1 && resourceChange.Change.Actions[0] == tfjson.ActionNoop {
continue
}
beforeMap, beforeMapOk := resourceChange.Change.Before.(map[string]interface{})
afterMap, afterMapOk := resourceChange.Change.After.(map[string]interface{})
if !beforeMapOk || !afterMapOk {
continue
}
if afterMap["id"] == nil {
logrus.Errorf("resource %s has no id", resourceChange.Address)
continue
}
var change types.Change
if _, ok := beforeMap["body"].(string); ok {
change = types.Change{
Before: beforeMap["body"].(string),
After: afterMap["body"].(string),
}
} else {
payloadBefore, _ := json.Marshal(beforeMap["body"])
payloadAfter, _ := json.Marshal(afterMap["body"])
change = types.Change{
Before: string(payloadBefore),
After: string(payloadAfter),
}
}
out.Diffs = append(out.Diffs, types.Diff{
Id: afterMap["id"].(string),
Type: afterMap["type"].(string),
Address: resourceChange.Address,
Change: change,
})
}
return out
}
func NewPassReportFromState(state *tfjson.State) types.PassReport {
out := types.PassReport{
Resources: make([]types.Resource, 0),
}
if state == nil || state.Values == nil || state.Values.RootModule == nil || state.Values.RootModule.Resources == nil {
logrus.Warnf("new pass report from state: state is nil")
return out
}
for _, res := range state.Values.RootModule.Resources {
if !strings.HasPrefix(res.Address, "azapi_") {
continue
}
resourceType := ""
if v, ok := res.AttributeValues["type"]; ok {
resourceType = v.(string)
}
out.Resources = append(out.Resources, types.Resource{
Type: resourceType,
Address: res.Address,
})
}
return out
}
func NewPassReport(plan *tfjson.Plan) types.PassReport {
out := types.PassReport{
Resources: make([]types.Resource, 0),
}
if plan == nil {
return out
}
for _, resourceChange := range plan.ResourceChanges {
if resourceChange == nil || resourceChange.Change == nil {
continue
}
if !strings.HasPrefix(resourceChange.Address, "azapi_") {
continue
}
if len(resourceChange.Change.Actions) == 1 && resourceChange.Change.Actions[0] == tfjson.ActionNoop {
beforeMap, beforeMapOk := resourceChange.Change.Before.(map[string]interface{})
if !beforeMapOk {
continue
}
out.Resources = append(out.Resources, types.Resource{
Type: beforeMap["type"].(string),
Address: resourceChange.Address,
})
}
}
return out
}
func NewCoverageReportFromState(state *tfjson.State, swaggerPath string) (coverage.CoverageReport, error) {
defer func() {
if r := recover(); r != nil {
logrus.Errorf("panic when producing coverage report from state: %+v", r)
}
}()
out := coverage.CoverageReport{
Coverages: make(map[string]*coverage.CoverageItem, 0),
}
if state == nil || state.Values == nil || state.Values.RootModule == nil || state.Values.RootModule.Resources == nil {
logrus.Warnf("new coverage report from state: state is nil")
return out, nil
}
for _, res := range state.Values.RootModule.Resources {
if res.Type != "azapi_resource" || res.Mode != tfjson.ManagedResourceMode {
continue
}
id := ""
if v, ok := res.AttributeValues["id"]; ok {
id = v.(string)
}
resourceType := ""
if v, ok := res.AttributeValues["type"]; ok {
resourceType = v.(string)
}
body, err := getBody(res.AttributeValues)
if err != nil {
return out, err
}
err = out.AddCoverageFromState(id, resourceType, body, swaggerPath)
if err != nil {
return out, err
}
}
return out, nil
}
func NewCoverageReport(plan *tfjson.Plan, swaggerPath string) (coverage.CoverageReport, error) {
defer func() {
if r := recover(); r != nil {
logrus.Errorf("panic when producing coverage report: %+v", r)
}
}()
out := coverage.CoverageReport{
Coverages: make(map[string]*coverage.CoverageItem, 0),
}
if plan == nil {
return out, nil
}
for _, resourceChange := range plan.ResourceChanges {
if resourceChange.Type != "azapi_resource" {
continue
}
if resourceChange == nil || resourceChange.Change == nil {
continue
}
if actions := resourceChange.Change.Actions; len(actions) == 1 && (actions[0] == tfjson.ActionNoop || actions[0] == tfjson.ActionUpdate) {
outMap, beforeMapOk := resourceChange.Change.Before.(map[string]interface{})
if !beforeMapOk {
continue
}
beforeMap := DeepCopy(outMap).(map[string]interface{})
id := ""
if v, ok := beforeMap["id"]; ok {
id = v.(string)
}
resourceType := ""
if v, ok := beforeMap["type"]; ok {
resourceType = v.(string)
}
body, err := getBody(beforeMap)
if err != nil {
return out, err
}
err = out.AddCoverageFromState(id, resourceType, body, swaggerPath)
if err != nil {
return out, err
}
}
}
return out, nil
}
func getBody(input map[string]interface{}) (map[string]interface{}, error) {
output := map[string]interface{}{}
bodyRaw, ok := input["body"]
if !ok || bodyRaw == nil {
return output, nil
}
if bodyStr, ok := bodyRaw.(string); ok && bodyStr != "" {
if value, ok := input["tags"]; ok && value != nil && len(value.(map[string]interface{})) > 0 {
output["tags"] = value.(map[string]interface{})
}
if value, ok := input["location"]; ok && value != nil && value.(string) != "" {
output["location"] = value.(string)
}
if value, ok := input["identity"]; ok && value != nil && len(value.([]interface{})) > 0 {
output["identity"] = expandIdentity(value.([]interface{}))
}
err := json.Unmarshal([]byte(bodyStr), &output)
return output, err
}
if bodyMap, ok := bodyRaw.(map[string]interface{}); ok {
if value, ok := input["tags"]; ok && value != nil && len(value.(map[string]interface{})) > 0 {
bodyMap["tags"] = value.(map[string]interface{})
}
if value, ok := input["location"]; ok && value != nil && value.(string) != "" {
bodyMap["location"] = value.(string)
}
if value, ok := input["identity"]; ok && value != nil && len(value.([]interface{})) > 0 {
bodyMap["identity"] = expandIdentity(value.([]interface{}))
}
return bodyMap, nil
}
return output, nil
}
func expandIdentity(input []interface{}) map[string]interface{} {
config := map[string]interface{}{}
if len(input) == 0 {
return config
}
v := input[0].(map[string]interface{})
if identityTypeRaw, ok := v["type"]; ok && identityTypeRaw != nil && identityTypeRaw.(string) != "" {
config["type"] = identityTypeRaw.(string)
}
if identityIdsRaw, ok := v["identity_ids"]; ok && identityIdsRaw != nil && len(identityIdsRaw.([]interface{})) > 0 {
identityIds := identityIdsRaw.([]interface{})
userAssignedIdentities := make(map[string]interface{}, len(identityIds))
for _, id := range identityIds {
userAssignedIdentities[id.(string)] = make(map[string]interface{})
}
config["userAssignedIdentities"] = userAssignedIdentities
}
return config
}
func NewErrorReport(applyErr error, logs []paltypes.RequestTrace) types.ErrorReport {
out := types.ErrorReport{
Errors: make([]types.Error, 0),
Logs: logs,
}
if applyErr == nil {
return out
}
var res []string
if strings.Contains(applyErr.Error(), "Error: Failed to create/update resource") {
res = strings.Split(applyErr.Error(), "Error: Failed to create/update resource")
} else {
res = strings.Split(applyErr.Error(), "Error: creating/updating")
}
for _, e := range res {
var id, apiVersion, label string
errorMessage := e
if lastIndex := strings.LastIndex(e, "------"); lastIndex != -1 {
errorMessage = errorMessage[0:lastIndex]
}
if matches := regexp.MustCompile(`ResourceId\s+\\?"([^\\]+)\\?"\s+/\s+Api Version \\?"([^\\]+)\\?"\)`).FindAllStringSubmatch(e, -1); len(matches) == 1 {
id = matches[0][1]
apiVersion = matches[0][2]
}
if matches := regexp.MustCompile(`resource "azapi_.+" "(.+)"`).FindAllStringSubmatch(e, -1); len(matches) != 0 {
label = matches[0][1]
}
if len(label) == 0 {
continue
}
out.Errors = append(out.Errors, types.Error{
Id: id,
Type: fmt.Sprintf("%s@%s", utils.ResourceTypeOfResourceId(id), apiVersion),
Label: label,
Message: errorMessage,
})
}
return out
}
func NewCleanupErrorReport(applyErr error, logs []paltypes.RequestTrace) types.ErrorReport {
out := types.ErrorReport{
Errors: make([]types.Error, 0),
Logs: logs,
}
if applyErr == nil {
return out
}
var res []string
if strings.Contains(applyErr.Error(), "Error: Failed to delete resource") {
res = strings.Split(applyErr.Error(), "Error: Failed to delete resource")
} else {
res = strings.Split(applyErr.Error(), "Error: deleting")
}
for _, e := range res {
var id, apiVersion string
errorMessage := e
if lastIndex := strings.LastIndex(e, "------"); lastIndex != -1 {
errorMessage = errorMessage[0:lastIndex]
}
if matches := regexp.MustCompile(`ResourceId\s+\\?"([^\\]+)\\?"\s+/\s+Api Version \\?"([^\\]+)\\?"\)`).FindAllStringSubmatch(e, -1); len(matches) == 1 {
id = matches[0][1]
apiVersion = matches[0][2]
} else {
continue
}
out.Errors = append(out.Errors, types.Error{
Id: id,
Type: fmt.Sprintf("%s@%s", utils.ResourceTypeOfResourceId(id), apiVersion),
Message: errorMessage,
})
}
return out
}
func NewIdAddressFromState(state *tfjson.State) map[string]string {
out := map[string]string{}
if state == nil || state.Values == nil || state.Values.RootModule == nil || state.Values.RootModule.Resources == nil {
logrus.Warnf("new id address mapping from state: state is nil")
return out
}
for _, res := range state.Values.RootModule.Resources {
id := ""
if v, ok := res.AttributeValues["id"]; ok {
id = v.(string)
}
out[id] = res.Address
}
return out
}
func DeepCopy(input interface{}) interface{} {
if input == nil {
return nil
}
switch v := input.(type) {
case map[string]interface{}:
out := map[string]interface{}{}
for key, value := range v {
out[key] = DeepCopy(value)
}
return out
case []interface{}:
out := make([]interface{}, len(v))
for i, value := range v {
out[i] = DeepCopy(value)
}
return out
default:
return input
}
}