terraform-module-test-helper/breakingchange_test.go

902 строки
29 KiB
Go

package terraform_module_test_helper
import (
"fmt"
"github.com/spf13/afero"
"strconv"
"strings"
"testing"
"github.com/ahmetb/go-linq/v3"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/stretchr/testify/assert"
)
const basicOptionalVariable = `
variable "address_space" {
type = list(string)
description = "The address space that is used by the virtual network."
default = ["10.0.0.0/16"]
}
`
const basicRequiredVariable = `
variable "vnet_name" {
description = "Name of the vnet to create"
type = string
nullable = false
}
`
const unTypedVariable = `
variable "subnet_service_endpoints" {
description = "A map of subnet name to service endpoints to add to the subnet."
default = {}
}
`
const variableWithValidation = `
variable "identity_type" {
type = string
description = "this is a description."
default = "SystemAssigned"
validation {
condition = var.identity_type == "SystemAssigned" || var.identity_type == "UserAssigned" || var.identity_type == "SystemAssigned, UserAssigned"
error_message = "this is an error message."
}
}`
const basicOutput = `
output "vnet_subnets_name_id" {
description = "Can be queried subnet-id by subnet name by using lookup(module.vnet.vnet_subnets_name_id, subnet1)"
value = local.azurerm_subnets
}
`
const basicResource = `
resource "azurerm_virtual_network" "vnet" {
address_space = var.address_space
location = var.vnet_location
name = var.vnet_name
resource_group_name = var.resource_group_name
dns_servers = var.dns_servers
tags = var.tags
}
`
const basicSensitiveVariable = `
variable "db_username" {
description = "Database administrator username"
type = string
sensitive = true
}`
const jsonCode = `
{
"output": {
"vnet_subnets_name_id": [
{
"description": "Can be queried subnet-id by subnet name by using lookup(module.vnet.vnet_subnets_name_id, subnet1)",
"value": "${local.azurerm_subnets}"
}
]
},
"variable": {
"vnet_name": [
{
"description": "Name of the vnet to create",
"nullable": false,
"type": "string"
}
]
}
}
`
var basicBlocks = []string{basicRequiredVariable, basicOptionalVariable, basicSensitiveVariable, variableWithValidation, unTypedVariable, basicOutput, basicResource}
var tpl = strings.Join(basicBlocks, "\n")
func TestBreakingChange_JsonFormat(t *testing.T) {
newJsonCode := strings.ReplaceAll(jsonCode, "string", "number")
newJsonCode = strings.ReplaceAll(newJsonCode, "local.azurerm_subnets", "local.azurerm_subnet_names")
oldMoule := noError(t, func() (*Module, error) {
return loadModuleByJsonCode(jsonCode)
})
newModule := noError(t, func() (*Module, error) {
return loadModuleByJsonCode(newJsonCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldMoule, newModule)
})
assert.Equal(t, 2, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return c.Type == "update" && c.Category == "Variables" && c.Attribute != nil && *c.Attribute == "Type"
}))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return c.Type == "update" && c.Category == "Outputs" && c.Attribute != nil && *c.Attribute == "Value"
}))
}
func TestBreakingChange_NewRequiredVariableShouldBeBreakingChange(t *testing.T) {
newVariableName := "vnet_location"
newRequiredVariable := fmt.Sprintf(`
variable "%s" {
description = "The location of the vnet to create."
type = string
nullable = false
}
`, newVariableName)
newCode := fmt.Sprintf("%s\n%s", tpl, newRequiredVariable)
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.Equal(t, "create", changes[0].Type)
assert.Equal(t, newVariableName, *changes[0].Name)
assert.Equal(t, "Name", *changes[0].Attribute)
}
func TestBreakingChange_NewOptionalVariableShouldNotBeBreakingChange(t *testing.T) {
cases := []struct {
code string
name string
}{{
code: `
variable "vnet_location" {
description = "The location of the vnet to create."
type = string
nullable = false
default = "eastus"
}
`,
name: "optionalVariableWithNullableArgument",
}, {
code: `
variable "vnet_location2" {
description = "The location of the vnet to create."
type = string
default = "eastus"
}
`,
name: "optionalVariableWithoutNullableArgument",
}}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
newCode := fmt.Sprintf("%s\n%s", tpl, c.code)
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
})
}
}
func TestBreakingChange_RemoveVariableShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
newCode := strings.Join(removeBlocks(basicBlocks, basicOptionalVariable, basicRequiredVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 2, len(changes))
assert.Equal(t, "delete", changes[0].Type)
assert.Equal(t, "delete", changes[1].Type)
assert.Equal(t, "Name", *changes[0].Attribute)
assert.Equal(t, "Name", *changes[1].Attribute)
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
return *i.(Change).Name == "vnet_name"
}))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
return *i.(Change).Name == "address_space"
}))
}
func TestBreakingChange_ReorderVariablesShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
newCode := strings.Join([]string{unTypedVariable, basicOptionalVariable, basicRequiredVariable, variableWithValidation, basicOutput, basicResource, basicSensitiveVariable}, "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_RenameRequiredVariableShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
renamedVariable := `variable "renamed_name" {
description = "Name of the vnet to create"
type = string
}`
newCode := strings.Join(replaceString(basicBlocks, basicRequiredVariable, renamedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 2, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "vnet_name" && c.Type == "delete"
}))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "renamed_name" && c.Type == "create"
}))
}
func TestBreakingChange_RenameOptionalVariableShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
renamedVariable := `
variable "renamed_variable" {
type = list(string)
description = "The address space that is used by the virtual network."
default = ["10.0.0.0/16"]
}`
newCode := strings.Join(replaceString(basicBlocks, basicOptionalVariable, renamedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "address_space" && c.Type == "delete"
}))
}
func TestBreakingChange_RemoveVariableDefaultValueShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "address_space" {
type = list(string)
description = "The address space that is used by the virtual network."
}`
newCode := strings.Join(replaceString(basicBlocks, basicOptionalVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "address_space" && c.Type == "update" && *c.Attribute == "Default"
}))
}
func TestBreakingChange_ChangeVariableDefaultValueShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "address_space" {
type = list(string)
description = "The address space that is used by the virtual network."
default = ["192.168.0.0/16"]
}`
newCode := strings.Join(replaceString(basicBlocks, basicOptionalVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "address_space" && c.Type == "update" && *c.Attribute == "Default"
}))
}
func TestBreakingChange_AddVariableSensitiveShouldNotBeBreakingChange(t *testing.T) {
sensitiveValues := []bool{true, false}
for _, v := range sensitiveValues {
t.Run(strconv.FormatBool(v), func(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := fmt.Sprintf(`
variable "address_space" {
type = list(string)
description = "The address space that is used by the virtual network."
default = ["10.0.0.0/16"]
sensitive = %t
}
`, v)
newCode := strings.Join(replaceString(basicBlocks, basicOptionalVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 0, len(changes))
})
}
}
func TestBreakingChange_ChangeVariableSensitiveFromTrueToFalseShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "db_username" {
description = "Database administrator username"
type = string
sensitive = false
}`
newCode := strings.Join(replaceString(basicBlocks, basicSensitiveVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "db_username" && c.Type == "update" && *c.Attribute == "Sensitive"
}))
}
func TestBreakingChange_RemoveVariableSensitiveShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "db_username" {
description = "Database administrator username"
type = string
}`
newCode := strings.Join(replaceString(basicBlocks, basicSensitiveVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "db_username" && c.Type == "update" && *c.Attribute == "Sensitive"
}))
}
func TestBreakingChange_ChangeVariableNullableShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "vnet_name" {
description = "Name of the vnet to create"
type = string
nullable = true
}`
newCode := strings.Join(replaceString(basicBlocks, basicRequiredVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "vnet_name" && c.Type == "update" && *c.Attribute == "Nullable"
}))
}
func TestBreakingChange_AddVariableTypeShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "subnet_service_endpoints" {
description = "A map of subnet name to service endpoints to add to the subnet."
type = map(string)
default = {}
}
`
newCode := strings.Join(replaceString(basicBlocks, unTypedVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.Equal(t, "subnet_service_endpoints", *changes[0].Name)
assert.Equal(t, "update", changes[0].Type)
assert.Equal(t, "Type", *changes[0].Attribute)
}
func TestBreakingChange_RemoveVariableTypeShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "address_space" {
description = "The address space that is used by the virtual network."
default = ["10.0.0.0/16"]
}
`
newCode := strings.Join(replaceString(basicBlocks, basicOptionalVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_ChangeVariableTypeShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "address_space" {
type = set(string)
description = "The address space that is used by the virtual network."
default = ["10.0.0.0/16"]
}`
newCode := strings.Join(replaceString(basicBlocks, basicOptionalVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.True(t, linq.From(changes).AnyWith(func(i interface{}) bool {
c := i.(Change)
return *c.Name == "address_space" && c.Type == "update" && *c.Attribute == "Type"
}))
}
func TestBreakingChange_RemoveVariableDescriptionShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "address_space" {
type = list(string)
default = ["10.0.0.0/16"]
}`
newCode := strings.Join(replaceString(basicBlocks, basicOptionalVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_ChangeVariableDescriptionShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "address_space" {
type = list(string)
description = "Changed description"
default = ["10.0.0.0/16"]
}`
newCode := strings.Join(replaceString(basicBlocks, basicOptionalVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_AddVariableDefaultValueShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedVariable := `
variable "vnet_name" {
description = "Name of the vnet to create"
type = string
nullable = false
default = "vnet"
}
`
newCode := strings.Join(replaceString(basicBlocks, basicRequiredVariable, changedVariable), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_NewOutputShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
newOutput := `
output "vnet_subnets_name_id" {
description = "Can be queried subnet-id by subnet name by using lookup(module.vnet.vnet_subnets_name_id, subnet1)"
value = local.azurerm_subnets
}`
newCode := fmt.Sprintf("%s\n%s", tpl, newOutput)
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_RemoveOutputBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
newCode := strings.Join(removeBlocks(basicBlocks, basicOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.Equal(t, "delete", changes[0].Type)
assert.Equal(t, "vnet_subnets_name_id", *changes[0].Name)
assert.Equal(t, "Name", *changes[0].Attribute)
}
func TestBreakingChange_RenameOutputShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
renamedOutput := `
output "renamed_output" {
description = "Can be queried subnet-id by subnet name by using lookup(module.vnet.vnet_subnets_name_id, subnet1)"
value = local.azurerm_subnets
}
`
newCode := strings.Join(replaceString(basicBlocks, basicOutput, renamedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.Equal(t, "delete", changes[0].Type)
assert.Equal(t, "vnet_subnets_name_id", *changes[0].Name)
assert.Equal(t, "Name", *changes[0].Attribute)
}
func TestBreakingChange_ChangeOutputDescriptionShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
renamedOutput := `
output "vnet_subnets_name_id" {
description = "changed description"
value = local.azurerm_subnets
}
`
newCode := strings.Join(replaceString(basicBlocks, basicOutput, renamedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_RemoveOutputDescriptionShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedOutput := `
output "vnet_subnets_name_id" {
value = local.azurerm_subnets
}
`
newCode := strings.Join(replaceString(basicBlocks, basicOutput, changedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_ChangeOutputValueShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedOutput := `
output "vnet_subnets_name_id" {
description = "Can be queried subnet-id by subnet name by using lookup(module.vnet.vnet_subnets_name_id, subnet1)"
value = azurerm_subnet.main.id
}
`
newCode := strings.Join(replaceString(basicBlocks, basicOutput, changedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.Equal(t, "update", changes[0].Type)
assert.Equal(t, "vnet_subnets_name_id", *changes[0].Name)
assert.Equal(t, "Value", *changes[0].Attribute)
}
func TestBreakingChange_AddOutputSensitiveTrueShouldBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedOutput := `
output "vnet_subnets_name_id" {
description = "Can be queried subnet-id by subnet name by using lookup(module.vnet.vnet_subnets_name_id, subnet1)"
value = local.azurerm_subnets
sensitive = true
}
`
newCode := strings.Join(replaceString(basicBlocks, basicOutput, changedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.Equal(t, "update", changes[0].Type)
assert.Equal(t, "vnet_subnets_name_id", *changes[0].Name)
assert.Equal(t, "Sensitive", *changes[0].Attribute)
}
func TestBreakingChange_AddOutputSensitiveFalseShouldNotBeBreakingChange(t *testing.T) {
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(tpl)
})
changedOutput := `
output "vnet_subnets_name_id" {
description = "Can be queried subnet-id by subnet name by using lookup(module.vnet.vnet_subnets_name_id, subnet1)"
value = local.azurerm_subnets
sensitive = false
}
`
newCode := strings.Join(replaceString(basicBlocks, basicOutput, changedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_ChangeOutputSensitiveToFalseShouldNotBeBreakingChange(t *testing.T) {
sensitiveBlock := `
output "kube_admin_config_raw" {
description = "A sensitive output"
sensitive = true
value = azurerm_kubernetes_cluster.main.kube_admin_config_raw
}
`
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(strings.Join(append(basicBlocks, sensitiveBlock), "\n"))
})
changedOutput := `
output "kube_admin_config_raw" {
description = "A sensitive output"
sensitive = false
value = azurerm_kubernetes_cluster.main.kube_admin_config_raw
}
`
newCode := strings.Join(append(basicBlocks, changedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
}
func TestBreakingChange_ChangeOutputSensitiveToTrueShouldBeBreakingChange(t *testing.T) {
sensitiveBlock := `
output "kube_admin_config_raw" {
description = "A sensitive output"
sensitive = false
value = azurerm_kubernetes_cluster.main.kube_admin_config_raw
}
`
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(strings.Join(append(basicBlocks, sensitiveBlock), "\n"))
})
changedOutput := `
output "kube_admin_config_raw" {
description = "A sensitive output"
sensitive = true
value = azurerm_kubernetes_cluster.main.kube_admin_config_raw
}
`
newCode := strings.Join(append(basicBlocks, changedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Equal(t, 1, len(changes))
assert.Equal(t, "kube_admin_config_raw", *changes[0].Name)
assert.Equal(t, "update", changes[0].Type)
assert.Equal(t, "Sensitive", *changes[0].Attribute)
}
func TestBreakingChange_RemoveOutputSensitiveShouldNotBeBreakingChange(t *testing.T) {
values := []bool{true, false}
for _, v := range values {
t.Run(strconv.FormatBool(v), func(t *testing.T) {
sensitiveBlock := fmt.Sprintf(`
output "kube_admin_config_raw" {
description = "A sensitive output"
sensitive = %t
value = azurerm_kubernetes_cluster.main.kube_admin_config_raw
}
`, v)
oldModule := noError(t, func() (*Module, error) {
return loadModuleByCode(strings.Join(append(basicBlocks, sensitiveBlock), "\n"))
})
changedOutput := `
output "kube_admin_config_raw" {
description = "A sensitive output"
value = azurerm_kubernetes_cluster.main.kube_admin_config_raw
}
`
newCode := strings.Join(append(basicBlocks, changedOutput), "\n")
newModule := noError(t, func() (*Module, error) {
return loadModuleByCode(newCode)
})
changes := noError(t, func() ([]Change, error) {
return BreakingChanges(oldModule, newModule)
})
assert.Empty(t, changes)
})
}
}
func TestCompareHCLModules(t *testing.T) {
diff, err := CompareTwoModules("example/breaking_change/before", "example/breaking_change/after")
assert.Nil(t, err)
assert.NotEmpty(t, diff)
}
func TestCompareJsonModules(t *testing.T) {
diff, err := CompareTwoModules("example/breaking_change/before_json", "example/breaking_change/after_json")
assert.Nil(t, err)
assert.NotEmpty(t, diff)
}
func loadModuleByCode(code string) (*Module, error) {
parser := hclparse.NewParser()
file, diag := parser.ParseHCL([]byte(code), "main.tf")
if diag.HasErrors() {
return nil, diag
}
m := tfconfig.NewModule("")
diag = tfconfig.LoadModuleFromFile(file, m)
if diag.HasErrors() {
return nil, diag
}
mapFs := afero.NewMemMapFs()
f, err := mapFs.Create("main.tf")
if err != nil {
return nil, err
}
_, err = f.WriteString(code)
if err != nil {
return nil, err
}
fs := afero.Afero{Fs: mapFs}
return &Module{
Module: m,
VariableExts: make(map[string]Variable),
OutputExts: make(map[string]Output),
fs: fs,
}, nil
}
func loadModuleByJsonCode(json string) (*Module, error) {
parser := hclparse.NewParser()
file, diag := parser.ParseJSON([]byte(json), "main.tf.json")
if diag.HasErrors() {
return nil, diag
}
m := tfconfig.NewModule("")
diag = tfconfig.LoadModuleFromFile(file, m)
if diag.HasErrors() {
return nil, diag
}
mapFs := afero.NewMemMapFs()
f, err := mapFs.Create("main.tf.json")
if err != nil {
return nil, err
}
_, err = f.WriteString(json)
if err != nil {
return nil, err
}
fs := afero.Afero{Fs: mapFs}
return &Module{
Module: m,
VariableExts: make(map[string]Variable),
OutputExts: make(map[string]Output),
fs: fs,
}, nil
}
func noError[T any](t *testing.T, f func() (T, error)) T {
r, err := f()
assert.Nil(t, err)
return r
}
func removeBlocks(slice []string, itemsToRemove ...string) []string {
var r []string
linq.From(slice).Where(func(i interface{}) bool {
for _, item := range itemsToRemove {
if i.(string) == item {
return false
}
}
return true
}).ToSlice(&r)
return r
}
func replaceString(slice []string, old, new string) []string {
return append(removeBlocks(slice, old), new)
}