2022-03-01 08:17:23 +03:00
package terraform_module_test_helper
import (
"context"
"fmt"
2023-02-17 05:26:04 +03:00
"golang.org/x/oauth2"
"net/http"
2022-03-01 08:17:23 +03:00
"os"
"path/filepath"
2022-04-07 10:59:38 +03:00
"strconv"
2022-03-01 08:17:23 +03:00
"strings"
"testing"
2023-02-06 14:20:49 +03:00
"time"
2022-03-01 08:17:23 +03:00
"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"
2022-12-01 14:14:31 +03:00
"github.com/gruntwork-io/terratest/modules/logger"
2022-03-01 08:17:23 +03:00
"github.com/gruntwork-io/terratest/modules/terraform"
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
2023-02-02 06:51:42 +03:00
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"
2022-12-01 14:14:31 +03:00
"github.com/lonegunmanb/tfmodredirector"
2024-08-20 04:09:01 +03:00
"github.com/stretchr/testify/require"
2022-03-01 08:17:23 +03:00
"golang.org/x/mod/semver"
)
2022-05-27 07:15:29 +03:00
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-13 09:38:59 +03:00
2022-05-09 03:51:36 +03:00
type repositoryTag struct {
* github . RepositoryTag
version string
}
2022-05-13 09:38:59 +03:00
//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 )
2023-02-06 14:20:49 +03:00
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 )
2022-05-27 07:15:29 +03:00
if err == CannotTestError || err == SkipV0Error {
2024-08-20 04:09:01 +03:00
t . Skip ( err . Error ( ) )
2022-05-09 03:51:36 +03:00
}
2024-08-20 04:09:01 +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 ( )
}
2023-02-06 14:20:49 +03:00
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
}
2022-10-16 04:29:35 +03:00
//goland:noinspection GoUnusedExportedFunction
2022-03-01 13:21:20 +03:00
func GetCurrentModuleRootPath ( ) ( string , error ) {
current , err := os . Getwd ( )
if err != nil {
return "" , err
}
return filepath . ToSlash ( filepath . Join ( current , ".." , ".." ) ) , nil
}
2022-05-13 09:38:59 +03:00
func GetCurrentMajorVersionFromEnv ( ) ( int , error ) {
currentMajorVer := os . Getenv ( "PREVIOUS_MAJOR_VERSION" )
if currentMajorVer == "" {
return 0 , nil
2022-03-01 08:17:23 +03:00
}
2022-05-13 09:38:59 +03:00
previousMajorVer , err := strconv . Atoi ( strings . TrimPrefix ( currentMajorVer , "v" ) )
2022-03-01 08:17:23 +03:00
if err != nil {
2022-05-13 09:38:59 +03:00
return 0 , err
2022-03-01 08:17:23 +03:00
}
2022-05-13 09:38:59 +03:00
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-03-01 08:17:23 +03:00
}
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 {
2023-01-19 08:38:23 +03:00
if currentMajorVer == 0 {
return SkipV0Error
}
2022-03-01 08:17:23 +03:00
latestTag , err := getLatestTag ( owner , repo , currentMajorVer )
if err != nil {
return err
}
2022-05-27 07:15:29 +03:00
if semver . Major ( latestTag ) == "v0" {
return SkipV0Error
}
2022-10-16 04:29:35 +03:00
tmpDirForTag , err := cloneGithubRepo ( owner , repo , & latestTag )
2022-03-01 08:17:23 +03:00
if err != nil {
return err
}
2022-05-07 07:51:56 +03:00
fullTerraformModuleFolder := filepath . Join ( tmpDirForTag , moduleFolderRelativeToRoot )
exists := files . FileExists ( fullTerraformModuleFolder )
if ! exists {
2022-05-27 07:15:29 +03:00
return CannotTestError
2022-05-07 07:51:56 +03:00
}
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 ( ) {
2023-03-16 11:34:47 +03:00
_ = 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-03-01 08:17:23 +03:00
2022-05-09 10:43:23 +03:00
func diffTwoVersions ( t * testing . T , opts terraform . Options , originTerraformDir string , newModulePath string ) error {
opts . TerraformDir = originTerraformDir
2022-11-01 11:00:27 +03:00
defer destroy ( t , opts )
2023-02-02 06:51:42 +03:00
initAndApply ( t , & opts )
2022-05-09 10:43:23 +03:00
overrideModuleSourceToCurrentPath ( t , originTerraformDir , newModulePath )
2022-06-29 15:43:03 +03:00
return initAndPlanAndIdempotentAtEasyMode ( t , opts )
}
2022-05-09 10:43:23 +03:00
2023-03-22 08:27:41 +03:00
var initAndPlanAndIdempotentAtEasyMode = func ( t * testing . T , opts terraform . Options ) error {
2022-06-29 15:43:03 +03:00
opts . PlanFilePath = filepath . Join ( opts . TerraformDir , "tf.plan" )
2022-06-30 03:29:53 +03:00
opts . Logger = logger . Discard
2023-02-02 06:51:42 +03:00
exitCode := initAndPlanWithExitCode ( t , & opts )
2022-05-09 10:43:23 +03:00
plan := terraform . InitAndPlanAndShowWithStruct ( t , & opts )
changes := plan . ResourceChangesMap
2022-06-29 15:43:03 +03:00
if exitCode == 0 || noChange ( changes ) {
2022-05-09 13:17:53 +03:00
return nil
2022-03-01 08:17:23 +03:00
}
2022-05-09 13:17:53 +03:00
return fmt . Errorf ( "terraform configuration not idempotent:%s" , terraform . Plan ( t , & opts ) )
2022-03-01 08:17:23 +03:00
}
2022-06-29 15:43:03 +03:00
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 ( )
} )
}
2022-03-01 08:17:23 +03:00
func overrideModuleSourceToCurrentPath ( t * testing . T , moduleDir string , currentModulePath string ) {
2024-08-20 04:09:01 +03:00
require . NoError ( t , rewriteHcl ( moduleDir , currentModulePath ) )
2022-05-13 09:38:59 +03:00
}
2022-12-01 14:14:31 +03:00
func rewriteHcl ( moduleDir , newModuleSource string ) error {
entries , err := os . ReadDir ( moduleDir )
2022-03-01 08:17:23 +03:00
if err != nil {
2022-09-06 08:44:32 +03:00
return err
2022-03-01 08:17:23 +03:00
}
2022-12-01 14:14:31 +03:00
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
2022-12-01 14:14:31 +03:00
}
2022-03-01 08:17:23 +03:00
}
2022-05-13 09:38:59 +03:00
return nil
2022-03-01 08:17:23 +03:00
}
2022-10-16 04:29:35 +03:00
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 )
2022-03-01 08:17:23 +03:00
if err != nil {
2022-10-16 04:29:35 +03:00
return "" , fmt . Errorf ( "cannot clone repo:%s" , err . Error ( ) )
2022-03-01 08:17:23 +03:00
}
2022-10-16 04:29:35 +03:00
return tmpDir , nil
2022-03-01 08:17:23 +03:00
}
var getLatestTag = func ( owner string , repo string , currentMajorVer int ) ( string , error ) {
2023-02-17 05:26:04 +03:00
client := github . NewClient ( githubClient ( ) )
2022-03-01 08:17:23 +03:00
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 )
2022-03-01 08:17:23 +03:00
if first == nil {
2022-05-27 07:15:29 +03:00
return "" , CannotTestError
2022-03-01 08:17:23 +03:00
}
2022-05-09 03:51:36 +03:00
latestTag := first . GetName ( )
2022-03-01 08:17:23 +03:00
return latestTag , nil
}
2023-02-17 05:26:04 +03:00
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
}
2022-03-01 08:17:23 +03:00
func valid ( t interface { } ) bool {
if t == nil {
return false
}
2022-05-09 03:51:36 +03:00
tag := t . ( repositoryTag )
v := tag . version
2022-03-01 08:17:23 +03:00
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
2022-05-07 06:40:40 +03:00
}
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
2022-03-01 08:17:23 +03:00
}
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 )
2022-03-01 08:17:23 +03:00
currentMajor := fmt . Sprintf ( "v%d" , majorVersion )
return major == currentMajor
}
}
2023-02-02 06:51:42 +03:00
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 )
}