Skip to content

Commit edf056b

Browse files
authored
test: add mocked terraform installation files (#20757)
Adds mocked terraform installation files and uses them in provisioner/terraform.TestInstall Fixes: coder/internal#72
1 parent 9ca5b44 commit edf056b

File tree

3 files changed

+190
-10
lines changed

3 files changed

+190
-10
lines changed

provisioner/terraform/install.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ var (
3434
// operation.
3535
//
3636
//nolint:revive // verbose is a control flag that controls the verbosity of the log output.
37-
func Install(ctx context.Context, log slog.Logger, verbose bool, dir string, wantVersion *version.Version) (string, error) {
37+
func Install(ctx context.Context, log slog.Logger, verbose bool, dir string, wantVersion *version.Version, baseUrl string, verifyChecksums bool) (string, error) {
3838
err := os.MkdirAll(dir, 0o750)
3939
if err != nil {
4040
return "", err
@@ -63,11 +63,15 @@ func Install(ctx context.Context, log slog.Logger, verbose bool, dir string, wan
6363
}
6464

6565
installer := &releases.ExactVersion{
66-
InstallDir: dir,
67-
Product: product.Terraform,
68-
Version: TerraformVersion,
66+
InstallDir: dir,
67+
Product: product.Terraform,
68+
Version: TerraformVersion,
69+
SkipChecksumVerification: !verifyChecksums,
6970
}
7071
installer.SetLogger(slog.Stdlib(ctx, log, slog.LevelDebug))
72+
if baseUrl != "" {
73+
installer.ApiBaseURL = baseUrl
74+
}
7175

7276
logInstall := log.Debug
7377
if verbose {

provisioner/terraform/install_test.go

Lines changed: 181 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
package terraform_test
77

88
import (
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+
23199
func 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

provisioner/terraform/serve.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func Serve(ctx context.Context, options *ServeOptions) error {
103103
slog.F("min_version", minTerraformVersion.String()))
104104
}
105105

106-
binPath, err := Install(ctx, options.Logger, options.ExternalProvisioner, options.CachePath, TerraformVersion)
106+
binPath, err := Install(ctx, options.Logger, options.ExternalProvisioner, options.CachePath, TerraformVersion, "", true)
107107
if err != nil {
108108
return xerrors.Errorf("install terraform: %w", err)
109109
}

0 commit comments

Comments
 (0)