diff --git a/cli/templatepull.go b/cli/templatepull.go index eb772379b9..13286ab033 100644 --- a/cli/templatepull.go +++ b/cli/templatepull.go @@ -15,12 +15,15 @@ import ( ) func (r *RootCmd) templatePull() *clibase.Cmd { - var tarMode bool + var ( + tarMode bool + versionName string + ) client := new(codersdk.Client) cmd := &clibase.Cmd{ Use: "pull [destination]", - Short: "Download the latest version of a template to a path.", + Short: "Download the active, latest, or specified version of a template to a path.", Middleware: clibase.Chain( clibase.RequireRangeArgs(1, 2), r.InitClient(client), @@ -36,39 +39,67 @@ func (r *RootCmd) templatePull() *clibase.Cmd { dest = inv.Args[1] } - // TODO(JonA): Do we need to add a flag for organization? organization, err := CurrentOrganization(inv, client) if err != nil { - return xerrors.Errorf("current organization: %w", err) + return xerrors.Errorf("get current organization: %w", err) } template, err := client.TemplateByName(ctx, organization.ID, templateName) if err != nil { - return xerrors.Errorf("template by name: %w", err) + return xerrors.Errorf("get template by name: %w", err) } - // Pull the versions for the template. We'll find the latest - // one and download the source. - versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{ - TemplateID: template.ID, - }) - if err != nil { - return xerrors.Errorf("template versions by template: %w", err) + var latestVersion codersdk.TemplateVersion + { + // Determine the latest template version and compare with the + // active version. If they aren't the same, warn the user. + versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + }) + if err != nil { + return xerrors.Errorf("template versions by template: %w", err) + } + + if len(versions) == 0 { + return xerrors.Errorf("no template versions for template %q", templateName) + } + + // Sort the slice from newest to oldest template. + sort.SliceStable(versions, func(i, j int) bool { + return versions[i].CreatedAt.After(versions[j].CreatedAt) + }) + + latestVersion = versions[0] } - if len(versions) == 0 { - return xerrors.Errorf("no template versions for template %q", templateName) + var templateVersion codersdk.TemplateVersion + switch versionName { + case "", "active": + activeVersion, err := client.TemplateVersion(ctx, template.ActiveVersionID) + if err != nil { + return xerrors.Errorf("get active template version: %w", err) + } + if versionName == "" && activeVersion.ID != latestVersion.ID { + cliui.Warn(inv.Stderr, + "A newer template version than the active version exists. Pulling the active version instead.", + "Use "+cliui.Code("--template latest")+" to pull the latest version.", + ) + } + templateVersion = activeVersion + case "latest": + templateVersion = latestVersion + default: + version, err := client.TemplateVersionByName(ctx, template.ID, versionName) + if err != nil { + return xerrors.Errorf("get template version: %w", err) + } + templateVersion = version } - // Sort the slice from newest to oldest template. - sort.SliceStable(versions, func(i, j int) bool { - return versions[i].CreatedAt.After(versions[j].CreatedAt) - }) - - latest := versions[0] + cliui.Info(inv.Stderr, "Pulling template version "+cliui.Bold(templateVersion.Name)+"...") // Download the tar archive. - raw, ctype, err := client.Download(ctx, latest.Job.FileID) + raw, ctype, err := client.Download(ctx, templateVersion.Job.FileID) if err != nil { return xerrors.Errorf("download template: %w", err) } @@ -121,6 +152,12 @@ func (r *RootCmd) templatePull() *clibase.Cmd { Value: clibase.BoolOf(&tarMode), }, + { + Description: "The name of the template version to pull. Use 'active' to pull the active version, 'latest' to pull the latest version, or the name of the template version to pull.", + Flag: "version", + + Value: clibase.StringOf(&versionName), + }, cliui.SkipPromptOption(), } diff --git a/cli/templatepull_test.go b/cli/templatepull_test.go index 7d22f31f74..01dfa1a9ec 100644 --- a/cli/templatepull_test.go +++ b/cli/templatepull_test.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "os" "path/filepath" + "strings" "testing" "github.com/codeclysm/extract/v3" @@ -49,7 +50,7 @@ func TestTemplatePull_NoName(t *testing.T) { require.Error(t, err) } -// Stdout tests that 'templates pull' pulls down the latest template +// Stdout tests that 'templates pull' pulls down the active template // and writes it to stdout. func TestTemplatePull_Stdout(t *testing.T) { t.Parallel() @@ -78,6 +79,7 @@ func TestTemplatePull_Stdout(t *testing.T) { // are being sorted correctly. updatedVersion := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, source2, template.ID) _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, updatedVersion.ID) + coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, updatedVersion.ID) inv, root := clitest.New(t, "templates", "pull", "--tar", template.Name) clitest.SetupConfig(t, templateAdmin, root) @@ -91,7 +93,123 @@ func TestTemplatePull_Stdout(t *testing.T) { require.True(t, bytes.Equal(expected, buf.Bytes()), "tar files differ") } -// ToDir tests that 'templates pull' pulls down the latest template +// Stdout tests that 'templates pull' pulls down the non-latest active template +// and writes it to stdout. +func TestTemplatePull_ActiveOldStdout(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + + source1 := genTemplateVersionSource() + source2 := genTemplateVersionSource() + + expected, err := echo.Tar(source1) + require.NoError(t, err) + + version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) + + updatedVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, updatedVersion.ID) + + inv, root := clitest.New(t, "templates", "pull", "--tar", template.Name) + clitest.SetupConfig(t, client, root) + + var buf bytes.Buffer + inv.Stdout = &buf + var stderr strings.Builder + inv.Stderr = &stderr + + err = inv.Run() + require.NoError(t, err) + + require.True(t, bytes.Equal(expected, buf.Bytes()), "tar files differ") + require.Contains(t, stderr.String(), "A newer template version than the active version exists.") +} + +// Stdout tests that 'templates pull' pulls down the specified template and +// writes it to stdout. +func TestTemplatePull_SpecifiedStdout(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + + source1 := genTemplateVersionSource() + source2 := genTemplateVersionSource() + source3 := genTemplateVersionSource() + + expected, err := echo.Tar(source1) + require.NoError(t, err) + + version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) + + updatedVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, updatedVersion.ID) + + updatedVersion2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source3, template.ID) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, updatedVersion2.ID) + coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, updatedVersion2.ID) + + inv, root := clitest.New(t, "templates", "pull", "--tar", template.Name, "--version", version1.Name) + clitest.SetupConfig(t, client, root) + + var buf bytes.Buffer + inv.Stdout = &buf + + err = inv.Run() + require.NoError(t, err) + + require.True(t, bytes.Equal(expected, buf.Bytes()), "tar files differ") +} + +// Stdout tests that 'templates pull' pulls down the latest template +// and writes it to stdout. +func TestTemplatePull_LatestStdout(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + + source1 := genTemplateVersionSource() + source2 := genTemplateVersionSource() + + expected, err := echo.Tar(source1) + require.NoError(t, err) + + version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) + + updatedVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, updatedVersion.ID) + + inv, root := clitest.New(t, "templates", "pull", "--tar", template.Name, "latest") + clitest.SetupConfig(t, client, root) + + var buf bytes.Buffer + inv.Stdout = &buf + + err = inv.Run() + require.NoError(t, err) + + require.True(t, bytes.Equal(expected, buf.Bytes()), "tar files differ") +} + +// ToDir tests that 'templates pull' pulls down the active template // and writes it to the correct directory. func TestTemplatePull_ToDir(t *testing.T) { t.Parallel() @@ -120,6 +238,7 @@ func TestTemplatePull_ToDir(t *testing.T) { // are being sorted correctly. updatedVersion := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, source2, template.ID) _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, updatedVersion.ID) + coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, updatedVersion.ID) dir := t.TempDir() @@ -143,8 +262,9 @@ func TestTemplatePull_ToDir(t *testing.T) { ) } -// ToDir tests that 'templates pull' pulls down the latest template -// and writes it to a directory with the name of the template if the path is not implicitly supplied. +// ToDir tests that 'templates pull' pulls down the active template and writes +// it to a directory with the name of the template if the path is not implicitly +// supplied. // nolint: paralleltest func TestTemplatePull_ToImplicit(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ @@ -171,6 +291,7 @@ func TestTemplatePull_ToImplicit(t *testing.T) { // are being sorted correctly. updatedVersion := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, source2, template.ID) _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, updatedVersion.ID) + coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, updatedVersion.ID) // create a tempdir and change the working directory to it for the duration of the test (cannot run in parallel) dir := t.TempDir() @@ -233,6 +354,7 @@ func TestTemplatePull_FolderConflict(t *testing.T) { // are being sorted correctly. updatedVersion := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, source2, template.ID) _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, updatedVersion.ID) + coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, updatedVersion.ID) dir := t.TempDir() diff --git a/cli/testdata/coder_templates_--help.golden b/cli/testdata/coder_templates_--help.golden index ee1a558938..d4c7e25996 100644 --- a/cli/testdata/coder_templates_--help.golden +++ b/cli/testdata/coder_templates_--help.golden @@ -29,7 +29,8 @@ SUBCOMMANDS: edit Edit the metadata of a template by name. init Get started with a templated template. list List all the templates available for the organization - pull Download the latest version of a template to a path. + pull Download the active, latest, or specified version of a template + to a path. push Push a new template version from the current directory or as specified by flag versions Manage different versions of the specified template diff --git a/cli/testdata/coder_templates_pull_--help.golden b/cli/testdata/coder_templates_pull_--help.golden index 3036a5e497..65cb302a65 100644 --- a/cli/testdata/coder_templates_pull_--help.golden +++ b/cli/testdata/coder_templates_pull_--help.golden @@ -3,12 +3,17 @@ coder v0.0.0-devel USAGE: coder templates pull [flags] [destination] - Download the latest version of a template to a path. + Download the active, latest, or specified version of a template to a path. OPTIONS: --tar bool Output the template as a tar archive to stdout. + --version string + The name of the template version to pull. Use 'active' to pull the + active version, 'latest' to pull the latest version, or the name of + the template version to pull. + -y, --yes bool Bypass prompts. diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 2f58fa1209..1eb8be474a 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -753,11 +753,12 @@ func CreateTemplate(t *testing.T, client *codersdk.Client, organization uuid.UUI // UpdateTemplateVersion creates a new template version with the "echo" provisioner // and associates it with the given templateID. func UpdateTemplateVersion(t *testing.T, client *codersdk.Client, organizationID uuid.UUID, res *echo.Responses, templateID uuid.UUID) codersdk.TemplateVersion { + ctx := context.Background() data, err := echo.Tar(res) require.NoError(t, err) - file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, bytes.NewReader(data)) + file, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(data)) require.NoError(t, err) - templateVersion, err := client.CreateTemplateVersion(context.Background(), organizationID, codersdk.CreateTemplateVersionRequest{ + templateVersion, err := client.CreateTemplateVersion(ctx, organizationID, codersdk.CreateTemplateVersionRequest{ TemplateID: templateID, FileID: file.ID, StorageMethod: codersdk.ProvisionerStorageMethodFile, @@ -767,6 +768,13 @@ func UpdateTemplateVersion(t *testing.T, client *codersdk.Client, organizationID return templateVersion } +func UpdateActiveTemplateVersion(t *testing.T, client *codersdk.Client, templateID, versionID uuid.UUID) { + err := client.UpdateActiveTemplateVersion(context.Background(), templateID, codersdk.UpdateActiveTemplateVersion{ + ID: versionID, + }) + require.NoError(t, err) +} + // AwaitTemplateVersionJobRunning waits for the build to be picked up by a provisioner. func AwaitTemplateVersionJobRunning(t *testing.T, client *codersdk.Client, version uuid.UUID) codersdk.TemplateVersion { t.Helper() diff --git a/docs/cli/templates.md b/docs/cli/templates.md index 602b4c4fb6..410308b103 100644 --- a/docs/cli/templates.md +++ b/docs/cli/templates.md @@ -40,6 +40,6 @@ Templates are written in standard Terraform and describe the infrastructure for | [edit](./templates_edit.md) | Edit the metadata of a template by name. | | [init](./templates_init.md) | Get started with a templated template. | | [list](./templates_list.md) | List all the templates available for the organization | -| [pull](./templates_pull.md) | Download the latest version of a template to a path. | +| [pull](./templates_pull.md) | Download the active, latest, or specified version of a template to a path. | | [push](./templates_push.md) | Push a new template version from the current directory or as specified by flag | | [versions](./templates_versions.md) | Manage different versions of the specified template | diff --git a/docs/cli/templates_pull.md b/docs/cli/templates_pull.md index 8e0dd41d13..9ad51ab64c 100644 --- a/docs/cli/templates_pull.md +++ b/docs/cli/templates_pull.md @@ -2,7 +2,7 @@ # templates pull -Download the latest version of a template to a path. +Download the active, latest, or specified version of a template to a path. ## Usage @@ -20,6 +20,14 @@ coder templates pull [flags] [destination] Output the template as a tar archive to stdout. +### --version + +| | | +| ---- | ------------------- | +| Type | string | + +The name of the template version to pull. Use 'active' to pull the active version, 'latest' to pull the latest version, or the name of the template version to pull. + ### -y, --yes | | | diff --git a/docs/manifest.json b/docs/manifest.json index f0af5d4176..543171399a 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -838,7 +838,7 @@ }, { "title": "templates pull", - "description": "Download the latest version of a template to a path.", + "description": "Download the active, latest, or specified version of a template to a path.", "path": "cli/templates_pull.md" }, {