mirror of https://github.com/coder/coder.git
chore(cli): warn on template push or create when no lockfile present (#8059)
This commit is contained in:
parent
a47a9b1cfe
commit
adf14f1917
|
@ -6,7 +6,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -86,7 +85,10 @@ func SetupConfig(t *testing.T, client *codersdk.Client, root config.Root) {
|
||||||
// new temporary testing directory.
|
// new temporary testing directory.
|
||||||
func CreateTemplateVersionSource(t *testing.T, responses *echo.Responses) string {
|
func CreateTemplateVersionSource(t *testing.T, responses *echo.Responses) string {
|
||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
f, err := ioutil.TempFile(directory, "*.tf")
|
f, err := os.CreateTemp(directory, "*.tf")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_ = f.Close()
|
||||||
|
f, err = os.Create(filepath.Join(directory, ".terraform.lock.hcl"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
data, err := echo.Tar(responses)
|
data, err := echo.Tar(responses)
|
||||||
|
|
|
@ -87,6 +87,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
|
||||||
return xerrors.Errorf("A template already exists named %q!", templateName)
|
return xerrors.Errorf("A template already exists named %q!", templateName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = uploadFlags.checkForLockfile(inv)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("check for lockfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Confirm upload of the directory.
|
// Confirm upload of the directory.
|
||||||
resp, err := uploadFlags.upload(inv, client)
|
resp, err := uploadFlags.upload(inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -185,7 +190,6 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
|
||||||
Default: "0h",
|
Default: "0h",
|
||||||
Value: clibase.DurationOf(&inactivityTTL),
|
Value: clibase.DurationOf(&inactivityTTL),
|
||||||
},
|
},
|
||||||
uploadFlags.option(),
|
|
||||||
{
|
{
|
||||||
Flag: "test.provisioner",
|
Flag: "test.provisioner",
|
||||||
Description: "Customize the provisioner backend.",
|
Description: "Customize the provisioner backend.",
|
||||||
|
@ -195,6 +199,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
|
||||||
},
|
},
|
||||||
cliui.SkipPromptOption(),
|
cliui.SkipPromptOption(),
|
||||||
}
|
}
|
||||||
|
cmd.Options = append(cmd.Options, uploadFlags.options()...)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -80,6 +81,87 @@ func TestTemplateCreate(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
t.Run("CreateNoLockfile", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
coderdtest.CreateFirstUser(t, client)
|
||||||
|
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
|
||||||
|
Parse: echo.ParseComplete,
|
||||||
|
ProvisionApply: provisionCompleteWithAgent,
|
||||||
|
})
|
||||||
|
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
|
||||||
|
args := []string{
|
||||||
|
"templates",
|
||||||
|
"create",
|
||||||
|
"my-template",
|
||||||
|
"--directory", source,
|
||||||
|
"--test.provisioner", string(database.ProvisionerTypeEcho),
|
||||||
|
"--default-ttl", "24h",
|
||||||
|
}
|
||||||
|
inv, root := clitest.New(t, args...)
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
|
||||||
|
execDone := make(chan error)
|
||||||
|
go func() {
|
||||||
|
execDone <- inv.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
matches := []struct {
|
||||||
|
match string
|
||||||
|
write string
|
||||||
|
}{
|
||||||
|
{match: "No .terraform.lock.hcl file found"},
|
||||||
|
{match: "Upload", write: "no"},
|
||||||
|
}
|
||||||
|
for _, m := range matches {
|
||||||
|
pty.ExpectMatch(m.match)
|
||||||
|
if len(m.write) > 0 {
|
||||||
|
pty.WriteLine(m.write)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmd should error once we say no.
|
||||||
|
require.Error(t, <-execDone)
|
||||||
|
})
|
||||||
|
t.Run("CreateNoLockfileIgnored", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
coderdtest.CreateFirstUser(t, client)
|
||||||
|
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
|
||||||
|
Parse: echo.ParseComplete,
|
||||||
|
ProvisionApply: provisionCompleteWithAgent,
|
||||||
|
})
|
||||||
|
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
|
||||||
|
args := []string{
|
||||||
|
"templates",
|
||||||
|
"create",
|
||||||
|
"my-template",
|
||||||
|
"--directory", source,
|
||||||
|
"--test.provisioner", string(database.ProvisionerTypeEcho),
|
||||||
|
"--default-ttl", "24h",
|
||||||
|
"--ignore-lockfile",
|
||||||
|
}
|
||||||
|
inv, root := clitest.New(t, args...)
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
|
||||||
|
execDone := make(chan error)
|
||||||
|
go func() {
|
||||||
|
execDone <- inv.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
{
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
pty.ExpectNoMatchBefore(ctx, "No .terraform.lock.hcl file found", "Upload")
|
||||||
|
pty.WriteLine("no")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmd should error once we say no.
|
||||||
|
require.Error(t, <-execDone)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("CreateStdin", func(t *testing.T) {
|
t.Run("CreateStdin", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
|
@ -19,17 +19,23 @@ import (
|
||||||
|
|
||||||
// templateUploadFlags is shared by `templates create` and `templates push`.
|
// templateUploadFlags is shared by `templates create` and `templates push`.
|
||||||
type templateUploadFlags struct {
|
type templateUploadFlags struct {
|
||||||
directory string
|
directory string
|
||||||
|
ignoreLockfile bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pf *templateUploadFlags) option() clibase.Option {
|
func (pf *templateUploadFlags) options() []clibase.Option {
|
||||||
return clibase.Option{
|
return []clibase.Option{{
|
||||||
Flag: "directory",
|
Flag: "directory",
|
||||||
FlagShorthand: "d",
|
FlagShorthand: "d",
|
||||||
Description: "Specify the directory to create from, use '-' to read tar from stdin.",
|
Description: "Specify the directory to create from, use '-' to read tar from stdin.",
|
||||||
Default: ".",
|
Default: ".",
|
||||||
Value: clibase.StringOf(&pf.directory),
|
Value: clibase.StringOf(&pf.directory),
|
||||||
}
|
}, {
|
||||||
|
Flag: "ignore-lockfile",
|
||||||
|
Description: "Ignore warnings about not having a .terraform.lock.hcl file present in the template.",
|
||||||
|
Default: "false",
|
||||||
|
Value: clibase.BoolOf(&pf.ignoreLockfile),
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pf *templateUploadFlags) setWorkdir(wd string) {
|
func (pf *templateUploadFlags) setWorkdir(wd string) {
|
||||||
|
@ -84,6 +90,26 @@ func (pf *templateUploadFlags) upload(inv *clibase.Invocation, client *codersdk.
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pf *templateUploadFlags) checkForLockfile(inv *clibase.Invocation) error {
|
||||||
|
if pf.stdin() || pf.ignoreLockfile {
|
||||||
|
// Just assume there's a lockfile if reading from stdin.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hasLockfile, err := provisionersdk.DirHasLockfile(pf.directory)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("dir has lockfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasLockfile {
|
||||||
|
cliui.Warn(inv.Stdout, "No .terraform.lock.hcl file found",
|
||||||
|
"When provisioning, Coder will be unable to cache providers without a lockfile and must download them from the internet each time.",
|
||||||
|
"Create one by running "+cliui.DefaultStyles.Code.Render("terraform init")+" in your template directory.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (pf *templateUploadFlags) templateName(args []string) (string, error) {
|
func (pf *templateUploadFlags) templateName(args []string) (string, error) {
|
||||||
if pf.stdin() {
|
if pf.stdin() {
|
||||||
// Can't infer name from directory if none provided.
|
// Can't infer name from directory if none provided.
|
||||||
|
@ -143,6 +169,11 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = uploadFlags.checkForLockfile(inv)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("check for lockfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := uploadFlags.upload(inv, client)
|
resp, err := uploadFlags.upload(inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -236,7 +267,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
|
||||||
Value: clibase.BoolOf(&activate),
|
Value: clibase.BoolOf(&activate),
|
||||||
},
|
},
|
||||||
cliui.SkipPromptOption(),
|
cliui.SkipPromptOption(),
|
||||||
uploadFlags.option(),
|
|
||||||
}
|
}
|
||||||
|
cmd.Options = append(cmd.Options, uploadFlags.options()...)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"github.com/coder/coder/provisionersdk/proto"
|
||||||
"github.com/coder/coder/pty/ptytest"
|
"github.com/coder/coder/pty/ptytest"
|
||||||
|
"github.com/coder/coder/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTemplatePush(t *testing.T) {
|
func TestTemplatePush(t *testing.T) {
|
||||||
|
@ -69,6 +70,86 @@ func TestTemplatePush(t *testing.T) {
|
||||||
require.Equal(t, "example", templateVersions[1].Name)
|
require.Equal(t, "example", templateVersions[1].Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("NoLockfile", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
|
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
|
// Test the cli command.
|
||||||
|
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
|
||||||
|
Parse: echo.ParseComplete,
|
||||||
|
ProvisionApply: echo.ProvisionComplete,
|
||||||
|
})
|
||||||
|
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
|
||||||
|
|
||||||
|
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example")
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
|
||||||
|
execDone := make(chan error)
|
||||||
|
go func() {
|
||||||
|
execDone <- inv.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
matches := []struct {
|
||||||
|
match string
|
||||||
|
write string
|
||||||
|
}{
|
||||||
|
{match: "No .terraform.lock.hcl file found"},
|
||||||
|
{match: "Upload", write: "no"},
|
||||||
|
}
|
||||||
|
for _, m := range matches {
|
||||||
|
pty.ExpectMatch(m.match)
|
||||||
|
if m.write != "" {
|
||||||
|
pty.WriteLine(m.write)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmd should error once we say no.
|
||||||
|
require.Error(t, <-execDone)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NoLockfileIgnored", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
|
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
|
// Test the cli command.
|
||||||
|
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
|
||||||
|
Parse: echo.ParseComplete,
|
||||||
|
ProvisionApply: echo.ProvisionComplete,
|
||||||
|
})
|
||||||
|
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
|
||||||
|
|
||||||
|
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example", "--ignore-lockfile")
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
|
||||||
|
execDone := make(chan error)
|
||||||
|
go func() {
|
||||||
|
execDone <- inv.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
{
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
pty.ExpectNoMatchBefore(ctx, "No .terraform.lock.hcl file found", "Upload")
|
||||||
|
pty.WriteLine("no")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmd should error once we say no.
|
||||||
|
require.Error(t, <-execDone)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("PushInactiveTemplateVersion", func(t *testing.T) {
|
t.Run("PushInactiveTemplateVersion", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
|
|
@ -13,6 +13,10 @@ Create a template from the current directory or as specified by flag
|
||||||
Specify a failure TTL for workspaces created from this template. This
|
Specify a failure TTL for workspaces created from this template. This
|
||||||
licensed feature's default is 0h (off).
|
licensed feature's default is 0h (off).
|
||||||
|
|
||||||
|
--ignore-lockfile bool (default: false)
|
||||||
|
Ignore warnings about not having a .terraform.lock.hcl file present in
|
||||||
|
the template.
|
||||||
|
|
||||||
--inactivity-ttl duration (default: 0h)
|
--inactivity-ttl duration (default: 0h)
|
||||||
Specify an inactivity TTL for workspaces created from this template.
|
Specify an inactivity TTL for workspaces created from this template.
|
||||||
This licensed feature's default is 0h (off).
|
This licensed feature's default is 0h (off).
|
||||||
|
|
|
@ -13,6 +13,10 @@ Push a new template version from the current directory or as specified by flag
|
||||||
-d, --directory string (default: .)
|
-d, --directory string (default: .)
|
||||||
Specify the directory to create from, use '-' to read tar from stdin.
|
Specify the directory to create from, use '-' to read tar from stdin.
|
||||||
|
|
||||||
|
--ignore-lockfile bool (default: false)
|
||||||
|
Ignore warnings about not having a .terraform.lock.hcl file present in
|
||||||
|
the template.
|
||||||
|
|
||||||
--name string
|
--name string
|
||||||
Specify a name for the new template version. It will be automatically
|
Specify a name for the new template version. It will be automatically
|
||||||
generated if not provided.
|
generated if not provided.
|
||||||
|
|
|
@ -39,6 +39,15 @@ Specify the directory to create from, use '-' to read tar from stdin.
|
||||||
|
|
||||||
Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).
|
Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).
|
||||||
|
|
||||||
|
### --ignore-lockfile
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
| Default | <code>false</code> |
|
||||||
|
|
||||||
|
Ignore warnings about not having a .terraform.lock.hcl file present in the template.
|
||||||
|
|
||||||
### --inactivity-ttl
|
### --inactivity-ttl
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
@ -38,6 +38,15 @@ Always prompt all parameters. Does not pull parameter values from active templat
|
||||||
|
|
||||||
Specify the directory to create from, use '-' to read tar from stdin.
|
Specify the directory to create from, use '-' to read tar from stdin.
|
||||||
|
|
||||||
|
### --ignore-lockfile
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
| Default | <code>false</code> |
|
||||||
|
|
||||||
|
Ignore warnings about not having a .terraform.lock.hcl file present in the template.
|
||||||
|
|
||||||
### --name
|
### --name
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
@ -34,6 +34,10 @@ func dirHasExt(dir string, exts ...string) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DirHasLockfile(dir string) (bool, error) {
|
||||||
|
return dirHasExt(dir, ".terraform.lock.hcl")
|
||||||
|
}
|
||||||
|
|
||||||
// Tar archives a Terraform directory.
|
// Tar archives a Terraform directory.
|
||||||
func Tar(w io.Writer, directory string, limit int64) error {
|
func Tar(w io.Writer, directory string, limit int64) error {
|
||||||
// The total bytes written must be under the limit, so use -1
|
// The total bytes written must be under the limit, so use -1
|
||||||
|
|
|
@ -181,6 +181,39 @@ func (e *outExpecter) ExpectMatchContext(ctx context.Context, str string) string
|
||||||
return buffer.String()
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpectNoMatchBefore validates that `match` does not occur before `before`.
|
||||||
|
func (e *outExpecter) ExpectNoMatchBefore(ctx context.Context, match, before string) string {
|
||||||
|
e.t.Helper()
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err := e.doMatchWithDeadline(ctx, "ExpectNoMatchBefore", func() error {
|
||||||
|
for {
|
||||||
|
r, _, err := e.runeReader.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = buffer.WriteRune(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(buffer.String(), match) {
|
||||||
|
return xerrors.Errorf("found %q before %q", match, before)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(buffer.String(), before) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
e.fatalf("read error", "%v (wanted no %q before %q; got %q)", err, match, before, buffer.String())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
e.logf("matched %q = %q", before, stripansi.Strip(buffer.String()))
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (e *outExpecter) Peek(ctx context.Context, n int) []byte {
|
func (e *outExpecter) Peek(ctx context.Context, n int) []byte {
|
||||||
e.t.Helper()
|
e.t.Helper()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue