terraform-module-test-helper/upgradetest.go

301 строка
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"
2022-05-09 10:43:23 +03:00
"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"
2022-06-15 01:08:37 +03:00
"github.com/hashicorp/go-getter/v2"
"github.com/hashicorp/terraform-json"
"github.com/lonegunmanb/tfmodredirector"
"github.com/stretchr/testify/require"
"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")
2022-05-09 03:51:36 +03:00
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) {
2023-03-08 04:41:23 +03:00
tryParallel(t)
2023-02-06 04:43:34 +03:00
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)
2023-02-06 04:43:34 +03:00
2022-07-06 09:16:29 +03:00
err := moduleUpgrade(t, owner, repo, moduleFolderRelativeToRoot, currentModulePath, retryableOptions(t, opts), currentMajorVer)
if err == CannotTestError || err == SkipV0Error {
t.Skip(err.Error())
2022-05-09 03:51:36 +03:00
}
require.NoError(t, err)
2022-05-09 03:51:36 +03:00
}
2023-03-08 04:41:23 +03:00
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
}
2022-05-09 10:43:23 +03:00
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
}
2022-05-09 10:43:23 +03:00
tmpTestDir := test_structure.CopyTerraformFolderToTemp(t, tmpDirForTag, moduleFolderRelativeToRoot)
2023-03-14 10:55:08 +03:00
defer func() {
_ = os.RemoveAll(filepath.Clean(tmpTestDir))
2023-03-14 10:55:08 +03:00
}()
2022-05-09 10:43:23 +03:00
return diffTwoVersions(t, opts, tmpTestDir, newModulePath)
}
2022-05-09 10:43:23 +03:00
func diffTwoVersions(t *testing.T, opts terraform.Options, originTerraformDir string, newModulePath string) error {
opts.TerraformDir = originTerraformDir
defer destroy(t, opts)
initAndApply(t, &opts)
2022-05-09 10:43:23 +03:00
overrideModuleSourceToCurrentPath(t, originTerraformDir, newModulePath)
return initAndPlanAndIdempotentAtEasyMode(t, opts)
}
2022-05-09 10:43:23 +03:00
var initAndPlanAndIdempotentAtEasyMode = func(t *testing.T, opts terraform.Options) error {
opts.PlanFilePath = filepath.Join(opts.TerraformDir, "tf.plan")
opts.Logger = logger.Discard
exitCode := initAndPlanWithExitCode(t, &opts)
2022-05-09 10:43:23 +03:00
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 {
2022-05-09 17:03:27 +03:00
if len(changes) == 0 {
return true
}
2022-09-06 09:49:32 +03:00
return linq.From(changes).Select(func(i interface{}) interface{} {
return i.(linq.KeyValue).Value
}).All(func(i interface{}) bool {
change := i.(*tfjson.ResourceChange).Change
2022-05-09 17:03:27 +03:00
if change == nil {
return true
}
if change.Actions == nil {
return true
}
return change.Actions.NoOp()
})
}
func overrideModuleSourceToCurrentPath(t *testing.T, moduleDir string, currentModulePath string) {
require.NoError(t, rewriteHcl(moduleDir, currentModulePath))
}
func rewriteHcl(moduleDir, newModuleSource string) error {
entries, err := os.ReadDir(moduleDir)
if err != nil {
2022-09-06 08:44:32 +03:00
return err
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".tf") {
continue
}
2023-02-01 18:06:31 +03:00
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
}
2024-09-14 05:40:44 +03:00
err = os.WriteFile(filePath, []byte(tfCode), 0600)
2023-02-01 18:06:31 +03:00
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")
}
2022-05-09 03:51:36 +03:00
first := latestTagWithinMajorVersion(tags, currentMajorVer)
if first == nil {
return "", CannotTestError
}
2022-05-09 03:51:36 +03:00
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
}
2022-05-09 03:51:36 +03:00
func latestTagWithinMajorVersion(tags []*github.RepositoryTag, currentMajorVer int) *github.RepositoryTag {
2022-09-06 08:44:32 +03:00
t := linq.From(tags).Where(notNil).Select(wrap).Where(valid).Sort(bySemantic).Where(sameMajorVersion(currentMajorVer)).Select(unwrap).First()
2022-05-09 03:51:36 +03:00
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
}
2022-05-09 03:51:36 +03:00
tag := t.(repositoryTag)
v := tag.version
return semver.IsValid(v) && !strings.Contains(v, "rc")
}
2022-09-06 08:44:32 +03:00
func bySemantic(i, j interface{}) bool {
2022-05-09 03:51:36 +03:00
it := i.(repositoryTag)
jt := j.(repositoryTag)
return semver.Compare(it.version, jt.version) > 0
}
func sterilize(v string) string {
2022-05-09 03:51:36 +03:00
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 {
2022-05-09 03:51:36 +03:00
major := semver.Major(i.(repositoryTag).version)
currentMajor := fmt.Sprintf("v%d", majorVersion)
return major == currentMajor
}
}
func initAndPlanWithExitCode(t terratest.TestingT, options *terraform.Options) int {
2023-02-02 08:45:09 +03:00
tfInit(t, options)
return terraform.PlanExitCode(t, options)
}