255 строки
6.7 KiB
Go
255 строки
6.7 KiB
Go
package terraform_module_test_helper
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/ahmetb/go-linq/v3"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/r3labs/diff/v3"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
type ChangeCategory = string
|
|
|
|
const (
|
|
variable ChangeCategory = "Variables"
|
|
output ChangeCategory = "Outputs"
|
|
)
|
|
|
|
type Change struct {
|
|
diff.Change
|
|
Category ChangeCategory `json:"category"`
|
|
Name *string `json:"name"`
|
|
Attribute *string `json:"attribute"`
|
|
}
|
|
|
|
func (c Change) ToString() string {
|
|
var name string
|
|
if c.Name != nil {
|
|
name = *c.Name
|
|
}
|
|
var attribute string
|
|
if c.Attribute != nil {
|
|
attribute = *c.Attribute
|
|
}
|
|
return fmt.Sprintf(`[%s] "%s.%s.%s" from '%v' to '%v'`, c.Type, c.Category, name, attribute, c.From, c.To)
|
|
}
|
|
|
|
func BreakingChangesDetect(currentModulePath, owner, repo string, tag *string) (string, error) {
|
|
tmpDirForLatestDefaultBranch, err := cloneGithubRepo(owner, repo, tag)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() {
|
|
_ = os.RemoveAll(tmpDirForLatestDefaultBranch)
|
|
}()
|
|
return CompareTwoModules(tmpDirForLatestDefaultBranch, currentModulePath)
|
|
}
|
|
|
|
func CompareTwoModules(dir1 string, dir2 string) (string, error) {
|
|
fs := afero.Afero{Fs: afero.OsFs{}}
|
|
oldModule, err := NewModule(dir1, fs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
currentModule, err := NewModule(dir2, fs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
changes, err := BreakingChanges(oldModule, currentModule)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
aggregated := linq.From(changes).Select(func(i interface{}) interface{} {
|
|
return i.(Change).ToString()
|
|
}).Aggregate(func(i interface{}, i2 interface{}) interface{} {
|
|
return fmt.Sprintf("%v\n%v", i, i2)
|
|
})
|
|
if r, ok := aggregated.(string); ok {
|
|
return r, nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func BreakingChanges(m1 *Module, m2 *Module) ([]Change, error) {
|
|
err := m1.Load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = m2.Load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
variableChangeLogs, err := changeLog(m1.VariableExts, m2.VariableExts, variable)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputChangeLogs, err := changeLog(m1.OutputExts, m2.OutputExts, output)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
changelog := append(variableChangeLogs, outputChangeLogs...)
|
|
return filterBreakingChanges(convert(changelog)), nil
|
|
}
|
|
|
|
func changeLog(i1, i2 interface{}, category ChangeCategory) (diff.Changelog, error) {
|
|
logs, err := diff.Diff(i1, i2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
linq.From(logs).Select(func(i interface{}) interface{} {
|
|
l := i.(diff.Change)
|
|
l.Path = append([]string{category}, l.Path...)
|
|
return l
|
|
}).ToSlice(&logs)
|
|
return logs, nil
|
|
}
|
|
|
|
func convert(cl diff.Changelog) (r []Change) {
|
|
linq.From(cl).Select(func(i interface{}) interface{} {
|
|
c := i.(diff.Change)
|
|
var name, attribute *string
|
|
if len(c.Path) > 1 {
|
|
name = &c.Path[1]
|
|
}
|
|
if len(c.Path) > 2 {
|
|
attribute = &c.Path[2]
|
|
}
|
|
return Change{
|
|
Change: diff.Change{
|
|
Type: c.Type,
|
|
Path: c.Path,
|
|
From: c.From,
|
|
To: c.To,
|
|
},
|
|
Category: c.Path[0],
|
|
Name: name,
|
|
Attribute: attribute,
|
|
}
|
|
}).ToSlice(&r)
|
|
return
|
|
}
|
|
|
|
func filterBreakingChanges(cl []Change) []Change {
|
|
variables := linq.From(cl).Where(func(i interface{}) bool {
|
|
return i.(Change).Category == variable
|
|
})
|
|
variableChanges := breakingVariables(variables)
|
|
outputs := linq.From(cl).Where(func(i interface{}) bool {
|
|
return i.(Change).Category == output
|
|
})
|
|
outputChanges := breakingOutputs(outputs)
|
|
return append(variableChanges, outputChanges...)
|
|
}
|
|
|
|
func breakingOutputs(outputs linq.Query) []Change {
|
|
var r []Change
|
|
deletedOutputs := outputs.Where(isDeletedOutput)
|
|
valueChangedOutputs := outputs.Where(valueChangedOutput)
|
|
sensitiveChangedOutputs := outputs.Where(sensitiveChangeToTrueVariable)
|
|
deletedOutputs.
|
|
Concat(valueChangedOutputs).
|
|
Concat(sensitiveChangedOutputs).ToSlice(&r)
|
|
return r
|
|
}
|
|
|
|
func sensitiveChangeToTrueVariable(i interface{}) bool {
|
|
c := i.(Change)
|
|
isSensitive := c.Type == "update" && c.Attribute != nil && *c.Attribute == "Sensitive"
|
|
if !isSensitive {
|
|
return false
|
|
}
|
|
s, ok := c.To.(string)
|
|
return ok && s == "true"
|
|
}
|
|
|
|
func valueChangedOutput(i interface{}) bool {
|
|
c := i.(Change)
|
|
return c.Type == "update" && c.Attribute != nil && (*c.Attribute == "Value")
|
|
}
|
|
|
|
func isDeletedOutput(i interface{}) bool {
|
|
c := i.(Change)
|
|
return c.Type == "delete" && c.Attribute != nil && *c.Attribute == "Name"
|
|
}
|
|
|
|
func breakingVariables(variables linq.Query) []Change {
|
|
var r []Change
|
|
newVariables := variables.Where(isNewVariable)
|
|
requiredNewVariables := groupByName(newVariables).Where(noDefaultValue)
|
|
deletedVariables := variables.Where(isDeletedVariable)
|
|
typeChangedVariables := variables.Where(typeChanged)
|
|
defaultValueBreakingChangeVariables := variables.Where(newDefaultValue)
|
|
nullableChangeVariables := variables.Where(nullableChanged)
|
|
sensitiveBrokenVariables := variables.Where(func(i interface{}) bool {
|
|
c := i.(Change)
|
|
return c.Type == "update" && c.Attribute != nil && *c.Attribute == "Sensitive" && c.From == "true" && (c.To == "" || c.To == "false")
|
|
})
|
|
requiredNewVariables.Select(recordForName).
|
|
Concat(deletedVariables).
|
|
Concat(typeChangedVariables).
|
|
Concat(defaultValueBreakingChangeVariables).
|
|
Concat(nullableChangeVariables).
|
|
Concat(sensitiveBrokenVariables).
|
|
ToSlice(&r)
|
|
return r
|
|
}
|
|
|
|
func nullableChanged(i interface{}) bool {
|
|
c := i.(Change)
|
|
return c.Type == "update" && c.Attribute != nil && *c.Attribute == "Nullable"
|
|
}
|
|
|
|
func newDefaultValue(i interface{}) bool {
|
|
c := i.(Change)
|
|
return c.Type == "update" && c.Attribute != nil && (*c.Attribute == "Default" && c.From != "")
|
|
}
|
|
|
|
func typeChanged(i interface{}) bool {
|
|
c := i.(Change)
|
|
return c.Type == "update" && c.Attribute != nil && (*c.Attribute == "Type" && !isStringNilOrEmpty(c.To))
|
|
}
|
|
|
|
func isDeletedVariable(i interface{}) bool {
|
|
c := i.(Change)
|
|
return c.Type == "delete" && c.Attribute != nil && *c.Attribute == "Name"
|
|
}
|
|
|
|
func recordForName(g interface{}) interface{} {
|
|
return linq.From(g.(linq.Group).Group).FirstWith(func(i interface{}) bool {
|
|
return i.(Change).Attribute != nil && *i.(Change).Attribute == "Name"
|
|
})
|
|
}
|
|
|
|
func groupByName(newVariables linq.Query) linq.Query {
|
|
return newVariables.GroupBy(func(i interface{}) interface{} {
|
|
return *i.(Change).Name
|
|
}, func(i interface{}) interface{} {
|
|
return i
|
|
})
|
|
}
|
|
|
|
func noDefaultValue(g interface{}) bool {
|
|
return linq.From(g.(linq.Group).Group).All(func(i interface{}) bool {
|
|
return i.(Change).Attribute == nil || *i.(Change).Attribute != "Default"
|
|
})
|
|
}
|
|
|
|
func isNewVariable(i interface{}) bool {
|
|
return i.(Change).Type == "create"
|
|
}
|
|
|
|
func isStringNilOrEmpty(i interface{}) bool {
|
|
if i == nil {
|
|
return true
|
|
}
|
|
s, ok := i.(string)
|
|
return !ok || s == ""
|
|
}
|
|
|
|
func attributeValueString(a *hcl.Attribute, f *hcl.File) string {
|
|
return string(a.Expr.Range().SliceBytes(f.Bytes))
|
|
}
|