Skip to content

Commit adf14f1

Browse files
authored
chore(cli): warn on template push or create when no lockfile present (#8059)
1 parent a47a9b1 commit adf14f1

File tree

11 files changed

+272
-8
lines changed

11 files changed

+272
-8
lines changed

cli/clitest/clitest.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"context"
77
"errors"
88
"io"
9-
"io/ioutil"
109
"os"
1110
"path/filepath"
1211
"strings"
@@ -86,7 +85,10 @@ func SetupConfig(t *testing.T, client *codersdk.Client, root config.Root) {
8685
// new temporary testing directory.
8786
func CreateTemplateVersionSource(t *testing.T, responses *echo.Responses) string {
8887
directory := t.TempDir()
89-
f, err := ioutil.TempFile(directory, "*.tf")
88+
f, err := os.CreateTemp(directory, "*.tf")
89+
require.NoError(t, err)
90+
_ = f.Close()
91+
f, err = os.Create(filepath.Join(directory, ".terraform.lock.hcl"))
9092
require.NoError(t, err)
9193
_ = f.Close()
9294
data, err := echo.Tar(responses)

cli/templatecreate.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
8787
return xerrors.Errorf("A template already exists named %q!", templateName)
8888
}
8989

90+
err = uploadFlags.checkForLockfile(inv)
91+
if err != nil {
92+
return xerrors.Errorf("check for lockfile: %w", err)
93+
}
94+
9095
// Confirm upload of the directory.
9196
resp, err := uploadFlags.upload(inv, client)
9297
if err != nil {
@@ -185,7 +190,6 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
185190
Default: "0h",
186191
Value: clibase.DurationOf(&inactivityTTL),
187192
},
188-
uploadFlags.option(),
189193
{
190194
Flag: "test.provisioner",
191195
Description: "Customize the provisioner backend.",
@@ -195,6 +199,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
195199
},
196200
cliui.SkipPromptOption(),
197201
}
202+
cmd.Options = append(cmd.Options, uploadFlags.options()...)
198203
return cmd
199204
}
200205

cli/templatecreate_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"os"
7+
"path/filepath"
78
"testing"
89

910
"github.com/stretchr/testify/assert"
@@ -80,6 +81,87 @@ func TestTemplateCreate(t *testing.T) {
8081
}
8182
}
8283
})
84+
t.Run("CreateNoLockfile", func(t *testing.T) {
85+
t.Parallel()
86+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
87+
coderdtest.CreateFirstUser(t, client)
88+
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
89+
Parse: echo.ParseComplete,
90+
ProvisionApply: provisionCompleteWithAgent,
91+
})
92+
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
93+
args := []string{
94+
"templates",
95+
"create",
96+
"my-template",
97+
"--directory", source,
98+
"--test.provisioner", string(database.ProvisionerTypeEcho),
99+
"--default-ttl", "24h",
100+
}
101+
inv, root := clitest.New(t, args...)
102+
clitest.SetupConfig(t, client, root)
103+
pty := ptytest.New(t).Attach(inv)
104+
105+
execDone := make(chan error)
106+
go func() {
107+
execDone <- inv.Run()
108+
}()
109+
110+
matches := []struct {
111+
match string
112+
write string
113+
}{
114+
{match: "No .terraform.lock.hcl file found"},
115+
{match: "Upload", write: "no"},
116+
}
117+
for _, m := range matches {
118+
pty.ExpectMatch(m.match)
119+
if len(m.write) > 0 {
120+
pty.WriteLine(m.write)
121+
}
122+
}
123+
124+
// cmd should error once we say no.
125+
require.Error(t, <-execDone)
126+
})
127+
t.Run("CreateNoLockfileIgnored", func(t *testing.T) {
128+
t.Parallel()
129+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
130+
coderdtest.CreateFirstUser(t, client)
131+
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
132+
Parse: echo.ParseComplete,
133+
ProvisionApply: provisionCompleteWithAgent,
134+
})
135+
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
136+
args := []string{
137+
"templates",
138+
"create",
139+
"my-template",
140+
"--directory", source,
141+
"--test.provisioner", string(database.ProvisionerTypeEcho),
142+
"--default-ttl", "24h",
143+
"--ignore-lockfile",
144+
}
145+
inv, root := clitest.New(t, args...)
146+
clitest.SetupConfig(t, client, root)
147+
pty := ptytest.New(t).Attach(inv)
148+
149+
execDone := make(chan error)
150+
go func() {
151+
execDone <- inv.Run()
152+
}()
153+
154+
{
155+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
156+
defer cancel()
157+
158+
pty.ExpectNoMatchBefore(ctx, "No .terraform.lock.hcl file found", "Upload")
159+
pty.WriteLine("no")
160+
}
161+
162+
// cmd should error once we say no.
163+
require.Error(t, <-execDone)
164+
})
83165

84166
t.Run("CreateStdin", func(t *testing.T) {
85167
t.Parallel()

cli/templatepush.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@ import (
1919

2020
// templateUploadFlags is shared by `templates create` and `templates push`.
2121
type templateUploadFlags struct {
22-
directory string
22+
directory string
23+
ignoreLockfile bool
2324
}
2425

25-
func (pf *templateUploadFlags) option() clibase.Option {
26-
return clibase.Option{
26+
func (pf *templateUploadFlags) options() []clibase.Option {
27+
return []clibase.Option{{
2728
Flag: "directory",
2829
FlagShorthand: "d",
2930
Description: "Specify the directory to create from, use '-' to read tar from stdin.",
3031
Default: ".",
3132
Value: clibase.StringOf(&pf.directory),
32-
}
33+
}, {
34+
Flag: "ignore-lockfile",
35+
Description: "Ignore warnings about not having a .terraform.lock.hcl file present in the template.",
36+
Default: "false",
37+
Value: clibase.BoolOf(&pf.ignoreLockfile),
38+
}}
3339
}
3440

3541
func (pf *templateUploadFlags) setWorkdir(wd string) {
@@ -84,6 +90,26 @@ func (pf *templateUploadFlags) upload(inv *clibase.Invocation, client *codersdk.
8490
return &resp, nil
8591
}
8692

93+
func (pf *templateUploadFlags) checkForLockfile(inv *clibase.Invocation) error {
94+
if pf.stdin() || pf.ignoreLockfile {
95+
// Just assume there's a lockfile if reading from stdin.
96+
return nil
97+
}
98+
99+
hasLockfile, err := provisionersdk.DirHasLockfile(pf.directory)
100+
if err != nil {
101+
return xerrors.Errorf("dir has lockfile: %w", err)
102+
}
103+
104+
if !hasLockfile {
105+
cliui.Warn(inv.Stdout, "No .terraform.lock.hcl file found",
106+
"When provisioning, Coder will be unable to cache providers without a lockfile and must download them from the internet each time.",
107+
"Create one by running "+cliui.DefaultStyles.Code.Render("terraform init")+" in your template directory.",
108+
)
109+
}
110+
return nil
111+
}
112+
87113
func (pf *templateUploadFlags) templateName(args []string) (string, error) {
88114
if pf.stdin() {
89115
// Can't infer name from directory if none provided.
@@ -143,6 +169,11 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
143169
return err
144170
}
145171

172+
err = uploadFlags.checkForLockfile(inv)
173+
if err != nil {
174+
return xerrors.Errorf("check for lockfile: %w", err)
175+
}
176+
146177
resp, err := uploadFlags.upload(inv, client)
147178
if err != nil {
148179
return err
@@ -236,7 +267,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
236267
Value: clibase.BoolOf(&activate),
237268
},
238269
cliui.SkipPromptOption(),
239-
uploadFlags.option(),
240270
}
271+
cmd.Options = append(cmd.Options, uploadFlags.options()...)
241272
return cmd
242273
}

cli/templatepush_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/coder/coder/provisioner/echo"
1919
"github.com/coder/coder/provisionersdk/proto"
2020
"github.com/coder/coder/pty/ptytest"
21+
"github.com/coder/coder/testutil"
2122
)
2223

2324
func TestTemplatePush(t *testing.T) {
@@ -69,6 +70,86 @@ func TestTemplatePush(t *testing.T) {
6970
require.Equal(t, "example", templateVersions[1].Name)
7071
})
7172

73+
t.Run("NoLockfile", func(t *testing.T) {
74+
t.Parallel()
75+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
76+
user := coderdtest.CreateFirstUser(t, client)
77+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
78+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
79+
80+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
81+
82+
// Test the cli command.
83+
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
84+
Parse: echo.ParseComplete,
85+
ProvisionApply: echo.ProvisionComplete,
86+
})
87+
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
88+
89+
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example")
90+
clitest.SetupConfig(t, client, root)
91+
pty := ptytest.New(t).Attach(inv)
92+
93+
execDone := make(chan error)
94+
go func() {
95+
execDone <- inv.Run()
96+
}()
97+
98+
matches := []struct {
99+
match string
100+
write string
101+
}{
102+
{match: "No .terraform.lock.hcl file found"},
103+
{match: "Upload", write: "no"},
104+
}
105+
for _, m := range matches {
106+
pty.ExpectMatch(m.match)
107+
if m.write != "" {
108+
pty.WriteLine(m.write)
109+
}
110+
}
111+
112+
// cmd should error once we say no.
113+
require.Error(t, <-execDone)
114+
})
115+
116+
t.Run("NoLockfileIgnored", func(t *testing.T) {
117+
t.Parallel()
118+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
119+
user := coderdtest.CreateFirstUser(t, client)
120+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
121+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
122+
123+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
124+
125+
// Test the cli command.
126+
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
127+
Parse: echo.ParseComplete,
128+
ProvisionApply: echo.ProvisionComplete,
129+
})
130+
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
131+
132+
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example", "--ignore-lockfile")
133+
clitest.SetupConfig(t, client, root)
134+
pty := ptytest.New(t).Attach(inv)
135+
136+
execDone := make(chan error)
137+
go func() {
138+
execDone <- inv.Run()
139+
}()
140+
141+
{
142+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
143+
defer cancel()
144+
145+
pty.ExpectNoMatchBefore(ctx, "No .terraform.lock.hcl file found", "Upload")
146+
pty.WriteLine("no")
147+
}
148+
149+
// cmd should error once we say no.
150+
require.Error(t, <-execDone)
151+
})
152+
72153
t.Run("PushInactiveTemplateVersion", func(t *testing.T) {
73154
t.Parallel()
74155
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})

cli/testdata/coder_templates_create_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Create a template from the current directory or as specified by flag
1313
Specify a failure TTL for workspaces created from this template. This
1414
licensed feature's default is 0h (off).
1515

16+
--ignore-lockfile bool (default: false)
17+
Ignore warnings about not having a .terraform.lock.hcl file present in
18+
the template.
19+
1620
--inactivity-ttl duration (default: 0h)
1721
Specify an inactivity TTL for workspaces created from this template.
1822
This licensed feature's default is 0h (off).

cli/testdata/coder_templates_push_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Push a new template version from the current directory or as specified by flag
1313
-d, --directory string (default: .)
1414
Specify the directory to create from, use '-' to read tar from stdin.
1515

16+
--ignore-lockfile bool (default: false)
17+
Ignore warnings about not having a .terraform.lock.hcl file present in
18+
the template.
19+
1620
--name string
1721
Specify a name for the new template version. It will be automatically
1822
generated if not provided.

docs/cli/templates_create.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ Specify the directory to create from, use '-' to read tar from stdin.
3939

4040
Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).
4141

42+
### --ignore-lockfile
43+
44+
| | |
45+
| ------- | ------------------ |
46+
| Type | <code>bool</code> |
47+
| Default | <code>false</code> |
48+
49+
Ignore warnings about not having a .terraform.lock.hcl file present in the template.
50+
4251
### --inactivity-ttl
4352

4453
| | |

docs/cli/templates_push.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ Always prompt all parameters. Does not pull parameter values from active templat
3838

3939
Specify the directory to create from, use '-' to read tar from stdin.
4040

41+
### --ignore-lockfile
42+
43+
| | |
44+
| ------- | ------------------ |
45+
| Type | <code>bool</code> |
46+
| Default | <code>false</code> |
47+
48+
Ignore warnings about not having a .terraform.lock.hcl file present in the template.
49+
4150
### --name
4251

4352
| | |

provisionersdk/archive.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ func dirHasExt(dir string, exts ...string) (bool, error) {
3434
return false, nil
3535
}
3636

37+
func DirHasLockfile(dir string) (bool, error) {
38+
return dirHasExt(dir, ".terraform.lock.hcl")
39+
}
40+
3741
// Tar archives a Terraform directory.
3842
func Tar(w io.Writer, directory string, limit int64) error {
3943
// The total bytes written must be under the limit, so use -1

0 commit comments

Comments
 (0)