305 строки
8.4 KiB
Go
305 строки
8.4 KiB
Go
package terraform_module_test_helper
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"golang.org/x/oauth2"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ahmetb/go-linq/v3"
|
|
"github.com/google/go-github/v42/github"
|
|
"github.com/gruntwork-io/terratest/modules/files"
|
|
"github.com/gruntwork-io/terratest/modules/logger"
|
|
"github.com/gruntwork-io/terratest/modules/terraform"
|
|
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
|
|
terratest "github.com/gruntwork-io/terratest/modules/testing"
|
|
"github.com/hashicorp/go-getter/v2"
|
|
"github.com/hashicorp/terraform-json"
|
|
"github.com/lonegunmanb/tfmodredirector"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
var CannotTestError = fmt.Errorf("no previous tag yet or previous tag's folder structure is different than the current version, skip upgrade test")
|
|
var SkipV0Error = fmt.Errorf("v0 is meant to be unstable, skip upgrade test")
|
|
|
|
type repositoryTag struct {
|
|
*github.RepositoryTag
|
|
version string
|
|
}
|
|
|
|
//goland:noinspection GoUnusedExportedFunction
|
|
func ModuleUpgradeTest(t *testing.T, owner, repo, moduleFolderRelativeToRoot, currentModulePath string, opts terraform.Options, currentMajorVer int) {
|
|
tryParallel(t)
|
|
logger.Log(t, fmt.Sprintf("===> Starting test for %s/%s/examples/%s, since we're running tests in parallel, the test log will be buffered and output to stdout after the test was finished.", owner, repo, moduleFolderRelativeToRoot))
|
|
l := NewMemoryLogger()
|
|
defer func() { _ = l.Close() }()
|
|
opts.Logger = logger.New(l)
|
|
opts = setupRetryLogic(opts)
|
|
|
|
err := moduleUpgrade(t, owner, repo, moduleFolderRelativeToRoot, currentModulePath, retryableOptions(t, opts), currentMajorVer)
|
|
if err == CannotTestError || err == SkipV0Error {
|
|
t.Skipf(err.Error())
|
|
}
|
|
if err != nil {
|
|
t.Fatalf(err.Error())
|
|
}
|
|
}
|
|
|
|
func tryParallel(t *testing.T) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
t.Log("cannot run test in parallel, skip parallel")
|
|
}
|
|
}()
|
|
t.Parallel()
|
|
}
|
|
|
|
func setupRetryLogic(opts terraform.Options) terraform.Options {
|
|
if len(opts.RetryableTerraformErrors) == 0 {
|
|
return opts
|
|
}
|
|
if opts.MaxRetries == 0 {
|
|
opts.MaxRetries = 10
|
|
}
|
|
if opts.TimeBetweenRetries == time.Duration(0) {
|
|
opts.TimeBetweenRetries = time.Minute
|
|
}
|
|
return opts
|
|
}
|
|
|
|
//goland:noinspection GoUnusedExportedFunction
|
|
func GetCurrentModuleRootPath() (string, error) {
|
|
current, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.ToSlash(filepath.Join(current, "..", "..")), nil
|
|
}
|
|
|
|
func GetCurrentMajorVersionFromEnv() (int, error) {
|
|
currentMajorVer := os.Getenv("PREVIOUS_MAJOR_VERSION")
|
|
if currentMajorVer == "" {
|
|
return 0, nil
|
|
}
|
|
previousMajorVer, err := strconv.Atoi(strings.TrimPrefix(currentMajorVer, "v"))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return previousMajorVer + 1, nil
|
|
}
|
|
|
|
func wrap(t interface{}) interface{} {
|
|
tag := t.(*github.RepositoryTag)
|
|
return repositoryTag{
|
|
RepositoryTag: tag,
|
|
version: sterilize(tag.GetName()),
|
|
}
|
|
}
|
|
|
|
func unwrap(t interface{}) interface{} {
|
|
return t.(repositoryTag).RepositoryTag
|
|
}
|
|
|
|
func moduleUpgrade(t *testing.T, owner string, repo string, moduleFolderRelativeToRoot string, newModulePath string, opts terraform.Options, currentMajorVer int) error {
|
|
if currentMajorVer == 0 {
|
|
return SkipV0Error
|
|
}
|
|
latestTag, err := getLatestTag(owner, repo, currentMajorVer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if semver.Major(latestTag) == "v0" {
|
|
return SkipV0Error
|
|
}
|
|
tmpDirForTag, err := cloneGithubRepo(owner, repo, &latestTag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fullTerraformModuleFolder := filepath.Join(tmpDirForTag, moduleFolderRelativeToRoot)
|
|
|
|
exists := files.FileExists(fullTerraformModuleFolder)
|
|
if !exists {
|
|
return CannotTestError
|
|
}
|
|
tmpTestDir := test_structure.CopyTerraformFolderToTemp(t, tmpDirForTag, moduleFolderRelativeToRoot)
|
|
defer func() {
|
|
_ = os.RemoveAll(filepath.Clean(tmpTestDir))
|
|
}()
|
|
return diffTwoVersions(t, opts, tmpTestDir, newModulePath)
|
|
}
|
|
|
|
func diffTwoVersions(t *testing.T, opts terraform.Options, originTerraformDir string, newModulePath string) error {
|
|
opts.TerraformDir = originTerraformDir
|
|
defer destroy(t, opts)
|
|
initAndApply(t, &opts)
|
|
overrideModuleSourceToCurrentPath(t, originTerraformDir, newModulePath)
|
|
return initAndPlanAndIdempotentAtEasyMode(t, opts)
|
|
}
|
|
|
|
func initAndPlanAndIdempotentAtEasyMode(t *testing.T, opts terraform.Options) error {
|
|
opts.PlanFilePath = filepath.Join(opts.TerraformDir, "tf.plan")
|
|
opts.Logger = logger.Discard
|
|
exitCode := initAndPlanWithExitCode(t, &opts)
|
|
plan := terraform.InitAndPlanAndShowWithStruct(t, &opts)
|
|
changes := plan.ResourceChangesMap
|
|
if exitCode == 0 || noChange(changes) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("terraform configuration not idempotent:%s", terraform.Plan(t, &opts))
|
|
}
|
|
|
|
func noChange(changes map[string]*tfjson.ResourceChange) bool {
|
|
if len(changes) == 0 {
|
|
return true
|
|
}
|
|
return linq.From(changes).Select(func(i interface{}) interface{} {
|
|
return i.(linq.KeyValue).Value
|
|
}).All(func(i interface{}) bool {
|
|
change := i.(*tfjson.ResourceChange).Change
|
|
if change == nil {
|
|
return true
|
|
}
|
|
if change.Actions == nil {
|
|
return true
|
|
}
|
|
return change.Actions.NoOp()
|
|
})
|
|
}
|
|
|
|
func overrideModuleSourceToCurrentPath(t *testing.T, moduleDir string, currentModulePath string) {
|
|
err := rewriteHcl(moduleDir, currentModulePath)
|
|
if err != nil {
|
|
t.Errorf(err.Error())
|
|
}
|
|
}
|
|
|
|
func rewriteHcl(moduleDir, newModuleSource string) error {
|
|
entries, err := os.ReadDir(moduleDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".tf") {
|
|
continue
|
|
}
|
|
filePath := filepath.Clean(filepath.Join(moduleDir, entry.Name()))
|
|
f, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tfCode := string(f)
|
|
tfCode, err = tfmodredirector.RedirectModuleSource(tfCode, "../../", newModuleSource)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tfCode, err = tfmodredirector.RedirectModuleSource(tfCode, "../..", newModuleSource)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(filePath, []byte(tfCode), os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var cloneGithubRepo = func(owner string, repo string, tag *string) (string, error) {
|
|
repoUrl := fmt.Sprintf("github.com/%s/%s", owner, repo)
|
|
dirPath := []string{os.TempDir(), owner, repo}
|
|
if tag != nil {
|
|
dirPath = append(dirPath, *tag)
|
|
repoUrl = fmt.Sprintf("%s?ref=%s", repoUrl, *tag)
|
|
}
|
|
tmpDir := filepath.Join(dirPath...)
|
|
_, err := getter.Get(context.TODO(), tmpDir, repoUrl)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot clone repo:%s", err.Error())
|
|
}
|
|
return tmpDir, nil
|
|
}
|
|
|
|
var getLatestTag = func(owner string, repo string, currentMajorVer int) (string, error) {
|
|
client := github.NewClient(githubClient())
|
|
tags, _, err := client.Repositories.ListTags(context.TODO(), owner, repo, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if tags == nil {
|
|
return "", fmt.Errorf("cannot find tags")
|
|
}
|
|
first := latestTagWithinMajorVersion(tags, currentMajorVer)
|
|
if first == nil {
|
|
return "", CannotTestError
|
|
}
|
|
latestTag := first.GetName()
|
|
return latestTag, nil
|
|
}
|
|
|
|
func githubClient() *http.Client {
|
|
var httpClient *http.Client
|
|
ghToken := os.Getenv("GITHUB_TOKEN")
|
|
if ghToken != "" {
|
|
ctx := context.Background()
|
|
ts := oauth2.StaticTokenSource(
|
|
&oauth2.Token{AccessToken: ghToken},
|
|
)
|
|
httpClient = oauth2.NewClient(ctx, ts)
|
|
}
|
|
return httpClient
|
|
}
|
|
|
|
func latestTagWithinMajorVersion(tags []*github.RepositoryTag, currentMajorVer int) *github.RepositoryTag {
|
|
t := linq.From(tags).Where(notNil).Select(wrap).Where(valid).Sort(bySemantic).Where(sameMajorVersion(currentMajorVer)).Select(unwrap).First()
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
return t.(*github.RepositoryTag)
|
|
}
|
|
|
|
func notNil(i interface{}) bool {
|
|
return i != nil
|
|
}
|
|
|
|
func valid(t interface{}) bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
tag := t.(repositoryTag)
|
|
v := tag.version
|
|
return semver.IsValid(v) && !strings.Contains(v, "rc")
|
|
}
|
|
|
|
func bySemantic(i, j interface{}) bool {
|
|
it := i.(repositoryTag)
|
|
jt := j.(repositoryTag)
|
|
return semver.Compare(it.version, jt.version) > 0
|
|
}
|
|
|
|
func sterilize(v string) string {
|
|
if !strings.HasPrefix(v, "v") {
|
|
return fmt.Sprintf("v%s", v)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func sameMajorVersion(majorVersion int) func(i interface{}) bool {
|
|
return func(i interface{}) bool {
|
|
major := semver.Major(i.(repositoryTag).version)
|
|
currentMajor := fmt.Sprintf("v%d", majorVersion)
|
|
return major == currentMajor
|
|
}
|
|
}
|
|
|
|
func initAndPlanWithExitCode(t terratest.TestingT, options *terraform.Options) int {
|
|
tfInit(t, options)
|
|
return terraform.PlanExitCode(t, options)
|
|
}
|