66package terraform_test
77
88import (
9+ "archive/zip"
910 "context"
11+ "encoding/json"
12+ "fmt"
13+ "net"
14+ "net/http"
1015 "os"
16+ "path/filepath"
17+ "strings"
1118 "sync"
1219 "testing"
1320 "time"
@@ -20,6 +27,175 @@ import (
2027 "github.com/coder/coder/v2/testutil"
2128)
2229
30+ const (
31+ // simple script that mocks `./terraform version -json`
32+ terraformExecutableTemplate = `#!/bin/bash
33+ cat <<EOF
34+ {
35+ "terraform_version": "${ver}",
36+ "platform": "linux_amd64",
37+ "provider_selections": {},
38+ "terraform_outdated": true
39+ }
40+ EOF
41+ `
42+ )
43+
44+ var (
45+ version1 = terraform .TerraformVersion
46+ version2 = version .Must (version .NewVersion ("1.2.0" ))
47+ )
48+
49+ type productBuild struct {
50+ Name string `json:"name"`
51+ Version string `json:"version"`
52+ OS string `json:"os"`
53+ Arch string `json:"arch"`
54+ Filename string `json:"filename"`
55+ URL string `json:"url"`
56+ }
57+
58+ type productVersion struct {
59+ Name string `json:"name"`
60+ Version * version.Version `json:"version"`
61+ Builds []productBuild `json:"builds"`
62+ }
63+
64+ type product struct {
65+ Name string `json:"name"`
66+ Versions map [string ]productVersion `json:"versions"`
67+ }
68+
69+ func zipFilename (v * version.Version ) string {
70+ return fmt .Sprintf ("terraform_%s_linux_amd64.zip" , v )
71+ }
72+
73+ // returns `/${version}/index.json` in struct format
74+ func versionedJSON (v * version.Version ) productVersion {
75+ return productVersion {
76+ Name : "terraform" ,
77+ Version : v ,
78+ Builds : []productBuild {
79+ {
80+ Arch : "amd64" ,
81+ Filename : zipFilename (v ),
82+ Name : "terraform" ,
83+ OS : "linux" ,
84+ URL : fmt .Sprintf ("/terraform/%s/%s" , v , zipFilename (v )),
85+ Version : v .String (),
86+ },
87+ },
88+ }
89+ }
90+
91+ // returns `/index.json` in struct format
92+ func mainJSON (versions ... * version.Version ) product {
93+ vj := map [string ]productVersion {}
94+ for _ , v := range versions {
95+ vj [v .String ()] = versionedJSON (v )
96+ }
97+ mj := product {
98+ Name : "terraform" ,
99+ Versions : vj ,
100+ }
101+ return mj
102+ }
103+
104+ func exeContent (v * version.Version ) []byte {
105+ return []byte (strings .ReplaceAll (terraformExecutableTemplate , "${ver}" , v .String ()))
106+ }
107+
108+ func mustMarshal (t * testing.T , obj any ) []byte {
109+ b , err := json .Marshal (obj )
110+ require .NoError (t , err )
111+ return b
112+ }
113+
114+ // Mock files are based on https://releases.hashicorp.com/terraform
115+ // mock directory structure:
116+ //
117+ // ${tmpDir}/index.json
118+ // ${tmpDir}/${version}/index.json
119+ // ${tmpDir}/${version}/terraform_${version}_linux_amd64.zip
120+ // -> zip contains 'terraform' binary and sometimes 'LICENSE.txt'
121+ func createFakeTerraformInstallationFiles (t * testing.T ) string {
122+ tmpDir := t .TempDir ()
123+
124+ mij := mustMarshal (t , mainJSON (version1 , version2 ))
125+ jv1 := mustMarshal (t , versionedJSON (version1 ))
126+ jv2 := mustMarshal (t , versionedJSON (version2 ))
127+
128+ // `index.json`
129+ require .NoError (t , os .WriteFile (filepath .Join (tmpDir , "index.json" ), mij , 0o400 ))
130+
131+ // `${version1}/index.json`
132+ require .NoError (t , os .Mkdir (filepath .Join (tmpDir , version1 .String ()), 0o700 ))
133+ require .NoError (t , os .WriteFile (filepath .Join (tmpDir , version1 .String (), "index.json" ), jv1 , 0o400 ))
134+
135+ // `${version2}/index.json`
136+ require .NoError (t , os .Mkdir (filepath .Join (tmpDir , version2 .String ()), 0o700 ))
137+ require .NoError (t , os .WriteFile (filepath .Join (tmpDir , version2 .String (), "index.json" ), jv2 , 0o400 ))
138+
139+ // `${version1}/linux_amd64.zip`
140+ zip1 , err := os .Create (filepath .Join (tmpDir , version1 .String (), zipFilename (version1 )))
141+ require .NoError (t , err )
142+ zip1Writer := zip .NewWriter (zip1 )
143+
144+ // `${version1}/linux_amd64.zip/terraform`
145+ exe1 , err := zip1Writer .Create ("terraform" )
146+ require .NoError (t , err )
147+ n , err := exe1 .Write (exeContent (version1 ))
148+ require .NoError (t , err )
149+ require .NotZero (t , n )
150+
151+ // `${version1}/linux_amd64.zip/LICENSE.txt`
152+ lic1 , err := zip1Writer .Create ("LICENSE.txt" )
153+ require .NoError (t , err )
154+ n , err = lic1 .Write ([]byte ("some license" ))
155+ require .NoError (t , err )
156+ require .NotZero (t , n )
157+ require .NoError (t , zip1Writer .Close ())
158+
159+ // `${version2}/linux_amd64.zip`
160+ zip2 , err := os .Create (filepath .Join (tmpDir , version2 .String (), zipFilename (version2 )))
161+ require .NoError (t , err )
162+ zip2Writer := zip .NewWriter (zip2 )
163+
164+ // `${version1}/linux_amd64.zip/terraform`
165+ exe2 , err := zip2Writer .Create ("terraform" )
166+ require .NoError (t , err )
167+ n , err = exe2 .Write (exeContent (version2 ))
168+ require .NoError (t , err )
169+ require .NotZero (t , n )
170+ require .NoError (t , zip2Writer .Close ())
171+
172+ return tmpDir
173+ }
174+
175+ // starts http server serving fake terraform installation files
176+ func startFakeTerraformServer (t * testing.T , tmpDir string ) string {
177+ listener , err := net .Listen ("tcp" , "127.0.0.1:0" )
178+ if err != nil {
179+ t .Fatalf ("failed to create listener" )
180+ }
181+
182+ mux := http .NewServeMux ()
183+ fs := http .FileServer (http .Dir (tmpDir ))
184+ mux .Handle ("/terraform/" , http .StripPrefix ("/terraform" , fs ))
185+
186+ srv := http.Server {
187+ ReadHeaderTimeout : time .Second ,
188+ Handler : mux ,
189+ }
190+ go srv .Serve (listener )
191+ t .Cleanup (func () {
192+ if err := srv .Close (); err != nil {
193+ t .Errorf ("failed to close server: %v" , err )
194+ }
195+ })
196+ return "http://" + listener .Addr ().String ()
197+ }
198+
23199func TestInstall (t * testing.T ) {
24200 t .Parallel ()
25201 if testing .Short () {
@@ -29,6 +205,9 @@ func TestInstall(t *testing.T) {
29205 dir := t .TempDir ()
30206 log := testutil .Logger (t )
31207
208+ tmpDir := createFakeTerraformInstallationFiles (t )
209+ addr := startFakeTerraformServer (t , tmpDir )
210+
32211 // Install spins off 8 installs with Version and waits for them all
33212 // to complete. The locking mechanism within Install should
34213 // prevent multiple binaries from being installed, so the function
@@ -40,7 +219,7 @@ func TestInstall(t *testing.T) {
40219 wg .Add (1 )
41220 go func () {
42221 defer wg .Done ()
43- p , err := terraform .Install (ctx , log , false , dir , version )
222+ p , err := terraform .Install (ctx , log , false , dir , version , addr , false )
44223 assert .NoError (t , err )
45224 paths <- p
46225 }()
@@ -60,7 +239,6 @@ func TestInstall(t *testing.T) {
60239 return firstPath
61240 }
62241
63- version1 := terraform .TerraformVersion
64242 binPath := install (version1 )
65243
66244 checkBinModTime := func () time.Time {
@@ -73,13 +251,11 @@ func TestInstall(t *testing.T) {
73251 modTime1 := checkBinModTime ()
74252
75253 // Since we're using the same version the install should be idempotent.
76- install (terraform . TerraformVersion )
254+ install (version1 )
77255 modTime2 := checkBinModTime ()
78256 require .Equal (t , modTime1 , modTime2 )
79257
80258 // Ensure a new install happens when version changes
81- version2 := version .Must (version .NewVersion ("1.2.0" ))
82-
83259 // Sanity-check
84260 require .NotEqual (t , version2 .String (), version1 .String ())
85261
0 commit comments