From fe1e504ce49d885abcb8daccf965d1d310d92704 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 13 Jan 2023 00:30:55 +0000 Subject: [PATCH 1/3] feat(provisionersdk): follow symlinks while archiving --- provisionersdk/archive.go | 80 ++++++++++++++++++++++------------ provisionersdk/archive_test.go | 66 +++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/provisionersdk/archive.go b/provisionersdk/archive.go index e5513d0f6e8b3..b3b8fb444b5e0 100644 --- a/provisionersdk/archive.go +++ b/provisionersdk/archive.go @@ -4,6 +4,7 @@ import ( "archive/tar" "bytes" "io" + "io/fs" "os" "path/filepath" "strings" @@ -31,43 +32,37 @@ func dirHasExt(dir string, ext string) (bool, error) { return false, nil } -// Tar archives a Terraform directory. -func Tar(directory string, limit int64) ([]byte, error) { - var buffer bytes.Buffer - tarWriter := tar.NewWriter(&buffer) - totalSize := int64(0) - const tfExt = ".tf" - hasTf, err := dirHasExt(directory, tfExt) - if err != nil { - return nil, err - } - if !hasTf { - absPath, err := filepath.Abs(directory) - if err != nil { - return nil, err - } +type archiver struct { - // Show absolute path to aid in debugging. E.g. showing "." is - // useless. - return nil, xerrors.Errorf( - "%s is not a valid template since it has no %s files", - absPath, tfExt, - ) - } +} - err = filepath.Walk(directory, func(file string, fileInfo os.FileInfo, err error) error { +func (a *archiver) walkFn(path string, fileInfo os.FileInfo, err error) error { if err != nil { return err } - var link string if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { - link, err = os.Readlink(file) + // Per https://github.com/coder/coder/issues/5677, we want to + // follow symlinks. + var linkDest string + linkDest, err = os.Readlink(file) if err != nil { return err } + + destInfo, err := os.Stat(linkDest) + if err != nil { + return err + } + if destInfo.IsDir() { + return filepath.Walk(linkDest, func(path string, info fs.FileInfo, err error) error { + walkFn(path, info, err) + }) + } + return nil } - header, err := tar.FileInfoHeader(fileInfo, link) + + header, err := tar.FileInfoHeader(fileInfo, "") if err != nil { return err } @@ -76,11 +71,10 @@ func Tar(directory string, limit int64) ([]byte, error) { return err } if strings.HasPrefix(rel, ".") || strings.HasPrefix(filepath.Base(rel), ".") { + // Don't archive hidden files! if fileInfo.IsDir() && rel != "." { - // Don't archive hidden files! return filepath.SkipDir } - // Don't archive hidden files! return nil } if strings.Contains(rel, ".tfstate") { @@ -109,7 +103,35 @@ func Tar(directory string, limit int64) ([]byte, error) { return xerrors.Errorf("Archive too big. Must be <= %d bytes", limit) } return data.Close() - }) + } +} + +// Tar archives a Terraform directory. +func Tar(directory string, limit int64) ([]byte, error) { + var buffer bytes.Buffer + tarWriter := tar.NewWriter(&buffer) + totalSize := int64(0) + + const tfExt = ".tf" + hasTf, err := dirHasExt(directory, tfExt) + if err != nil { + return nil, err + } + if !hasTf { + absPath, err := filepath.Abs(directory) + if err != nil { + return nil, err + } + + // Show absolute path to aid in debugging. E.g. showing "." is + // useless. + return nil, xerrors.Errorf( + "%s is not a valid template since it has no %s files", + absPath, tfExt, + ) + } + + err = filepath.Walk(directory, tarWalkFn()) if err != nil { return nil, err } diff --git a/provisionersdk/archive_test.go b/provisionersdk/archive_test.go index 4d37dd7ac5843..d0465e8a57d9c 100644 --- a/provisionersdk/archive_test.go +++ b/provisionersdk/archive_test.go @@ -12,6 +12,7 @@ import ( func TestTar(t *testing.T) { t.Parallel() + t.Run("NoTF", func(t *testing.T) { t.Parallel() dir := t.TempDir() @@ -90,15 +91,58 @@ func TestTar(t *testing.T) { func TestUntar(t *testing.T) { t.Parallel() - dir := t.TempDir() - file, err := os.CreateTemp(dir, "*.tf") - require.NoError(t, err) - _ = file.Close() - archive, err := provisionersdk.Tar(dir, 1024) - require.NoError(t, err) - dir = t.TempDir() - err = provisionersdk.Untar(dir, archive) - require.NoError(t, err) - _, err = os.Stat(filepath.Join(dir, filepath.Base(file.Name()))) - require.NoError(t, err) + t.Run("Normal", func(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + file, err := os.CreateTemp(dir, "*.tf") + require.NoError(t, err) + _ = file.Close() + archive, err := provisionersdk.Tar(dir, 1024) + require.NoError(t, err) + dir = t.TempDir() + err = provisionersdk.Untar(dir, archive) + require.NoError(t, err) + _, err = os.Stat(filepath.Join(dir, filepath.Base(file.Name()))) + require.NoError(t, err) + }) + t.Run("FollowSymlinks", func(t *testing.T) { + t.Parallel() + + externalDir := t.TempDir() + externalFile, err := os.CreateTemp(externalDir, "") + require.NoError(t, err) + const externalFileContents = "dogdogdog" + externalFile.WriteString(externalFileContents) + externalFile.Close() + + dir := t.TempDir() + + file, err := os.CreateTemp(dir, "*.tf") + require.NoError(t, err) + _ = file.Close() + + err = os.Symlink( + externalDir, + filepath.Join(dir, "link"), + ) + require.NoError(t, err) + + checkDir := func(dir string) { + gotContents, err := os.ReadFile(filepath.Join(dir, "link", filepath.Base(externalFile.Name()))) + require.NoError(t, err) + require.EqualValues(t, externalFileContents, gotContents) + } + + checkDir(dir) + + archive, err := provisionersdk.Tar(dir, 1024) + require.NoError(t, err) + + extractDir := t.TempDir() + err = provisionersdk.Untar(extractDir, archive) + require.NoError(t, err) + + checkDir(extractDir) + }) } From aac180b6e7167320ce32ae80bfc5152017cb7c5c Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 13 Jan 2023 17:18:17 +0000 Subject: [PATCH 2/3] Revert "feat(provisionersdk): follow symlinks while archiving" This reverts commit f38ecdb3c6b549ccc05069f2d69a056fbe7ec911. --- provisionersdk/archive.go | 80 ++++++++++++---------------------- provisionersdk/archive_test.go | 66 +++++----------------------- 2 files changed, 40 insertions(+), 106 deletions(-) diff --git a/provisionersdk/archive.go b/provisionersdk/archive.go index b3b8fb444b5e0..e5513d0f6e8b3 100644 --- a/provisionersdk/archive.go +++ b/provisionersdk/archive.go @@ -4,7 +4,6 @@ import ( "archive/tar" "bytes" "io" - "io/fs" "os" "path/filepath" "strings" @@ -32,37 +31,43 @@ func dirHasExt(dir string, ext string) (bool, error) { return false, nil } +// Tar archives a Terraform directory. +func Tar(directory string, limit int64) ([]byte, error) { + var buffer bytes.Buffer + tarWriter := tar.NewWriter(&buffer) + totalSize := int64(0) -type archiver struct { + const tfExt = ".tf" + hasTf, err := dirHasExt(directory, tfExt) + if err != nil { + return nil, err + } + if !hasTf { + absPath, err := filepath.Abs(directory) + if err != nil { + return nil, err + } -} + // Show absolute path to aid in debugging. E.g. showing "." is + // useless. + return nil, xerrors.Errorf( + "%s is not a valid template since it has no %s files", + absPath, tfExt, + ) + } -func (a *archiver) walkFn(path string, fileInfo os.FileInfo, err error) error { + err = filepath.Walk(directory, func(file string, fileInfo os.FileInfo, err error) error { if err != nil { return err } + var link string if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { - // Per https://github.com/coder/coder/issues/5677, we want to - // follow symlinks. - var linkDest string - linkDest, err = os.Readlink(file) + link, err = os.Readlink(file) if err != nil { return err } - - destInfo, err := os.Stat(linkDest) - if err != nil { - return err - } - if destInfo.IsDir() { - return filepath.Walk(linkDest, func(path string, info fs.FileInfo, err error) error { - walkFn(path, info, err) - }) - } - return nil } - - header, err := tar.FileInfoHeader(fileInfo, "") + header, err := tar.FileInfoHeader(fileInfo, link) if err != nil { return err } @@ -71,10 +76,11 @@ func (a *archiver) walkFn(path string, fileInfo os.FileInfo, err error) error { return err } if strings.HasPrefix(rel, ".") || strings.HasPrefix(filepath.Base(rel), ".") { - // Don't archive hidden files! if fileInfo.IsDir() && rel != "." { + // Don't archive hidden files! return filepath.SkipDir } + // Don't archive hidden files! return nil } if strings.Contains(rel, ".tfstate") { @@ -103,35 +109,7 @@ func (a *archiver) walkFn(path string, fileInfo os.FileInfo, err error) error { return xerrors.Errorf("Archive too big. Must be <= %d bytes", limit) } return data.Close() - } -} - -// Tar archives a Terraform directory. -func Tar(directory string, limit int64) ([]byte, error) { - var buffer bytes.Buffer - tarWriter := tar.NewWriter(&buffer) - totalSize := int64(0) - - const tfExt = ".tf" - hasTf, err := dirHasExt(directory, tfExt) - if err != nil { - return nil, err - } - if !hasTf { - absPath, err := filepath.Abs(directory) - if err != nil { - return nil, err - } - - // Show absolute path to aid in debugging. E.g. showing "." is - // useless. - return nil, xerrors.Errorf( - "%s is not a valid template since it has no %s files", - absPath, tfExt, - ) - } - - err = filepath.Walk(directory, tarWalkFn()) + }) if err != nil { return nil, err } diff --git a/provisionersdk/archive_test.go b/provisionersdk/archive_test.go index d0465e8a57d9c..4d37dd7ac5843 100644 --- a/provisionersdk/archive_test.go +++ b/provisionersdk/archive_test.go @@ -12,7 +12,6 @@ import ( func TestTar(t *testing.T) { t.Parallel() - t.Run("NoTF", func(t *testing.T) { t.Parallel() dir := t.TempDir() @@ -91,58 +90,15 @@ func TestTar(t *testing.T) { func TestUntar(t *testing.T) { t.Parallel() - t.Run("Normal", func(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - file, err := os.CreateTemp(dir, "*.tf") - require.NoError(t, err) - _ = file.Close() - archive, err := provisionersdk.Tar(dir, 1024) - require.NoError(t, err) - dir = t.TempDir() - err = provisionersdk.Untar(dir, archive) - require.NoError(t, err) - _, err = os.Stat(filepath.Join(dir, filepath.Base(file.Name()))) - require.NoError(t, err) - }) - t.Run("FollowSymlinks", func(t *testing.T) { - t.Parallel() - - externalDir := t.TempDir() - externalFile, err := os.CreateTemp(externalDir, "") - require.NoError(t, err) - const externalFileContents = "dogdogdog" - externalFile.WriteString(externalFileContents) - externalFile.Close() - - dir := t.TempDir() - - file, err := os.CreateTemp(dir, "*.tf") - require.NoError(t, err) - _ = file.Close() - - err = os.Symlink( - externalDir, - filepath.Join(dir, "link"), - ) - require.NoError(t, err) - - checkDir := func(dir string) { - gotContents, err := os.ReadFile(filepath.Join(dir, "link", filepath.Base(externalFile.Name()))) - require.NoError(t, err) - require.EqualValues(t, externalFileContents, gotContents) - } - - checkDir(dir) - - archive, err := provisionersdk.Tar(dir, 1024) - require.NoError(t, err) - - extractDir := t.TempDir() - err = provisionersdk.Untar(extractDir, archive) - require.NoError(t, err) - - checkDir(extractDir) - }) + dir := t.TempDir() + file, err := os.CreateTemp(dir, "*.tf") + require.NoError(t, err) + _ = file.Close() + archive, err := provisionersdk.Tar(dir, 1024) + require.NoError(t, err) + dir = t.TempDir() + err = provisionersdk.Untar(dir, archive) + require.NoError(t, err) + _, err = os.Stat(filepath.Join(dir, filepath.Base(file.Name()))) + require.NoError(t, err) } From cc4b44758003fc4ddf1989e478952c323ebaa7b2 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 13 Jan 2023 20:58:21 +0000 Subject: [PATCH 3/3] feat(cli): implement --tar in template push and create --- cli/templatecreate.go | 55 +++------- cli/templatecreate_test.go | 41 ++++++- cli/templatepush.go | 101 +++++++++++++----- cli/templatepush_test.go | 42 ++++++++ .../coder_templates_create_--help.golden | 4 +- .../coder_templates_push_--help.golden | 4 +- provisioner/echo/serve.go | 2 +- 7 files changed, 174 insertions(+), 75 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 8c8ce9e034610..7ab36feb3e6de 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -9,7 +9,6 @@ import ( "time" "unicode/utf8" - "github.com/briandowns/spinner" "github.com/google/uuid" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -19,16 +18,16 @@ import ( "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" "github.com/coder/coder/provisionerd" - "github.com/coder/coder/provisionersdk" ) func templateCreate() *cobra.Command { var ( - directory string provisioner string provisionerTags []string parameterFile string defaultTTL time.Duration + + uploadFlags templateUploadFlags ) cmd := &cobra.Command{ Use: "create [name]", @@ -45,11 +44,9 @@ func templateCreate() *cobra.Command { return err } - var templateName string - if len(args) == 0 { - templateName = filepath.Base(directory) - } else { - templateName = args[0] + templateName, err := uploadFlags.templateName(args) + if err != nil { + return err } if utf8.RuneCountInString(templateName) > 31 { @@ -62,31 +59,10 @@ func templateCreate() *cobra.Command { } // Confirm upload of the directory. - prettyDir := prettyDirectoryPath(directory) - _, err = cliui.Prompt(cmd, cliui.PromptOptions{ - Text: fmt.Sprintf("Create and upload %q?", prettyDir), - IsConfirm: true, - Default: cliui.ConfirmYes, - }) - if err != nil { - return err - } - - spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) - spin.Writer = cmd.OutOrStdout() - spin.Suffix = cliui.Styles.Keyword.Render(" Uploading directory...") - spin.Start() - defer spin.Stop() - archive, err := provisionersdk.Tar(directory, provisionersdk.TemplateArchiveLimit) - if err != nil { - return err - } - - resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, archive) + resp, err := uploadFlags.upload(cmd, client) if err != nil { return err } - spin.Stop() tags, err := ParseProvisionerTags(provisionerTags) if err != nil { @@ -105,12 +81,14 @@ func templateCreate() *cobra.Command { return err } - _, err = cliui.Prompt(cmd, cliui.PromptOptions{ - Text: "Confirm create?", - IsConfirm: true, - }) - if err != nil { - return err + if !uploadFlags.stdin() { + _, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "Confirm create?", + IsConfirm: true, + }) + if err != nil { + return err + } } createReq := codersdk.CreateTemplateRequest{ @@ -134,12 +112,11 @@ func templateCreate() *cobra.Command { return nil }, } - currentDirectory, _ := os.Getwd() - cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from") - cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend") cmd.Flags().StringVarP(¶meterFile, "parameter-file", "", "", "Specify a file path with parameter values.") cmd.Flags().StringArrayVarP(&provisionerTags, "provisioner-tag", "", []string{}, "Specify a set of tags to target provisioner daemons.") cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 24*time.Hour, "Specify a default TTL for workspaces created from this template.") + uploadFlags.register(cmd.Flags()) + cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend") // This is for testing! err := cmd.Flags().MarkHidden("test.provisioner") if err != nil { diff --git a/cli/templatecreate_test.go b/cli/templatecreate_test.go index d9ed25ebdb99c..e44c8c7e2a2ce 100644 --- a/cli/templatecreate_test.go +++ b/cli/templatecreate_test.go @@ -1,6 +1,7 @@ package cli_test import ( + "bytes" "os" "testing" @@ -69,7 +70,7 @@ func TestTemplateCreate(t *testing.T) { match string write string }{ - {match: "Create and upload", write: "yes"}, + {match: "Upload", write: "yes"}, {match: "compute.main"}, {match: "smith (linux, i386)"}, {match: "Confirm create?", write: "yes"}, @@ -84,6 +85,38 @@ func TestTemplateCreate(t *testing.T) { require.NoError(t, <-execDone) }) + t.Run("CreateStdin", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + source, err := echo.Tar(&echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: provisionCompleteWithAgent, + }) + require.NoError(t, err) + + args := []string{ + "templates", + "create", + "my-template", + "--directory", "-", + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--default-ttl", "24h", + } + cmd, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + cmd.SetIn(bytes.NewReader(source)) + cmd.SetOut(pty.Output()) + + execDone := make(chan error) + go func() { + execDone <- cmd.Execute() + }() + + require.NoError(t, <-execDone) + }) + t.Run("WithParameter", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) @@ -108,7 +141,7 @@ func TestTemplateCreate(t *testing.T) { match string write string }{ - {match: "Create and upload", write: "yes"}, + {match: "Upload", write: "yes"}, {match: "Enter a value:", write: "bananas"}, {match: "Confirm create?", write: "yes"}, } @@ -148,7 +181,7 @@ func TestTemplateCreate(t *testing.T) { match string write string }{ - {match: "Create and upload", write: "yes"}, + {match: "Upload", write: "yes"}, {match: "Confirm create?", write: "yes"}, } for _, m := range matches { @@ -188,7 +221,7 @@ func TestTemplateCreate(t *testing.T) { write string }{ { - match: "Create and upload", + match: "Upload", write: "yes", }, { diff --git a/cli/templatepush.go b/cli/templatepush.go index 9c146a9b8cbcf..3ba612d7e674f 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -2,12 +2,14 @@ package cli import ( "fmt" + "io" "os" "path/filepath" "time" "github.com/briandowns/spinner" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/xerrors" "github.com/coder/coder/cli/cliui" @@ -16,14 +18,81 @@ import ( "github.com/coder/coder/provisionersdk" ) +// templateUploadFlags is shared by `templates create` and `templates push`. +type templateUploadFlags struct { + directory string +} + +func (pf *templateUploadFlags) register(f *pflag.FlagSet) { + currentDirectory, _ := os.Getwd() + f.StringVarP(&pf.directory, "directory", "d", currentDirectory, "Specify the directory to create from, use '-' to read tar from stdin") +} + +func (pf *templateUploadFlags) stdin() bool { + return pf.directory == "-" +} + +func (pf *templateUploadFlags) upload(cmd *cobra.Command, client *codersdk.Client) (*codersdk.UploadResponse, error) { + var ( + content []byte + err error + ) + if pf.stdin() { + content, err = io.ReadAll(cmd.InOrStdin()) + } else { + prettyDir := prettyDirectoryPath(pf.directory) + _, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: fmt.Sprintf("Upload %q?", prettyDir), + IsConfirm: true, + Default: cliui.ConfirmYes, + }) + if err != nil { + return nil, err + } + + content, err = provisionersdk.Tar(pf.directory, provisionersdk.TemplateArchiveLimit) + } + if err != nil { + return nil, xerrors.Errorf("read tar: %w", err) + } + + spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) + spin.Writer = cmd.OutOrStdout() + spin.Suffix = cliui.Styles.Keyword.Render(" Uploading directory...") + spin.Start() + defer spin.Stop() + + resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, content) + if err != nil { + return nil, xerrors.Errorf("upload: %w", err) + } + return &resp, nil +} + +func (pf *templateUploadFlags) templateName(args []string) (string, error) { + if pf.stdin() { + // Can't infer name from directory if none provided. + if len(args) == 0 { + return "", xerrors.New("template name argument must be provided") + } + return args[0], nil + } + + name := filepath.Base(pf.directory) + if len(args) > 0 { + name = args[0] + } + return name, nil +} + func templatePush() *cobra.Command { var ( - directory string versionName string provisioner string parameterFile string alwaysPrompt bool provisionerTags []string + uploadFlags templateUploadFlags ) cmd := &cobra.Command{ @@ -40,41 +109,20 @@ func templatePush() *cobra.Command { return err } - name := filepath.Base(directory) - if len(args) > 0 { - name = args[0] - } - - template, err := client.TemplateByName(cmd.Context(), organization.ID, name) + name, err := uploadFlags.templateName(args) if err != nil { return err } - // Confirm upload of the directory. - prettyDir := prettyDirectoryPath(directory) - _, err = cliui.Prompt(cmd, cliui.PromptOptions{ - Text: fmt.Sprintf("Upload %q?", prettyDir), - IsConfirm: true, - Default: cliui.ConfirmYes, - }) + template, err := client.TemplateByName(cmd.Context(), organization.ID, name) if err != nil { return err } - spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) - spin.Writer = cmd.OutOrStdout() - spin.Suffix = cliui.Styles.Keyword.Render(" Uploading directory...") - spin.Start() - defer spin.Stop() - content, err := provisionersdk.Tar(directory, provisionersdk.TemplateArchiveLimit) + resp, err := uploadFlags.upload(cmd, client) if err != nil { return err } - resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, content) - if err != nil { - return err - } - spin.Stop() tags, err := ParseProvisionerTags(provisionerTags) if err != nil { @@ -112,13 +160,12 @@ func templatePush() *cobra.Command { }, } - currentDirectory, _ := os.Getwd() - cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from") cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend") cmd.Flags().StringVarP(¶meterFile, "parameter-file", "", "", "Specify a file path with parameter values.") cmd.Flags().StringVarP(&versionName, "name", "", "", "Specify a name for the new template version. It will be automatically generated if not provided.") cmd.Flags().StringArrayVarP(&provisionerTags, "provisioner-tag", "", []string{}, "Specify a set of tags to target provisioner daemons.") cmd.Flags().BoolVar(&alwaysPrompt, "always-prompt", false, "Always prompt all parameters. Does not pull parameter values from active template version") + uploadFlags.register(cmd.Flags()) cliui.AllowSkipPrompt(cmd) // This is for testing! err := cmd.Flags().MarkHidden("test.provisioner") diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 4aa7867c0df8b..0ade770324813 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -1,6 +1,7 @@ package cli_test import ( + "bytes" "context" "path/filepath" "testing" @@ -208,6 +209,47 @@ func TestTemplatePush(t *testing.T) { assert.Len(t, templateVersions, 2) assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID) }) + + t.Run("Stdin", 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) + + source, err := echo.Tar(&echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ProvisionComplete, + }) + require.NoError(t, err) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + cmd, root := clitest.New( + t, "templates", "push", "--directory", "-", + "--test.provisioner", string(database.ProvisionerTypeEcho), + template.Name, + ) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + cmd.SetIn(bytes.NewReader(source)) + cmd.SetOut(pty.Output()) + + execDone := make(chan error) + go func() { + execDone <- cmd.Execute() + }() + + require.NoError(t, <-execDone) + + // Assert that the template version changed. + templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + }) + require.NoError(t, err) + assert.Len(t, templateVersions, 2) + assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID) + }) } func latestTemplateVersion(t *testing.T, client *codersdk.Client, templateID uuid.UUID) (codersdk.TemplateVersion, []codersdk.Parameter) { diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden index 7fd1d9121d58f..aebabdd55d1f5 100644 --- a/cli/testdata/coder_templates_create_--help.golden +++ b/cli/testdata/coder_templates_create_--help.golden @@ -6,8 +6,8 @@ Usage: Flags: --default-ttl duration Specify a default TTL for workspaces created from this template. (default 24h0m0s) - -d, --directory string Specify the directory to create from (default - "/tmp/coder-cli-test-workdir") + -d, --directory string Specify the directory to create from, use '-' to read + tar from stdin (default "/tmp/coder-cli-test-workdir") -h, --help help for create --parameter-file string Specify a file path with parameter values. --provisioner-tag stringArray Specify a set of tags to target provisioner daemons. diff --git a/cli/testdata/coder_templates_push_--help.golden b/cli/testdata/coder_templates_push_--help.golden index 348adaf179a92..baf55609e7f17 100644 --- a/cli/testdata/coder_templates_push_--help.golden +++ b/cli/testdata/coder_templates_push_--help.golden @@ -6,8 +6,8 @@ Usage: Flags: --always-prompt Always prompt all parameters. Does not pull parameter values from active template version - -d, --directory string Specify the directory to create from (default - "/tmp/coder-cli-test-workdir") + -d, --directory string Specify the directory to create from, use '-' to read + tar from stdin (default "/tmp/coder-cli-test-workdir") -h, --help help for push --name string Specify a name for the new template version. It will be automatically generated if not provided. diff --git a/provisioner/echo/serve.go b/provisioner/echo/serve.go index 6684b92c6ee94..705fb0a3be2ae 100644 --- a/provisioner/echo/serve.go +++ b/provisioner/echo/serve.go @@ -88,7 +88,7 @@ func (e *echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ if err != nil { if index == 0 { // Error if nothing is around to enable failed states. - return xerrors.New("no state") + return xerrors.Errorf("no state: %w", err) } break }