Skip to content

Commit 592ce3b

Browse files
authored
feat(cli): allow direct tar upload in template update/create (#5720)
1 parent 5f7cce7 commit 592ce3b

File tree

7 files changed

+174
-75
lines changed

7 files changed

+174
-75
lines changed

cli/templatecreate.go

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"time"
1010
"unicode/utf8"
1111

12-
"github.com/briandowns/spinner"
1312
"github.com/google/uuid"
1413
"github.com/spf13/cobra"
1514
"golang.org/x/xerrors"
@@ -19,16 +18,16 @@ import (
1918
"github.com/coder/coder/coderd/util/ptr"
2019
"github.com/coder/coder/codersdk"
2120
"github.com/coder/coder/provisionerd"
22-
"github.com/coder/coder/provisionersdk"
2321
)
2422

2523
func templateCreate() *cobra.Command {
2624
var (
27-
directory string
2825
provisioner string
2926
provisionerTags []string
3027
parameterFile string
3128
defaultTTL time.Duration
29+
30+
uploadFlags templateUploadFlags
3231
)
3332
cmd := &cobra.Command{
3433
Use: "create [name]",
@@ -45,11 +44,9 @@ func templateCreate() *cobra.Command {
4544
return err
4645
}
4746

48-
var templateName string
49-
if len(args) == 0 {
50-
templateName = filepath.Base(directory)
51-
} else {
52-
templateName = args[0]
47+
templateName, err := uploadFlags.templateName(args)
48+
if err != nil {
49+
return err
5350
}
5451

5552
if utf8.RuneCountInString(templateName) > 31 {
@@ -62,31 +59,10 @@ func templateCreate() *cobra.Command {
6259
}
6360

6461
// Confirm upload of the directory.
65-
prettyDir := prettyDirectoryPath(directory)
66-
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
67-
Text: fmt.Sprintf("Create and upload %q?", prettyDir),
68-
IsConfirm: true,
69-
Default: cliui.ConfirmYes,
70-
})
71-
if err != nil {
72-
return err
73-
}
74-
75-
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
76-
spin.Writer = cmd.OutOrStdout()
77-
spin.Suffix = cliui.Styles.Keyword.Render(" Uploading directory...")
78-
spin.Start()
79-
defer spin.Stop()
80-
archive, err := provisionersdk.Tar(directory, provisionersdk.TemplateArchiveLimit)
81-
if err != nil {
82-
return err
83-
}
84-
85-
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, archive)
62+
resp, err := uploadFlags.upload(cmd, client)
8663
if err != nil {
8764
return err
8865
}
89-
spin.Stop()
9066

9167
tags, err := ParseProvisionerTags(provisionerTags)
9268
if err != nil {
@@ -105,12 +81,14 @@ func templateCreate() *cobra.Command {
10581
return err
10682
}
10783

108-
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
109-
Text: "Confirm create?",
110-
IsConfirm: true,
111-
})
112-
if err != nil {
113-
return err
84+
if !uploadFlags.stdin() {
85+
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
86+
Text: "Confirm create?",
87+
IsConfirm: true,
88+
})
89+
if err != nil {
90+
return err
91+
}
11492
}
11593

11694
createReq := codersdk.CreateTemplateRequest{
@@ -134,12 +112,11 @@ func templateCreate() *cobra.Command {
134112
return nil
135113
},
136114
}
137-
currentDirectory, _ := os.Getwd()
138-
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
139-
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
140115
cmd.Flags().StringVarP(&parameterFile, "parameter-file", "", "", "Specify a file path with parameter values.")
141116
cmd.Flags().StringArrayVarP(&provisionerTags, "provisioner-tag", "", []string{}, "Specify a set of tags to target provisioner daemons.")
142117
cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 24*time.Hour, "Specify a default TTL for workspaces created from this template.")
118+
uploadFlags.register(cmd.Flags())
119+
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
143120
// This is for testing!
144121
err := cmd.Flags().MarkHidden("test.provisioner")
145122
if err != nil {

cli/templatecreate_test.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli_test
22

33
import (
4+
"bytes"
45
"os"
56
"testing"
67

@@ -69,7 +70,7 @@ func TestTemplateCreate(t *testing.T) {
6970
match string
7071
write string
7172
}{
72-
{match: "Create and upload", write: "yes"},
73+
{match: "Upload", write: "yes"},
7374
{match: "compute.main"},
7475
{match: "smith (linux, i386)"},
7576
{match: "Confirm create?", write: "yes"},
@@ -84,6 +85,38 @@ func TestTemplateCreate(t *testing.T) {
8485
require.NoError(t, <-execDone)
8586
})
8687

88+
t.Run("CreateStdin", func(t *testing.T) {
89+
t.Parallel()
90+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
91+
coderdtest.CreateFirstUser(t, client)
92+
source, err := echo.Tar(&echo.Responses{
93+
Parse: echo.ParseComplete,
94+
ProvisionApply: provisionCompleteWithAgent,
95+
})
96+
require.NoError(t, err)
97+
98+
args := []string{
99+
"templates",
100+
"create",
101+
"my-template",
102+
"--directory", "-",
103+
"--test.provisioner", string(database.ProvisionerTypeEcho),
104+
"--default-ttl", "24h",
105+
}
106+
cmd, root := clitest.New(t, args...)
107+
clitest.SetupConfig(t, client, root)
108+
pty := ptytest.New(t)
109+
cmd.SetIn(bytes.NewReader(source))
110+
cmd.SetOut(pty.Output())
111+
112+
execDone := make(chan error)
113+
go func() {
114+
execDone <- cmd.Execute()
115+
}()
116+
117+
require.NoError(t, <-execDone)
118+
})
119+
87120
t.Run("WithParameter", func(t *testing.T) {
88121
t.Parallel()
89122
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
@@ -108,7 +141,7 @@ func TestTemplateCreate(t *testing.T) {
108141
match string
109142
write string
110143
}{
111-
{match: "Create and upload", write: "yes"},
144+
{match: "Upload", write: "yes"},
112145
{match: "Enter a value:", write: "bananas"},
113146
{match: "Confirm create?", write: "yes"},
114147
}
@@ -148,7 +181,7 @@ func TestTemplateCreate(t *testing.T) {
148181
match string
149182
write string
150183
}{
151-
{match: "Create and upload", write: "yes"},
184+
{match: "Upload", write: "yes"},
152185
{match: "Confirm create?", write: "yes"},
153186
}
154187
for _, m := range matches {
@@ -188,7 +221,7 @@ func TestTemplateCreate(t *testing.T) {
188221
write string
189222
}{
190223
{
191-
match: "Create and upload",
224+
match: "Upload",
192225
write: "yes",
193226
},
194227
{

cli/templatepush.go

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package cli
22

33
import (
44
"fmt"
5+
"io"
56
"os"
67
"path/filepath"
78
"time"
89

910
"github.com/briandowns/spinner"
1011
"github.com/spf13/cobra"
12+
"github.com/spf13/pflag"
1113
"golang.org/x/xerrors"
1214

1315
"github.com/coder/coder/cli/cliui"
@@ -16,14 +18,81 @@ import (
1618
"github.com/coder/coder/provisionersdk"
1719
)
1820

21+
// templateUploadFlags is shared by `templates create` and `templates push`.
22+
type templateUploadFlags struct {
23+
directory string
24+
}
25+
26+
func (pf *templateUploadFlags) register(f *pflag.FlagSet) {
27+
currentDirectory, _ := os.Getwd()
28+
f.StringVarP(&pf.directory, "directory", "d", currentDirectory, "Specify the directory to create from, use '-' to read tar from stdin")
29+
}
30+
31+
func (pf *templateUploadFlags) stdin() bool {
32+
return pf.directory == "-"
33+
}
34+
35+
func (pf *templateUploadFlags) upload(cmd *cobra.Command, client *codersdk.Client) (*codersdk.UploadResponse, error) {
36+
var (
37+
content []byte
38+
err error
39+
)
40+
if pf.stdin() {
41+
content, err = io.ReadAll(cmd.InOrStdin())
42+
} else {
43+
prettyDir := prettyDirectoryPath(pf.directory)
44+
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
45+
Text: fmt.Sprintf("Upload %q?", prettyDir),
46+
IsConfirm: true,
47+
Default: cliui.ConfirmYes,
48+
})
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
content, err = provisionersdk.Tar(pf.directory, provisionersdk.TemplateArchiveLimit)
54+
}
55+
if err != nil {
56+
return nil, xerrors.Errorf("read tar: %w", err)
57+
}
58+
59+
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
60+
spin.Writer = cmd.OutOrStdout()
61+
spin.Suffix = cliui.Styles.Keyword.Render(" Uploading directory...")
62+
spin.Start()
63+
defer spin.Stop()
64+
65+
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, content)
66+
if err != nil {
67+
return nil, xerrors.Errorf("upload: %w", err)
68+
}
69+
return &resp, nil
70+
}
71+
72+
func (pf *templateUploadFlags) templateName(args []string) (string, error) {
73+
if pf.stdin() {
74+
// Can't infer name from directory if none provided.
75+
if len(args) == 0 {
76+
return "", xerrors.New("template name argument must be provided")
77+
}
78+
return args[0], nil
79+
}
80+
81+
name := filepath.Base(pf.directory)
82+
if len(args) > 0 {
83+
name = args[0]
84+
}
85+
return name, nil
86+
}
87+
1988
func templatePush() *cobra.Command {
2089
var (
21-
directory string
2290
versionName string
2391
provisioner string
2492
parameterFile string
2593
alwaysPrompt bool
2694
provisionerTags []string
95+
uploadFlags templateUploadFlags
2796
)
2897

2998
cmd := &cobra.Command{
@@ -40,41 +109,20 @@ func templatePush() *cobra.Command {
40109
return err
41110
}
42111

43-
name := filepath.Base(directory)
44-
if len(args) > 0 {
45-
name = args[0]
46-
}
47-
48-
template, err := client.TemplateByName(cmd.Context(), organization.ID, name)
112+
name, err := uploadFlags.templateName(args)
49113
if err != nil {
50114
return err
51115
}
52116

53-
// Confirm upload of the directory.
54-
prettyDir := prettyDirectoryPath(directory)
55-
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
56-
Text: fmt.Sprintf("Upload %q?", prettyDir),
57-
IsConfirm: true,
58-
Default: cliui.ConfirmYes,
59-
})
117+
template, err := client.TemplateByName(cmd.Context(), organization.ID, name)
60118
if err != nil {
61119
return err
62120
}
63121

64-
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
65-
spin.Writer = cmd.OutOrStdout()
66-
spin.Suffix = cliui.Styles.Keyword.Render(" Uploading directory...")
67-
spin.Start()
68-
defer spin.Stop()
69-
content, err := provisionersdk.Tar(directory, provisionersdk.TemplateArchiveLimit)
122+
resp, err := uploadFlags.upload(cmd, client)
70123
if err != nil {
71124
return err
72125
}
73-
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, content)
74-
if err != nil {
75-
return err
76-
}
77-
spin.Stop()
78126

79127
tags, err := ParseProvisionerTags(provisionerTags)
80128
if err != nil {
@@ -112,13 +160,12 @@ func templatePush() *cobra.Command {
112160
},
113161
}
114162

115-
currentDirectory, _ := os.Getwd()
116-
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
117163
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
118164
cmd.Flags().StringVarP(&parameterFile, "parameter-file", "", "", "Specify a file path with parameter values.")
119165
cmd.Flags().StringVarP(&versionName, "name", "", "", "Specify a name for the new template version. It will be automatically generated if not provided.")
120166
cmd.Flags().StringArrayVarP(&provisionerTags, "provisioner-tag", "", []string{}, "Specify a set of tags to target provisioner daemons.")
121167
cmd.Flags().BoolVar(&alwaysPrompt, "always-prompt", false, "Always prompt all parameters. Does not pull parameter values from active template version")
168+
uploadFlags.register(cmd.Flags())
122169
cliui.AllowSkipPrompt(cmd)
123170
// This is for testing!
124171
err := cmd.Flags().MarkHidden("test.provisioner")

0 commit comments

Comments
 (0)