Skip to content

Commit

Permalink
support dynamic properties
Browse files Browse the repository at this point in the history
  • Loading branch information
ms-henglu committed May 8, 2024
1 parent f77cf4f commit c6ae9a9
Show file tree
Hide file tree
Showing 10 changed files with 903 additions and 24 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## v0.14.0
FEATURES:
- Support generating and testing with `azapi` dynamic property feature.

BUG FIXES:
- Fix the bug that the default resource name is not randomly generated.

## v0.13.0
FEATURES:
- Support `credscan` command to scan the credentials in the testing configuration files.
Expand Down
4 changes: 4 additions & 0 deletions dependency/azapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dependency
import (
"embed"
"fmt"
"os"
"path"
"strings"
"sync"
Expand Down Expand Up @@ -42,6 +43,9 @@ func LoadAzapiDependencies() ([]Dependency, error) {
}
for _, entry := range entries {
filename := path.Join(dir, entry.Name(), "main.tf")
if _, err := StaticFiles.Open(filename); os.IsNotExist(err) {
filename = path.Join(dir, entry.Name(), "basic", "main.tf")
}
data, err := StaticFiles.ReadFile(filename)
if err != nil {
return nil, err
Expand Down
23 changes: 20 additions & 3 deletions resource/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,46 @@ type Context struct {
azapiAddingMap map[string]bool
}

const DefaultProviderConfig = `terraform {
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 = "acctest0001"
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)
Expand Down
2 changes: 1 addition & 1 deletion resource/types/azapi_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (def AzapiDefinition) String() string {
BODY`, jsonBody)
} else {
expressions += fmt.Sprintf(`
body = jsonencode(%[1]s)`, hcl.MarshalIndent(bodyMap, " ", " "))
body = %[1]s`, hcl.MarshalIndent(bodyMap, " ", " "))
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions resource/types/azapi_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ func Test_AzapiDefinitionString(t *testing.T) {
type = "Microsoft.Network/virtualNetworks@2020-06-01"
parent_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test"
name = "test"
body = jsonencode({
body = {
properties = {
addressSpace = {
addressPrefixes = [
"192.0.0.0/16",
]
}
}
})
}
}
`,
},
Expand Down
2 changes: 1 addition & 1 deletion tf/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (t *Terraform) Show() (*tfjson.State, error) {
}

func (t *Terraform) Plan() (*tfjson.Plan, error) {
ok, err := t.exec.Plan(context.TODO(), tfexec.Out(planfile))
ok, err := t.exec.Plan(context.TODO(), tfexec.Out(planfile), tfexec.Refresh(true))
if err != nil {
return nil, err
}
Expand Down
101 changes: 84 additions & 17 deletions tf/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ func NewDiffReport(plan *tfjson.Plan, logs []paltypes.RequestTrace) types.DiffRe
}

for _, resourceChange := range plan.ResourceChanges {
if !strings.HasPrefix(resourceChange.Address, "azapi_") {
if resourceChange == nil || resourceChange.Change == nil || resourceChange.Change.Before == nil || resourceChange.Change.After == nil {
continue
}
if resourceChange == nil || resourceChange.Change == nil || resourceChange.Change.Before == nil || resourceChange.Change.After == nil {
if !strings.HasPrefix(resourceChange.Address, "azapi_") {
continue
}
if len(resourceChange.Change.Actions) == 1 && resourceChange.Change.Actions[0] == tfjson.ActionNoop {
Expand All @@ -83,14 +83,28 @@ func NewDiffReport(plan *tfjson.Plan, logs []paltypes.RequestTrace) types.DiffRe
logrus.Errorf("resource %s has no id", resourceChange.Address)
continue
}

change := types.Change{}

Check failure on line 87 in tf/utils.go

View workflow job for this annotation

GitHub Actions / golint

ineffectual assignment to change (ineffassign)

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: types.Change{
Before: beforeMap["body"].(string),
After: afterMap["body"].(string),
},
Change: change,
})
}

Expand Down Expand Up @@ -130,10 +144,10 @@ func NewPassReport(plan *tfjson.Plan) types.PassReport {
}

for _, resourceChange := range plan.ResourceChanges {
if !strings.HasPrefix(resourceChange.Address, "azapi_") {
if resourceChange == nil || resourceChange.Change == nil {
continue
}
if resourceChange == nil || resourceChange.Change == nil {
if !strings.HasPrefix(resourceChange.Address, "azapi_") {
continue
}
if len(resourceChange.Change.Actions) == 1 && resourceChange.Change.Actions[0] == tfjson.ActionNoop {
Expand Down Expand Up @@ -214,11 +228,13 @@ func NewCoverageReport(plan *tfjson.Plan, swaggerPath string) (coverage.Coverage
continue
}
if actions := resourceChange.Change.Actions; len(actions) == 1 && (actions[0] == tfjson.ActionNoop || actions[0] == tfjson.ActionUpdate) {
beforeMap, beforeMapOk := resourceChange.Change.Before.(map[string]interface{})
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)
Expand Down Expand Up @@ -247,7 +263,11 @@ func NewCoverageReport(plan *tfjson.Plan, swaggerPath string) (coverage.Coverage

func getBody(input map[string]interface{}) (map[string]interface{}, error) {
output := map[string]interface{}{}
if bodyRaw, ok := input["body"]; ok && bodyRaw != nil && bodyRaw.(string) != "" {
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{})
}
Expand All @@ -260,10 +280,22 @@ func getBody(input map[string]interface{}) (map[string]interface{}, error) {
output["identity"] = expandIdentity(value.([]interface{}))
}

err := json.Unmarshal([]byte(bodyRaw.(string)), &output)
if err != nil {
return output, err
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
Expand Down Expand Up @@ -300,14 +332,19 @@ func NewErrorReport(applyErr error, logs []paltypes.RequestTrace) types.ErrorRep
if applyErr == nil {
return out
}
res := strings.Split(applyErr.Error(), "Error: creating/updating")
res := make([]string, 0)

Check failure on line 335 in tf/utils.go

View workflow job for this annotation

GitHub Actions / golint

ineffectual assignment to res (ineffassign)
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 \\"(.+)\\" / Api Version \\"(.+)\\"\)`).FindAllStringSubmatch(e, -1); len(matches) == 1 {
if matches := regexp.MustCompile(`ResourceId\s+\\?"([^\\]+)\\?"\s+/\s+Api Version \\?"([^\\]+)\\?"\)`).FindAllStringSubmatch(e, -1); len(matches) == 1 {
id = matches[0][1]
apiVersion = matches[0][2]
}
Expand All @@ -332,14 +369,22 @@ func NewCleanupErrorReport(applyErr error, logs []paltypes.RequestTrace) types.E
Errors: make([]types.Error, 0),
Logs: logs,
}
res := strings.Split(applyErr.Error(), "Error: deleting")
if applyErr == nil {
return out
}
res := make([]string, 0)

Check failure on line 375 in tf/utils.go

View workflow job for this annotation

GitHub Actions / golint

ineffectual assignment to res (ineffassign)
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 \\"(.+)\\" / Api Version \\"(.+)\\"\)`).FindAllStringSubmatch(e, -1); len(matches) == 1 {
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 {
Expand Down Expand Up @@ -370,3 +415,25 @@ func NewIdAddressFromState(state *tfjson.State) map[string]string {
}
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
}
}
Loading

0 comments on commit c6ae9a9

Please sign in to comment.