feat: OpenTofu support (#4499)

Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com>
This commit is contained in:
Henry Muru Paenga
2024-09-30 14:13:20 +13:00
committed by GitHub
parent 82b95fce9d
commit 9c7e5fcac8
19 changed files with 718 additions and 301 deletions

View File

@@ -37,6 +37,12 @@ const (
CheckoutStrategyMerge = "merge"
)
// TF distributions
const (
TFDistributionTerraform = "terraform"
TFDistributionOpenTofu = "opentofu"
)
// To add a new flag you must:
// 1. Add a const with the flag name (in alphabetic order).
// 2. Add a new field to server.UserConfig and set the mapstructure tag equal to the flag name.
@@ -134,6 +140,7 @@ const (
SSLCertFileFlag = "ssl-cert-file"
SSLKeyFileFlag = "ssl-key-file"
RestrictFileList = "restrict-file-list"
TFDistributionFlag = "tf-distribution"
TFDownloadFlag = "tf-download"
TFDownloadURLFlag = "tf-download-url"
UseTFPluginCache = "use-tf-plugin-cache"
@@ -176,6 +183,7 @@ const (
DefaultRedisPort = 6379
DefaultRedisTLSEnabled = false
DefaultRedisInsecureSkipVerify = false
DefaultTFDistribution = TFDistributionTerraform
DefaultTFDownloadURL = "https://releases.hashicorp.com"
DefaultTFDownload = true
DefaultTFEHostname = "app.terraform.io"
@@ -406,6 +414,10 @@ var stringFlags = map[string]stringFlag{
SSLKeyFileFlag: {
description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag),
},
TFDistributionFlag: {
description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu),
defaultValue: DefaultTFDistribution,
},
TFDownloadURLFlag: {
description: "Base URL to download Terraform versions from.",
defaultValue: DefaultTFDownloadURL,
@@ -897,6 +909,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig, v *viper.Viper) {
if c.RedisPort == 0 {
c.RedisPort = DefaultRedisPort
}
if c.TFDistribution == "" {
c.TFDistribution = DefaultTFDistribution
}
if c.TFDownloadURL == "" {
c.TFDownloadURL = DefaultTFDownloadURL
}
@@ -923,6 +938,11 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
return fmt.Errorf("invalid log level: must be one of %v", ValidLogLevels)
}
if userConfig.TFDistribution != TFDistributionTerraform && userConfig.TFDistribution != TFDistributionOpenTofu {
return fmt.Errorf("invalid tf distribution: expected one of %s or %s",
TFDistributionTerraform, TFDistributionOpenTofu)
}
checkoutStrategy := userConfig.CheckoutStrategy
if checkoutStrategy != CheckoutStrategyBranch && checkoutStrategy != CheckoutStrategyMerge {
return fmt.Errorf("invalid checkout strategy: not one of %s or %s",

View File

@@ -136,6 +136,7 @@ var testFlags = map[string]interface{}{
SSLCertFileFlag: "cert-file",
SSLKeyFileFlag: "key-file",
RestrictFileList: false,
TFDistributionFlag: "terraform",
TFDownloadFlag: true,
TFDownloadURLFlag: "https://my-hostname.com",
TFEHostnameFlag: "my-hostname",

5
go.mod
View File

@@ -32,6 +32,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0
github.com/moby/patternmatcher v0.6.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/opentofu/tofudl v0.0.0-20240923062014-8c1e00f33ce6
github.com/petergtz/pegomock/v4 v4.1.0
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.6.1
@@ -67,13 +68,15 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/gopenpgp/v2 v2.7.5 // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect

31
go.sum
View File

@@ -45,8 +45,13 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg=
github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA=
github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -78,6 +83,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cactus/go-statsd-client/v5 v5.0.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY=
github.com/cactus/go-statsd-client/v5 v5.1.0 h1:sbbdfIl9PgisjEoXzvXI1lwUKWElngsjJKaZeC021P4=
github.com/cactus/go-statsd-client/v5 v5.1.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY=
@@ -90,8 +96,9 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -347,6 +354,8 @@ github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/opentofu/tofudl v0.0.0-20240923062014-8c1e00f33ce6 h1:+1yJm0gEoDaxYmMhmmU3gRAOMx3A43z84bokm1dQroU=
github.com/opentofu/tofudl v0.0.0-20240923062014-8c1e00f33ce6/go.mod h1:CD1BhvxxNPp4ZBwNBjWycf5isG9UaPrzfE7J/E/s6RY=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/petergtz/pegomock/v4 v4.1.0 h1:Reoy2rlwshuxNaD2ZWp5TrSCrmoFH5SSLHb5U1z2pog=
@@ -434,6 +443,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -490,6 +500,8 @@ golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -525,6 +537,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -560,6 +573,9 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -582,6 +598,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -625,11 +642,17 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -640,6 +663,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -688,6 +714,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -1241,6 +1241,14 @@ This is useful when you have many projects and want to keep the pull request cle
Namespace for emitting stats/metrics. See [stats](stats.md) section.
### `--tf-distribution`
```bash
atlantis server --tf-distribution="terraform"
# or
ATLANTIS_TF_DISTRIBUTION="terraform"
```
Which TF distribution to use. Can be set to `terraform` or `opentofu`.
### `--tf-download`
```bash
@@ -1266,6 +1274,8 @@ Setting this to `false` can be useful in an air-gapped environment where a downl
This has no impact if `--tf-download` is set to `false`.
This setting is not yet supported when `--tf-distribution` is set to `opentofu`.
### `--tfe-hostname`
```bash

View File

@@ -26,7 +26,9 @@ import (
"github.com/runatlantis/atlantis/server/core/runtime"
runtimemocks "github.com/runatlantis/atlantis/server/core/runtime/mocks"
"github.com/runatlantis/atlantis/server/core/runtime/policy"
mock_policy "github.com/runatlantis/atlantis/server/core/runtime/policy/mocks"
"github.com/runatlantis/atlantis/server/core/terraform"
terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/mocks"
@@ -53,10 +55,6 @@ var mockPreWorkflowHookRunner *runtimemocks.MockPreWorkflowHookRunner
var mockPostWorkflowHookRunner *runtimemocks.MockPostWorkflowHookRunner
func (m *NoopTFDownloader) GetAny(_, _ string) error {
return nil
}
func (m *NoopTFDownloader) Install(_ string, _ string, _ *version.Version) (string, error) {
return "", nil
}
@@ -1317,7 +1315,11 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
ExecutableName: "atlantis",
AllowCommands: allowCommands,
}
terraformClient, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", &NoopTFDownloader{}, true, false, projectCmdOutputHandler)
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
terraformClient, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler)
Ok(t, err)
boltdb, err := db.New(dataDir)
Ok(t, err)
@@ -1431,7 +1433,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
Ok(t, err)
conftextExec := policy.NewConfTestExecutorWorkflow(logger, binDir, &NoopTFDownloader{})
conftextExec := policy.NewConfTestExecutorWorkflow(logger, binDir, mock_policy.NewMockDownloader())
// swapping out version cache to something that always returns local conftest
// binary
@@ -1881,4 +1883,7 @@ func ensureRunning014(t *testing.T) {
//
// Terraform v0.11.10
// => 0.11.10
var versionRegex = regexp.MustCompile("Terraform v(.*?)(\\s.*)?\n")
//
// OpenTofu v1.0.0
// => 1.0.0
var versionRegex = regexp.MustCompile("(?:Terraform|OpenTofu) v(.*?)(\\s.*)?\n")

View File

@@ -1,6 +1,7 @@
package policy
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -10,13 +11,13 @@ import (
"encoding/json"
"regexp"
"github.com/hashicorp/go-getter/v2"
"github.com/hashicorp/go-multierror"
version "github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/runtime/cache"
runtime_models "github.com/runatlantis/atlantis/server/core/runtime/models"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
@@ -103,8 +104,21 @@ func (p *SourceResolverProxy) Resolve(policySet valid.PolicySet) (string, error)
}
}
//go:generate pegomock generate --package mocks -o mocks/mock_downloader.go Downloader
type Downloader interface {
GetAny(dst, src string) error
}
type ConfTestGoGetterVersionDownloader struct{}
func (c ConfTestGoGetterVersionDownloader) GetAny(dst, src string) error {
_, err := getter.GetAny(context.Background(), dst, src)
return err
}
type ConfTestVersionDownloader struct {
downloader terraform.Downloader
downloader Downloader
}
func (c ConfTestVersionDownloader) downloadConfTestVersion(v *version.Version, destPath string) (runtime_models.FilePath, error) {
@@ -142,7 +156,7 @@ type ConfTestExecutorWorkflow struct {
Exec runtime_models.Exec
}
func NewConfTestExecutorWorkflow(log logging.SimpleLogging, versionRootDir string, conftestDownloder terraform.Downloader) *ConfTestExecutorWorkflow {
func NewConfTestExecutorWorkflow(log logging.SimpleLogging, versionRootDir string, conftestDownloder Downloader) *ConfTestExecutorWorkflow {
downloader := ConfTestVersionDownloader{
downloader: conftestDownloder,
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/runatlantis/atlantis/server/core/runtime/cache/mocks"
models_mocks "github.com/runatlantis/atlantis/server/core/runtime/models/mocks"
conftest_mocks "github.com/runatlantis/atlantis/server/core/runtime/policy/mocks"
terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
@@ -27,9 +26,11 @@ func TestConfTestVersionDownloader(t *testing.T) {
RegisterMockTestingT(t)
mockDownloader := terraform_mocks.NewMockDownloader()
mockDownloader := conftest_mocks.NewMockDownloader()
subject := ConfTestVersionDownloader{downloader: mockDownloader}
subject := ConfTestVersionDownloader{
downloader: mockDownloader,
}
t.Run("success", func(t *testing.T) {

View File

@@ -0,0 +1,112 @@
// Code generated by pegomock. DO NOT EDIT.
// Source: github.com/runatlantis/atlantis/server/core/runtime/policy (interfaces: Downloader)
package mocks
import (
pegomock "github.com/petergtz/pegomock/v4"
"reflect"
"time"
)
type MockDownloader struct {
fail func(message string, callerSkip ...int)
}
func NewMockDownloader(options ...pegomock.Option) *MockDownloader {
mock := &MockDownloader{}
for _, option := range options {
option.Apply(mock)
}
return mock
}
func (mock *MockDownloader) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockDownloader) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockDownloader) GetAny(dst string, src string) error {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockDownloader().")
}
_params := []pegomock.Param{dst, src}
_result := pegomock.GetGenericMockFrom(mock).Invoke("GetAny", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var _ret0 error
if len(_result) != 0 {
if _result[0] != nil {
_ret0 = _result[0].(error)
}
}
return _ret0
}
func (mock *MockDownloader) VerifyWasCalledOnce() *VerifierMockDownloader {
return &VerifierMockDownloader{
mock: mock,
invocationCountMatcher: pegomock.Times(1),
}
}
func (mock *MockDownloader) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockDownloader {
return &VerifierMockDownloader{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
}
}
func (mock *MockDownloader) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockDownloader {
return &VerifierMockDownloader{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
inOrderContext: inOrderContext,
}
}
func (mock *MockDownloader) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockDownloader {
return &VerifierMockDownloader{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
timeout: timeout,
}
}
type VerifierMockDownloader struct {
mock *MockDownloader
invocationCountMatcher pegomock.InvocationCountMatcher
inOrderContext *pegomock.InOrderContext
timeout time.Duration
}
func (verifier *VerifierMockDownloader) GetAny(dst string, src string) *MockDownloader_GetAny_OngoingVerification {
_params := []pegomock.Param{dst, src}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetAny", _params, verifier.timeout)
return &MockDownloader_GetAny_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockDownloader_GetAny_OngoingVerification struct {
mock *MockDownloader
methodInvocations []pegomock.MethodInvocation
}
func (c *MockDownloader_GetAny_OngoingVerification) GetCapturedArguments() (string, string) {
dst, src := c.GetAllCapturedArguments()
return dst[len(dst)-1], src[len(src)-1]
}
func (c *MockDownloader_GetAny_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string) {
_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(_params) > 0 {
if len(_params) > 0 {
_param0 = make([]string, len(c.methodInvocations))
for u, param := range _params[0] {
_param0[u] = param.(string)
}
}
if len(_params) > 1 {
_param1 = make([]string, len(c.methodInvocations))
for u, param := range _params[1] {
_param1[u] = param.(string)
}
}
}
return
}

View File

@@ -0,0 +1,133 @@
package terraform
import (
"context"
"fmt"
"sort"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
"github.com/opentofu/tofudl"
)
type Distribution interface {
BinName() string
Downloader() Downloader
// ResolveConstraint gets the latest version for the given constraint
ResolveConstraint(context.Context, string) (*version.Version, error)
}
type DistributionOpenTofu struct {
downloader Downloader
}
func NewDistributionOpenTofu() Distribution {
return &DistributionOpenTofu{
downloader: &TofuDownloader{},
}
}
func NewDistributionOpenTofuWithDownloader(downloader Downloader) Distribution {
return &DistributionOpenTofu{
downloader: downloader,
}
}
func (*DistributionOpenTofu) BinName() string {
return "tofu"
}
func (d *DistributionOpenTofu) Downloader() Downloader {
return d.downloader
}
func (*DistributionOpenTofu) ResolveConstraint(ctx context.Context, constraintStr string) (*version.Version, error) {
dl, err := tofudl.New()
if err != nil {
return nil, err
}
vc, err := version.NewConstraint(constraintStr)
if err != nil {
return nil, fmt.Errorf("error parsing constraint string: %s", err)
}
allVersions, err := dl.ListVersions(ctx)
if err != nil {
return nil, fmt.Errorf("error listing OpenTofu versions: %s", err)
}
var versions []*version.Version
for _, ver := range allVersions {
v, err := version.NewVersion(string(ver.ID))
if err != nil {
return nil, err
}
if vc.Check(v) {
versions = append(versions, v)
}
}
sort.Sort(version.Collection(versions))
if len(versions) == 0 {
return nil, fmt.Errorf("no OpenTofu versions found for constraints %s", constraintStr)
}
// We want to select the highest version that satisfies the constraint.
version := versions[len(versions)-1]
// Get the Version object from the versionDownloader.
return version, nil
}
type DistributionTerraform struct {
downloader Downloader
}
func NewDistributionTerraform() Distribution {
return &DistributionTerraform{
downloader: &TerraformDownloader{},
}
}
func NewDistributionTerraformWithDownloader(downloader Downloader) Distribution {
return &DistributionTerraform{
downloader: downloader,
}
}
func (*DistributionTerraform) BinName() string {
return "terraform"
}
func (d *DistributionTerraform) Downloader() Downloader {
return d.downloader
}
func (*DistributionTerraform) ResolveConstraint(ctx context.Context, constraintStr string) (*version.Version, error) {
vc, err := version.NewConstraint(constraintStr)
if err != nil {
return nil, fmt.Errorf("error parsing constraint string: %s", err)
}
constrainedVersions := &releases.Versions{
Product: product.Terraform,
Constraints: vc,
}
installCandidates, err := constrainedVersions.List(ctx)
if err != nil {
return nil, fmt.Errorf("error listing available versions: %s", err)
}
if len(installCandidates) == 0 {
return nil, fmt.Errorf("no Terraform versions found for constraints %s", constraintStr)
}
// We want to select the highest version that satisfies the constraint.
versionDownloader := installCandidates[len(installCandidates)-1]
// Get the Version object from the versionDownloader.
return versionDownloader.(*releases.ExactVersion).Version, nil
}

View File

@@ -0,0 +1,33 @@
package terraform_test
import (
"context"
"testing"
"github.com/runatlantis/atlantis/server/core/terraform"
. "github.com/runatlantis/atlantis/testing"
)
func TestOpenTofuBinName(t *testing.T) {
d := terraform.NewDistributionOpenTofu()
Equals(t, d.BinName(), "tofu")
}
func TestResolveOpenTofuVersions(t *testing.T) {
d := terraform.NewDistributionOpenTofu()
version, err := d.ResolveConstraint(context.Background(), "= 1.8.0")
Ok(t, err)
Equals(t, version.String(), "1.8.0")
}
func TestTerraformBinName(t *testing.T) {
d := terraform.NewDistributionTerraform()
Equals(t, d.BinName(), "terraform")
}
func TestResolveTerraformVersions(t *testing.T) {
d := terraform.NewDistributionTerraform()
version, err := d.ResolveConstraint(context.Background(), "= 1.9.3")
Ok(t, err)
Equals(t, version.String(), "1.9.3")
}

View File

@@ -0,0 +1,70 @@
package terraform
import (
"context"
"os"
"path/filepath"
"github.com/hashicorp/go-version"
install "github.com/hashicorp/hc-install"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
"github.com/hashicorp/hc-install/src"
"github.com/opentofu/tofudl"
)
//go:generate pegomock generate --package mocks -o mocks/mock_downloader.go Downloader
// Downloader is for downloading terraform versions.
type Downloader interface {
Install(ctx context.Context, dir string, downloadURL string, v *version.Version) (string, error)
}
type TofuDownloader struct{}
func (d *TofuDownloader) Install(ctx context.Context, dir string, _downloadURL string, v *version.Version) (string, error) {
// Initialize the downloader:
dl, err := tofudl.New()
if err != nil {
return "", err
}
binary, err := dl.Download(ctx, tofudl.DownloadOptVersion(tofudl.Version(v.String())))
if err != nil {
return "", err
}
// Write out the tofu binary to the disk:
file := filepath.Join(dir, "tofu"+v.String())
if err := os.WriteFile(file, binary, 0755); /* #nosec G306 */ err != nil {
return "", err
}
return file, nil
}
type TerraformDownloader struct{}
func (d *TerraformDownloader) Install(ctx context.Context, dir string, downloadURL string, v *version.Version) (string, error) {
installer := install.NewInstaller()
execPath, err := installer.Install(ctx, []src.Installable{
&releases.ExactVersion{
Product: product.Terraform,
Version: v,
InstallDir: dir,
ApiBaseURL: downloadURL,
},
})
if err != nil {
return "", err
}
// hc-install installs terraform binary as just "terraform".
// We need to rename it to terraform{version} to be consistent with current naming convention.
newPath := filepath.Join(dir, "terraform"+v.String())
if err := os.Rename(execPath, newPath); err != nil {
return "", err
}
return newPath, nil
}

View File

@@ -0,0 +1,46 @@
package terraform_test
import (
"context"
"os"
"testing"
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/cmd"
"github.com/runatlantis/atlantis/server/core/terraform"
)
func TestTerraformInstall(t *testing.T) {
d := &terraform.TerraformDownloader{}
RegisterMockTestingT(t)
binDir := t.TempDir()
v, _ := version.NewVersion("1.8.1")
newPath, err := d.Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if _, err := os.Stat(newPath); os.IsNotExist(err) {
t.Errorf("Binary not found at %s", newPath)
}
}
func TestOpenTofuInstall(t *testing.T) {
d := &terraform.TofuDownloader{}
RegisterMockTestingT(t)
binDir := t.TempDir()
v, _ := version.NewVersion("1.8.0")
newPath, err := d.Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if _, err := os.Stat(newPath); os.IsNotExist(err) {
t.Errorf("Binary not found at %s", newPath)
}
}

View File

@@ -4,6 +4,7 @@
package mocks
import (
context "context"
go_version "github.com/hashicorp/go-version"
pegomock "github.com/petergtz/pegomock/v4"
"reflect"
@@ -25,38 +26,23 @@ func NewMockDownloader(options ...pegomock.Option) *MockDownloader {
func (mock *MockDownloader) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockDownloader) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockDownloader) GetAny(dst string, src string) error {
func (mock *MockDownloader) Install(ctx context.Context, dir string, downloadURL string, v *go_version.Version) (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockDownloader().")
}
params := []pegomock.Param{dst, src}
result := pegomock.GetGenericMockFrom(mock).Invoke("GetAny", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var ret0 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(error)
_params := []pegomock.Param{ctx, dir, downloadURL, v}
_result := pegomock.GetGenericMockFrom(mock).Invoke("Install", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var _ret0 string
var _ret1 error
if len(_result) != 0 {
if _result[0] != nil {
_ret0 = _result[0].(string)
}
if _result[1] != nil {
_ret1 = _result[1].(error)
}
}
return ret0
}
func (mock *MockDownloader) Install(dir string, downloadURL string, v *go_version.Version) (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockDownloader().")
}
params := []pegomock.Param{dir, downloadURL, v}
result := pegomock.GetGenericMockFrom(mock).Invoke("Install", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
return _ret0, _ret1
}
func (mock *MockDownloader) VerifyWasCalledOnce() *VerifierMockDownloader {
@@ -96,40 +82,9 @@ type VerifierMockDownloader struct {
timeout time.Duration
}
func (verifier *VerifierMockDownloader) GetAny(dst string, src string) *MockDownloader_GetAny_OngoingVerification {
params := []pegomock.Param{dst, src}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetAny", params, verifier.timeout)
return &MockDownloader_GetAny_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockDownloader_GetAny_OngoingVerification struct {
mock *MockDownloader
methodInvocations []pegomock.MethodInvocation
}
func (c *MockDownloader_GetAny_OngoingVerification) GetCapturedArguments() (string, string) {
dst, src := c.GetAllCapturedArguments()
return dst[len(dst)-1], src[len(src)-1]
}
func (c *MockDownloader_GetAny_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]string, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(string)
}
_param1 = make([]string, len(c.methodInvocations))
for u, param := range params[1] {
_param1[u] = param.(string)
}
}
return
}
func (verifier *VerifierMockDownloader) Install(dir string, downloadURL string, v *go_version.Version) *MockDownloader_Install_OngoingVerification {
params := []pegomock.Param{dir, downloadURL, v}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Install", params, verifier.timeout)
func (verifier *VerifierMockDownloader) Install(ctx context.Context, dir string, downloadURL string, v *go_version.Version) *MockDownloader_Install_OngoingVerification {
_params := []pegomock.Param{ctx, dir, downloadURL, v}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Install", _params, verifier.timeout)
return &MockDownloader_Install_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
@@ -138,25 +93,37 @@ type MockDownloader_Install_OngoingVerification struct {
methodInvocations []pegomock.MethodInvocation
}
func (c *MockDownloader_Install_OngoingVerification) GetCapturedArguments() (string, string, *go_version.Version) {
dir, downloadURL, v := c.GetAllCapturedArguments()
return dir[len(dir)-1], downloadURL[len(downloadURL)-1], v[len(v)-1]
func (c *MockDownloader_Install_OngoingVerification) GetCapturedArguments() (context.Context, string, string, *go_version.Version) {
ctx, dir, downloadURL, v := c.GetAllCapturedArguments()
return ctx[len(ctx)-1], dir[len(dir)-1], downloadURL[len(downloadURL)-1], v[len(v)-1]
}
func (c *MockDownloader_Install_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []*go_version.Version) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]string, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(string)
func (c *MockDownloader_Install_OngoingVerification) GetAllCapturedArguments() (_param0 []context.Context, _param1 []string, _param2 []string, _param3 []*go_version.Version) {
_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(_params) > 0 {
if len(_params) > 0 {
_param0 = make([]context.Context, len(c.methodInvocations))
for u, param := range _params[0] {
_param0[u] = param.(context.Context)
}
}
_param1 = make([]string, len(c.methodInvocations))
for u, param := range params[1] {
_param1[u] = param.(string)
if len(_params) > 1 {
_param1 = make([]string, len(c.methodInvocations))
for u, param := range _params[1] {
_param1[u] = param.(string)
}
}
_param2 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range params[2] {
_param2[u] = param.(*go_version.Version)
if len(_params) > 2 {
_param2 = make([]string, len(c.methodInvocations))
for u, param := range _params[2] {
_param2[u] = param.(string)
}
}
if len(_params) > 3 {
_param3 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range _params[3] {
_param3[u] = param.(*go_version.Version)
}
}
}
return

View File

@@ -31,49 +31,49 @@ func (mock *MockClient) DetectVersion(log logging.SimpleLogging, projectDirector
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClient().")
}
params := []pegomock.Param{log, projectDirectory}
result := pegomock.GetGenericMockFrom(mock).Invoke("DetectVersion", params, []reflect.Type{reflect.TypeOf((**go_version.Version)(nil)).Elem()})
var ret0 *go_version.Version
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(*go_version.Version)
_params := []pegomock.Param{log, projectDirectory}
_result := pegomock.GetGenericMockFrom(mock).Invoke("DetectVersion", _params, []reflect.Type{reflect.TypeOf((**go_version.Version)(nil)).Elem()})
var _ret0 *go_version.Version
if len(_result) != 0 {
if _result[0] != nil {
_ret0 = _result[0].(*go_version.Version)
}
}
return ret0
return _ret0
}
func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) error {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClient().")
}
params := []pegomock.Param{log, v}
result := pegomock.GetGenericMockFrom(mock).Invoke("EnsureVersion", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var ret0 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(error)
_params := []pegomock.Param{log, v}
_result := pegomock.GetGenericMockFrom(mock).Invoke("EnsureVersion", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var _ret0 error
if len(_result) != 0 {
if _result[0] != nil {
_ret0 = _result[0].(error)
}
}
return ret0
return _ret0
}
func (mock *MockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClient().")
}
params := []pegomock.Param{ctx, path, args, envs, v, workspace}
result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandWithVersion", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
_params := []pegomock.Param{ctx, path, args, envs, v, workspace}
_result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandWithVersion", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var _ret0 string
var _ret1 error
if len(_result) != 0 {
if _result[0] != nil {
_ret0 = _result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(error)
if _result[1] != nil {
_ret1 = _result[1].(error)
}
}
return ret0, ret1
return _ret0, _ret1
}
func (mock *MockClient) VerifyWasCalledOnce() *VerifierMockClient {
@@ -114,8 +114,8 @@ type VerifierMockClient struct {
}
func (verifier *VerifierMockClient) DetectVersion(log logging.SimpleLogging, projectDirectory string) *MockClient_DetectVersion_OngoingVerification {
params := []pegomock.Param{log, projectDirectory}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DetectVersion", params, verifier.timeout)
_params := []pegomock.Param{log, projectDirectory}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DetectVersion", _params, verifier.timeout)
return &MockClient_DetectVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
@@ -130,23 +130,27 @@ func (c *MockClient_DetectVersion_OngoingVerification) GetCapturedArguments() (l
}
func (c *MockClient_DetectVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(logging.SimpleLogging)
_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(_params) > 0 {
if len(_params) > 0 {
_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))
for u, param := range _params[0] {
_param0[u] = param.(logging.SimpleLogging)
}
}
_param1 = make([]string, len(c.methodInvocations))
for u, param := range params[1] {
_param1[u] = param.(string)
if len(_params) > 1 {
_param1 = make([]string, len(c.methodInvocations))
for u, param := range _params[1] {
_param1[u] = param.(string)
}
}
}
return
}
func (verifier *VerifierMockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) *MockClient_EnsureVersion_OngoingVerification {
params := []pegomock.Param{log, v}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "EnsureVersion", params, verifier.timeout)
_params := []pegomock.Param{log, v}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "EnsureVersion", _params, verifier.timeout)
return &MockClient_EnsureVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
@@ -161,23 +165,27 @@ func (c *MockClient_EnsureVersion_OngoingVerification) GetCapturedArguments() (l
}
func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []*go_version.Version) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(logging.SimpleLogging)
_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(_params) > 0 {
if len(_params) > 0 {
_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))
for u, param := range _params[0] {
_param0[u] = param.(logging.SimpleLogging)
}
}
_param1 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range params[1] {
_param1[u] = param.(*go_version.Version)
if len(_params) > 1 {
_param1 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range _params[1] {
_param1[u] = param.(*go_version.Version)
}
}
}
return
}
func (verifier *VerifierMockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification {
params := []pegomock.Param{ctx, path, args, envs, v, workspace}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandWithVersion", params, verifier.timeout)
_params := []pegomock.Param{ctx, path, args, envs, v, workspace}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandWithVersion", _params, verifier.timeout)
return &MockClient_RunCommandWithVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
@@ -192,31 +200,43 @@ func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArgume
}
func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []*go_version.Version, _param5 []string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]command.ProjectContext, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(command.ProjectContext)
_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(_params) > 0 {
if len(_params) > 0 {
_param0 = make([]command.ProjectContext, len(c.methodInvocations))
for u, param := range _params[0] {
_param0[u] = param.(command.ProjectContext)
}
}
_param1 = make([]string, len(c.methodInvocations))
for u, param := range params[1] {
_param1[u] = param.(string)
if len(_params) > 1 {
_param1 = make([]string, len(c.methodInvocations))
for u, param := range _params[1] {
_param1[u] = param.(string)
}
}
_param2 = make([][]string, len(c.methodInvocations))
for u, param := range params[2] {
_param2[u] = param.([]string)
if len(_params) > 2 {
_param2 = make([][]string, len(c.methodInvocations))
for u, param := range _params[2] {
_param2[u] = param.([]string)
}
}
_param3 = make([]map[string]string, len(c.methodInvocations))
for u, param := range params[3] {
_param3[u] = param.(map[string]string)
if len(_params) > 3 {
_param3 = make([]map[string]string, len(c.methodInvocations))
for u, param := range _params[3] {
_param3[u] = param.(map[string]string)
}
}
_param4 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range params[4] {
_param4[u] = param.(*go_version.Version)
if len(_params) > 4 {
_param4 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range _params[4] {
_param4[u] = param.(*go_version.Version)
}
}
_param5 = make([]string, len(c.methodInvocations))
for u, param := range params[5] {
_param5[u] = param.(string)
if len(_params) > 5 {
_param5 = make([]string, len(c.methodInvocations))
for u, param := range _params[5] {
_param5[u] = param.(string)
}
}
}
return

View File

@@ -27,12 +27,7 @@ import (
"sync"
"time"
"github.com/hashicorp/go-getter/v2"
"github.com/hashicorp/go-version"
install "github.com/hashicorp/hc-install"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
"github.com/hashicorp/hc-install/src"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
@@ -62,6 +57,9 @@ type Client interface {
}
type DefaultClient struct {
// Distribution handles logic specific to the TF distribution being used by Atlantis
distribution Distribution
// defaultVersion is the default version of terraform to use if another
// version isn't specified.
defaultVersion *version.Version
@@ -72,8 +70,7 @@ type DefaultClient struct {
// overrideTF can be used to override the terraform binary during testing
// with another binary, ex. echo.
overrideTF string
// downloader downloads terraform versions.
downloader Downloader
// settings for the downloader.
downloadBaseURL string
downloadAllowed bool
// versions maps from the string representation of a tf version (ex. 0.11.10)
@@ -90,14 +87,6 @@ type DefaultClient struct {
projectCmdOutputHandler jobs.ProjectCommandOutputHandler
}
//go:generate pegomock generate --package mocks -o mocks/mock_downloader.go Downloader
// Downloader is for downloading terraform versions.
type Downloader interface {
Install(dir string, downloadURL string, v *version.Version) (string, error)
GetAny(dst, src string) error
}
// versionRegex extracts the version from `terraform version` output.
//
// Terraform v0.12.0-alpha4 (2c36829d3265661d8edbd5014de8090ea7e2a076)
@@ -105,11 +94,15 @@ type Downloader interface {
//
// Terraform v0.11.10
// => 0.11.10
var versionRegex = regexp.MustCompile("Terraform v(.*?)(\\s.*)?\n")
//
// OpenTofu v1.0.0
// => 1.0.0
var versionRegex = regexp.MustCompile("(?:Terraform|OpenTofu) v(.*?)(\\s.*)?\n")
// NewClientWithDefaultVersion creates a new terraform client and pre-fetches the default version
func NewClientWithDefaultVersion(
log logging.SimpleLogging,
distribution Distribution,
binDir string,
cacheDir string,
tfeToken string,
@@ -117,7 +110,6 @@ func NewClientWithDefaultVersion(
defaultVersionStr string,
defaultVersionFlagName string,
tfDownloadURL string,
tfDownloader Downloader,
tfDownloadAllowed bool,
usePluginCache bool,
fetchAsync bool,
@@ -128,9 +120,9 @@ func NewClientWithDefaultVersion(
versions := make(map[string]string)
var versionsLock sync.Mutex
localPath, err := exec.LookPath("terraform")
localPath, err := exec.LookPath(distribution.BinName())
if err != nil && defaultVersionStr == "" {
return nil, fmt.Errorf("terraform not found in $PATH. Set --%s or download terraform from https://developer.hashicorp.com/terraform/downloads", defaultVersionFlagName)
return nil, fmt.Errorf("%s not found in $PATH. Set --%s or download terraform from https://developer.hashicorp.com/terraform/downloads", distribution.BinName(), defaultVersionFlagName)
}
if err == nil {
localVersion, err = getVersion(localPath)
@@ -139,6 +131,7 @@ func NewClientWithDefaultVersion(
}
versions[localVersion.String()] = localPath
if defaultVersionStr == "" {
// If they haven't set a default version, then whatever they had
// locally is now the default.
finalDefaultVersion = localVersion
@@ -155,10 +148,10 @@ func NewClientWithDefaultVersion(
// Since ensureVersion might end up downloading terraform,
// we call it asynchronously so as to not delay server startup.
versionsLock.Lock()
_, err := ensureVersion(log, tfDownloader, versions, defaultVersion, binDir, tfDownloadURL, tfDownloadAllowed)
_, err := ensureVersion(log, distribution, versions, defaultVersion, binDir, tfDownloadURL, tfDownloadAllowed)
versionsLock.Unlock()
if err != nil {
log.Err("could not download terraform %s: %s", defaultVersion.String(), err)
log.Err("could not download %s %s: %s", distribution.BinName(), defaultVersion.String(), err)
}
}
@@ -180,10 +173,10 @@ func NewClientWithDefaultVersion(
}
}
return &DefaultClient{
distribution: distribution,
defaultVersion: finalDefaultVersion,
terraformPluginCacheDir: cacheDir,
binDir: binDir,
downloader: tfDownloader,
downloadBaseURL: tfDownloadURL,
downloadAllowed: tfDownloadAllowed,
versionsLock: &versionsLock,
@@ -196,6 +189,7 @@ func NewClientWithDefaultVersion(
func NewTestClient(
log logging.SimpleLogging,
distribution Distribution,
binDir string,
cacheDir string,
tfeToken string,
@@ -203,13 +197,13 @@ func NewTestClient(
defaultVersionStr string,
defaultVersionFlagName string,
tfDownloadURL string,
tfDownloader Downloader,
tfDownloadAllowed bool,
usePluginCache bool,
projectCmdOutputHandler jobs.ProjectCommandOutputHandler,
) (*DefaultClient, error) {
return NewClientWithDefaultVersion(
log,
distribution,
binDir,
cacheDir,
tfeToken,
@@ -217,7 +211,6 @@ func NewTestClient(
defaultVersionStr,
defaultVersionFlagName,
tfDownloadURL,
tfDownloader,
tfDownloadAllowed,
usePluginCache,
false,
@@ -231,10 +224,10 @@ func NewTestClient(
// a specific version is set.
// defaultVersionFlagName is the name of the flag that sets the default terraform
// version.
// tfDownloader is used to download terraform versions.
// Will asynchronously download the required version if it doesn't exist already.
func NewClient(
log logging.SimpleLogging,
distribution Distribution,
binDir string,
cacheDir string,
tfeToken string,
@@ -242,13 +235,13 @@ func NewClient(
defaultVersionStr string,
defaultVersionFlagName string,
tfDownloadURL string,
tfDownloader Downloader,
tfDownloadAllowed bool,
usePluginCache bool,
projectCmdOutputHandler jobs.ProjectCommandOutputHandler,
) (*DefaultClient, error) {
return NewClientWithDefaultVersion(
log,
distribution,
binDir,
cacheDir,
tfeToken,
@@ -256,7 +249,6 @@ func NewClient(
defaultVersionStr,
defaultVersionFlagName,
tfDownloadURL,
tfDownloader,
tfDownloadAllowed,
usePluginCache,
true,
@@ -324,33 +316,12 @@ func (c *DefaultClient) DetectVersion(log logging.SimpleLogging, projectDirector
return version
}
constraintStr := requiredVersionSetting
vc, err := version.NewConstraint(constraintStr)
downloadVersion, err := c.distribution.ResolveConstraint(context.Background(), requiredVersionSetting)
if err != nil {
log.Err("Error parsing constraint string: %s", err)
log.Err("%s", err)
return nil
}
constrainedVersions := &releases.Versions{
Product: product.Terraform,
Constraints: vc,
}
installCandidates, err := constrainedVersions.List(context.Background())
if err != nil {
log.Err("error listing available versions: %s", err)
return nil
}
if len(installCandidates) == 0 {
log.Err("no Terraform versions found for constraints %s", constraintStr)
return nil
}
// We want to select the highest version that satisfies the constraint.
versionDownloader := installCandidates[len(installCandidates)-1]
// Get the Version object from the versionDownloader.
downloadVersion := versionDownloader.(*releases.ExactVersion).Version
return downloadVersion
}
@@ -362,7 +333,7 @@ func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, v *version.Vers
var err error
c.versionsLock.Lock()
_, err = ensureVersion(log, c.downloader, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)
_, err = ensureVersion(log, c.distribution, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)
c.versionsLock.Unlock()
if err != nil {
return err
@@ -442,7 +413,7 @@ func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, w
} else {
var err error
c.versionsLock.Lock()
binPath, err = ensureVersion(log, c.downloader, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)
binPath, err = ensureVersion(log, c.distribution, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)
c.versionsLock.Unlock()
if err != nil {
return "", nil, err
@@ -515,7 +486,7 @@ func MustConstraint(v string) version.Constraints {
// It will download this version if we don't have it.
func ensureVersion(
log logging.SimpleLogging,
dl Downloader,
dist Distribution,
versions map[string]string,
v *version.Version,
binDir string,
@@ -529,7 +500,7 @@ func ensureVersion(
// This tf version might not yet be in the versions map even though it
// exists on disk. This would happen if users have manually added
// terraform{version} binaries. In this case we don't want to re-download.
binFile := "terraform" + v.String()
binFile := dist.BinName() + v.String()
if binPath, err := exec.LookPath(binFile); err == nil {
versions[v.String()] = binPath
return binPath, nil
@@ -544,22 +515,24 @@ func ensureVersion(
}
if !downloadsAllowed {
return "", fmt.Errorf(
"could not find terraform version %s in PATH or %s, and downloads are disabled",
"could not find %s version %s in PATH or %s, and downloads are disabled",
dist.BinName(),
v.String(),
binDir,
)
}
log.Info("could not find terraform version %s in PATH or %s", v.String(), binDir)
log.Info("could not find %s version %s in PATH or %s", dist.BinName(), v.String(), binDir)
log.Info("using Hashicorp's 'hc-install' to download Terraform version %s from download URL %s", v.String(), downloadURL)
execPath, err := dl.Install(binDir, downloadURL, v)
log.Info("downloading %s version %s from download URL %s", dist.BinName(), v.String(), downloadURL)
execPath, err := dist.Downloader().Install(context.Background(), binDir, downloadURL, v)
if err != nil {
return "", errors.Wrapf(err, "error downloading terraform version %s", v.String())
}
log.Info("Downloaded terraform %s to %s", v.String(), execPath)
log.Info("Downloaded %s %s to %s", dist.BinName(), v.String(), execPath)
versions[v.String()] = execPath
return execPath, nil
}
@@ -622,35 +595,3 @@ func getVersion(tfBinary string) (*version.Version, error) {
var rcFileContents = `credentials "%s" {
token = %q
}`
type DefaultDownloader struct{}
func (d *DefaultDownloader) Install(dir string, downloadURL string, v *version.Version) (string, error) {
installer := install.NewInstaller()
execPath, err := installer.Install(context.Background(), []src.Installable{
&releases.ExactVersion{
Product: product.Terraform,
Version: v,
InstallDir: dir,
ApiBaseURL: downloadURL,
},
})
if err != nil {
return "", err
}
// hc-install installs terraform binary as just "terraform".
// We need to rename it to terraform{version} to be consistent with current naming convention.
newPath := filepath.Join(dir, "terraform"+v.String())
if err := os.Rename(execPath, newPath); err != nil {
return "", err
}
return newPath, nil
}
// See go-getter.GetAny.
func (d *DefaultDownloader) GetAny(dst, src string) error {
_, err := getter.GetAny(context.Background(), dst, src)
return err
}

View File

@@ -14,6 +14,7 @@
package terraform_test
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -24,7 +25,6 @@ import (
version "github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
pegomock "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/cmd"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
@@ -77,7 +77,10 @@ is 0.11.13. You can update by downloading from developer.hashicorp.com/terraform
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, true, projectCmdOutputHandler)
mockDownloader := mocks.NewMockDownloader()
distibution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logger, distibution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
@@ -111,7 +114,10 @@ is 0.11.13. You can update by downloading from developer.hashicorp.com/terraform
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, true, projectCmdOutputHandler)
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
@@ -132,7 +138,10 @@ func TestNewClient_NoTF(t *testing.T) {
// Set PATH to only include our empty directory.
defer tempSetEnv(t, "PATH", tmp)()
_, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, true, projectCmdOutputHandler)
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
_, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
ErrEquals(t, "terraform not found in $PATH. Set --default-tf-version or download terraform from https://developer.hashicorp.com/terraform/downloads", err)
}
@@ -155,7 +164,10 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) {
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, false, true, projectCmdOutputHandler)
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, false, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
@@ -183,7 +195,10 @@ func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) {
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()
c, err := terraform.NewClient(logging.NewNoopLogger(t), binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, true, projectCmdOutputHandler)
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logging.NewNoopLogger(t), distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
@@ -211,18 +226,19 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
defer tempSetEnv(t, "PATH", "")()
mockDownloader := mocks.NewMockDownloader()
When(mockDownloader.Install(Any[string](), Any[string](), Any[*version.Version]())).Then(func(params []pegomock.Param) pegomock.ReturnValues {
binPath := filepath.Join(params[0].(string), "terraform0.11.10")
When(mockDownloader.Install(Any[context.Context](), Any[string](), Any[string](), Any[*version.Version]())).Then(func(params []Param) ReturnValues {
binPath := filepath.Join(params[1].(string), "terraform0.11.10")
err := os.WriteFile(binPath, []byte("#!/bin/sh\necho '\nTerraform v0.11.10\n'"), 0700) // #nosec G306
return []pegomock.ReturnValue{binPath, err}
return []ReturnValue{binPath, err}
})
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true, true, projectCmdOutputHandler)
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
mockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(binDir, cmd.DefaultTFDownloadURL, version.Must(version.NewVersion("0.11.10")))
mockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, version.Must(version.NewVersion("0.11.10")))
// Reset PATH so that it has sh.
Ok(t, os.Setenv("PATH", orig))
@@ -237,7 +253,9 @@ func TestNewClient_BadVersion(t *testing.T) {
logger := logging.NewNoopLogger(t)
_, binDir, cacheDir := mkSubDirs(t)
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
_, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, true, projectCmdOutputHandler)
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
_, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
ErrEquals(t, "Malformed version: malformed", err)
}
@@ -257,14 +275,15 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) {
Ok(t, err)
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
// Set up our mock downloader to write a fake tf binary when it's called.
When(mockDownloader.Install(binDir, cmd.DefaultTFDownloadURL, v)).Then(func(params []pegomock.Param) pegomock.ReturnValues {
binPath := filepath.Join(params[0].(string), "terraform99.99.99")
When(mockDownloader.Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)).Then(func(params []Param) ReturnValues {
binPath := filepath.Join(params[1].(string), "terraform99.99.99")
err := os.WriteFile(binPath, []byte("#!/bin/sh\necho '\nTerraform v99.99.99\n'"), 0700) // #nosec G306
return []pegomock.ReturnValue{binPath, err}
return []ReturnValue{binPath, err}
})
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true, true, projectCmdOutputHandler)
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
@@ -282,8 +301,10 @@ func TestEnsureVersion_downloaded(t *testing.T) {
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
mockDownloader := mocks.NewMockDownloader()
distibution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
downloadsAllowed := true
c, err := terraform.NewTestClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, downloadsAllowed, true, projectCmdOutputHandler)
c, err := terraform.NewTestClient(logger, distibution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
@@ -291,17 +312,17 @@ func TestEnsureVersion_downloaded(t *testing.T) {
v, err := version.NewVersion("99.99.99")
Ok(t, err)
When(mockDownloader.Install(binDir, cmd.DefaultTFDownloadURL, v)).Then(func(params []pegomock.Param) pegomock.ReturnValues {
binPath := filepath.Join(params[0].(string), "terraform99.99.99")
When(mockDownloader.Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)).Then(func(params []Param) ReturnValues {
binPath := filepath.Join(params[1].(string), "terraform99.99.99")
err := os.WriteFile(binPath, []byte("#!/bin/sh\necho '\nTerraform v99.99.99\n'"), 0700) // #nosec G306
return []pegomock.ReturnValue{binPath, err}
return []ReturnValue{binPath, err}
})
err = c.EnsureVersion(logger, v)
Ok(t, err)
mockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(binDir, cmd.DefaultTFDownloadURL, v)
mockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)
}
// Test that EnsureVersion downloads terraform from a custom URL.
@@ -312,10 +333,11 @@ func TestEnsureVersion_downloaded_customURL(t *testing.T) {
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
downloadsAllowed := true
customURL := "http://releases.example.com"
c, err := terraform.NewTestClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, customURL, mockDownloader, downloadsAllowed, true, projectCmdOutputHandler)
c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, customURL, downloadsAllowed, true, projectCmdOutputHandler)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
@@ -323,17 +345,17 @@ func TestEnsureVersion_downloaded_customURL(t *testing.T) {
v, err := version.NewVersion("99.99.99")
Ok(t, err)
When(mockDownloader.Install(binDir, customURL, v)).Then(func(params []pegomock.Param) pegomock.ReturnValues {
binPath := filepath.Join(params[0].(string), "terraform99.99.99")
When(mockDownloader.Install(context.Background(), binDir, customURL, v)).Then(func(params []Param) ReturnValues {
binPath := filepath.Join(params[1].(string), "terraform99.99.99")
err := os.WriteFile(binPath, []byte("#!/bin/sh\necho '\nTerraform v99.99.99\n'"), 0700) // #nosec G306
return []pegomock.ReturnValue{binPath, err}
return []ReturnValue{binPath, err}
})
err = c.EnsureVersion(logger, v)
Ok(t, err)
mockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(binDir, customURL, v)
mockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(context.Background(), binDir, customURL, v)
}
// Test that EnsureVersion throws an error when downloads are disabled
@@ -344,9 +366,10 @@ func TestEnsureVersion_downloaded_downloadingDisabled(t *testing.T) {
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
downloadsAllowed := false
c, err := terraform.NewTestClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, downloadsAllowed, true, projectCmdOutputHandler)
c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
@@ -475,9 +498,12 @@ terraform {
RegisterMockTestingT(t)
_, binDir, cacheDir := mkSubDirs(t)
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
mockDownloader := mocks.NewMockDownloader()
c, err := terraform.NewTestClient(logger,
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewTestClient(
logger,
distribution,
binDir,
cacheDir,
"",
@@ -485,7 +511,6 @@ terraform {
"",
cmd.DefaultTFVersionFlag,
cmd.DefaultTFDownloadURL,
mockDownloader,
downloadsAllowed,
true,
projectCmdOutputHandler)
@@ -515,32 +540,15 @@ terraform {
}
}
func TestInstall(t *testing.T) {
d := &terraform.DefaultDownloader{}
RegisterMockTestingT(t)
_, binDir, _ := mkSubDirs(t)
v, _ := version.NewVersion("1.8.1")
newPath, err := d.Install(binDir, cmd.DefaultTFDownloadURL, v)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if _, err := os.Stat(newPath); os.IsNotExist(err) {
t.Errorf("Binary not found at %s", newPath)
}
}
func TestExtractExactRegex(t *testing.T) {
logger := logging.NewNoopLogger(t)
RegisterMockTestingT(t)
_, binDir, cacheDir := mkSubDirs(t)
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewTestClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true, true, projectCmdOutputHandler)
c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
tests := []struct {

View File

@@ -421,8 +421,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
)
}
distribution := terraform.NewDistributionTerraform()
if userConfig.TFDistribution == "opentofu" {
distribution = terraform.NewDistributionOpenTofu()
}
terraformClient, err := terraform.NewClient(
logger,
distribution,
binDir,
cacheDir,
userConfig.TFEToken,
@@ -430,7 +436,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
userConfig.DefaultTFVersion,
config.DefaultTFVersionFlag,
userConfig.TFDownloadURL,
&terraform.DefaultDownloader{},
userConfig.TFDownload,
userConfig.UseTFPluginCache,
projectCmdOutputHandler)
@@ -438,7 +443,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
// are, then we don't error out because we don't have/want terraform
// installed on our CI system where the unit tests run.
if err != nil && flag.Lookup("test.v") == nil {
return nil, errors.Wrap(err, "initializing terraform")
return nil, errors.Wrap(err, fmt.Sprintf("initializing %s", userConfig.TFDistribution))
}
markdownRenderer := events.NewMarkdownRenderer(
gitlabClient.SupportsCommonMark(),
@@ -635,7 +640,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
policyCheckStepRunner, err := runtime.NewPolicyCheckStepRunner(
defaultTfVersion,
policy.NewConfTestExecutorWorkflow(logger, binDir, &terraform.DefaultDownloader{}),
policy.NewConfTestExecutorWorkflow(logger, binDir, &policy.ConfTestGoGetterVersionDownloader{}),
)
if err != nil {

View File

@@ -107,6 +107,7 @@ type UserConfig struct {
SSLCertFile string `mapstructure:"ssl-cert-file"`
SSLKeyFile string `mapstructure:"ssl-key-file"`
RestrictFileList bool `mapstructure:"restrict-file-list"`
TFDistribution string `mapstructure:"tf-distribution"`
TFDownload bool `mapstructure:"tf-download"`
TFDownloadURL string `mapstructure:"tf-download-url"`
TFEHostname string `mapstructure:"tfe-hostname"`