chore(cli): warn on template push or create when no lockfile present (#8059)

This commit is contained in:
Colin Adler 2023-06-20 10:02:44 -05:00 committed by GitHub
parent a47a9b1cfe
commit adf14f1917
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 272 additions and 8 deletions

View File

@ -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)

View File

@ -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
} }

View File

@ -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()

View File

@ -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
} }

View File

@ -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})

View File

@ -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).

View File

@ -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.

View File

@ -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
| | | | | |

View File

@ -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
| | | | | |

View File

@ -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

View File

@ -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()