Skip to content

feat(cli): allow direct tar upload in template update/create #5720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 16 additions & 39 deletions cli/templatecreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"time"
"unicode/utf8"

"github.com/briandowns/spinner"
"github.com/google/uuid"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
Expand All @@ -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]",
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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{
Expand All @@ -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(&parameterFile, "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 {
Expand Down
41 changes: 37 additions & 4 deletions cli/templatecreate_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli_test

import (
"bytes"
"os"
"testing"

Expand Down Expand Up @@ -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"},
Expand All @@ -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})
Expand All @@ -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"},
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -188,7 +221,7 @@ func TestTemplateCreate(t *testing.T) {
write string
}{
{
match: "Create and upload",
match: "Upload",
write: "yes",
},
{
Expand Down
101 changes: 74 additions & 27 deletions cli/templatepush.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{
Expand All @@ -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 {
Expand Down Expand Up @@ -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(&parameterFile, "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")
Expand Down
Loading