66package terraform_test
77
88import (
9- "archive/zip"
109"context"
11- "encoding/json "
12- "fmt "
10+ "errors "
11+ "io "
1312"net"
1413"net/http"
14+ "net/url"
1515"os"
1616"path/filepath"
1717"strings"
@@ -28,172 +28,93 @@ import (
2828)
2929
3030const (
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- `
31+ cacheSubDir = "terraform_install_test"
32+ terraformURL = "https://releases.hashicorp.com"
4233)
4334
4435var (
4536version1 = terraform .TerraformVersion
4637version2 = version .Must (version .NewVersion ("1.2.0" ))
4738)
4839
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"`
40+ type terraformProxy struct {
41+ t * testing.T
42+ cacheRoot string
43+ listener net.Listener
44+ srv * http.Server
45+ fsHandler http.Handler
46+ httpClient * http.Client
47+ mutex * sync.Mutex
6748}
6849
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- },
50+ // Simple cached proxy for terraform files.
51+ // Serves files from persistent cache or forwards requests to releases.hashicorp.com
52+ // Modifies downloaded index.json files so they point to proxy.
53+ func persistentlyCachedProxy (t * testing.T )* terraformProxy {
54+ cacheRoot := filepath .Join (testutil .PersistentCacheDir (t ),cacheSubDir )
55+ proxy := terraformProxy {
56+ t :t ,
57+ mutex :& sync.Mutex {},
58+ cacheRoot :cacheRoot ,
59+ fsHandler :http .FileServer (http .Dir (cacheRoot )),
60+ httpClient :& http.Client {},
8861}
89- }
9062
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 ,
63+ listener ,err := net .Listen ("tcp" ,"127.0.0.1:0" )
64+ if err != nil {
65+ t .Fatalf ("failed to create listener" )
10066}
101- return mj
102- }
67+ proxy .listener = listener
10368
104- func exeContent (v * version.Version ) []byte {
105- return []byte (strings .ReplaceAll (terraformExecutableTemplate ,"${ver}" ,v .String ()))
106- }
69+ m := http .NewServeMux ()
70+ m .HandleFunc ("GET /" ,proxy .handleGet )
10771
108- func mustMarshal (t * testing.T ,obj any ) []byte {
109- b ,err := json .Marshal (obj )
110- require .NoError (t ,err )
111- return b
72+ proxy .srv = & http.Server {
73+ WriteTimeout :30 * time .Second ,
74+ ReadTimeout :30 * time .Second ,
75+ Handler :m ,
76+ }
77+ return & proxy
11278}
11379
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 )
80+ func uriToFilename (u url.URL )string {
81+ return strings .ReplaceAll (u .RequestURI (),"/" ,"_" )
82+ }
14383
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 )
84+ func (p * terraformProxy )handleGet (w http.ResponseWriter ,r * http.Request ) {
85+ p .mutex .Lock ()
86+ defer p .mutex .Unlock ()
15087
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 ())
88+ filename := uriToFilename (* r .URL )
89+ path := filepath .Join (p .cacheRoot ,filename )
90+ if _ ,err := os .Stat (path );errors .Is (err ,os .ErrNotExist ) {
91+ require .NoError (p .t ,os .MkdirAll (p .cacheRoot ,os .ModeDir | 0o700 ))
15892
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 )
93+ // Update cache
94+ req ,err := http .NewRequestWithContext (p .t .Context (),"GET" ,terraformURL + r .URL .Path ,nil )
95+ require .NoError (p .t ,err )
16396
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 ())
97+ resp ,err := p .httpClient .Do (req )
98+ require .NoError (p .t ,err )
99+ defer resp .Body .Close ()
171100
172- return tmpDir
173- }
101+ body , err := io . ReadAll ( resp . Body )
102+ require . NoError ( p . t , err )
174103
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" )
104+ // update index.json so urls in it point to proxy by making them relative
105+ // "https://releases.hashicorp.com/terraform/1.13.4/terraform_1.13.4_windows_amd64.zip" -> "/terraform/1.13.4/terraform_1.13.4_windows_amd64.zip"
106+ if strings .HasSuffix (r .URL .Path ,"index.json" ) {
107+ body = []byte (strings .ReplaceAll (string (body ),terraformURL ,"" ))
108+ }
109+ require .NoError (p .t ,os .WriteFile (path ,body ,0o400 ))
110+ }else if err != nil {
111+ p .t .Errorf ("unexpected error when trying to read file from cache: %v" ,err )
180112}
181113
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 ()
114+ // Serve from cache
115+ r .URL .Path = filename
116+ r .URL .RawPath = filename
117+ p .fsHandler .ServeHTTP (w ,r )
197118}
198119
199120func TestInstall (t * testing.T ) {
@@ -205,8 +126,11 @@ func TestInstall(t *testing.T) {
205126dir := t .TempDir ()
206127log := testutil .Logger (t )
207128
208- tmpDir := createFakeTerraformInstallationFiles (t )
209- addr := startFakeTerraformServer (t ,tmpDir )
129+ proxy := persistentlyCachedProxy (t )
130+ go proxy .srv .Serve (proxy .listener )
131+ t .Cleanup (func () {
132+ require .NoError (t ,proxy .srv .Close ())
133+ })
210134
211135// Install spins off 8 installs with Version and waits for them all
212136// to complete. The locking mechanism within Install should
@@ -219,7 +143,7 @@ func TestInstall(t *testing.T) {
219143wg .Add (1 )
220144go func () {
221145defer wg .Done ()
222- p ,err := terraform .Install (ctx ,log ,false ,dir ,version ,addr , false )
146+ p ,err := terraform .Install (ctx ,log ,false ,dir ,version ,"http://" + proxy . listener . Addr (). String () )
223147assert .NoError (t ,err )
224148paths <- p
225149}()