Files
coder/cli/templateversions.go
T
Rowan Smith 1c4dd78b05 chore: add id to template version output columns (#22163)
At present it is not possible to obtain the `id` of the template version
in the table output:

```
➜  ~ coder templates version list -h                
coder v2.30.1+16408b1

USAGE:
  coder templates versions list [flags] <template>

  List all the versions of the specified template

OPTIONS:
  -O, --org string, $CODER_ORGANIZATION
          Select which organization (uuid or name) to use.

  -c, --column [name|created at|created by|status|active|archived] (default: name,created at,created by,status,active)
          Columns to display in table output.

➜  ~ coder templates version list aws-linux-dynamic 
NAME                 CREATED AT                 CREATED BY  STATUS     ACTIVE  
infallible_feistel2  2025-10-10T10:34:02+11:00  rowansmith  Succeeded  Active  
mystifying_almeida1  2025-10-10T10:32:38+11:00  rowansmith  Succeeded         
```

Adding this because it is useful when wanting to programatically
retrieve the details of the latest template version, and `-ojson` does
not include `active` details in it's output.

```
➜  Downloads ./coder-cli-templateversions-list-id templates version list -h                
coder v2.30.1-devel+bab99db9e7

USAGE:
  coder templates versions list [flags] <template>

  List all the versions of the specified template

OPTIONS:
  -O, --org string, $CODER_ORGANIZATION
          Select which organization (uuid or name) to use.

  -c, --column [id|name|created at|created by|status|active|archived] (default: name,created at,created by,status,active)
          Columns to display in table output.

      --include-archived bool
          Include archived versions in the result list.

  -o, --output table|json (default: table)
          Output format.

———
Run `coder --help` for a list of global options.

➜  Downloads ./coder-cli-templateversions-list-id templates version list aws-linux-dynamic -c id,name,'created at','created by',status,active
ID                                    NAME                 CREATED AT                 CREATED BY  STATUS     ACTIVE  
38f66eae-ec63-49b7-a9d2-cdb79c379d19  infallible_feistel2  2025-10-10T10:34:02+11:00  rowansmith  Succeeded  Active  
aa797ea5-4221-461b-80b0-90c5164f8dc0  mystifying_almeida1  2025-10-10T10:32:38+11:00  rowansmith  Succeeded
```
2026-02-18 16:47:45 +11:00

245 lines
6.8 KiB
Go

package cli
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) templateVersions() *serpent.Command {
cmd := &serpent.Command{
Use: "versions",
Short: "Manage different versions of the specified template",
Aliases: []string{"version"},
Long: FormatExamples(
Example{
Description: "List versions of a specific template",
Command: "coder templates versions list my-template",
},
),
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*serpent.Command{
r.templateVersionsList(),
r.archiveTemplateVersion(),
r.unarchiveTemplateVersion(),
r.templateVersionsPromote(),
},
}
return cmd
}
func (r *RootCmd) templateVersionsList() *serpent.Command {
defaultColumns := []string{
"name",
"created at",
"created by",
"status",
"active",
}
formatter := cliui.NewOutputFormatter(
cliui.TableFormat([]templateVersionRow{}, defaultColumns),
cliui.JSONFormat(),
)
orgContext := NewOrganizationContext()
var includeArchived serpent.Bool
cmd := &serpent.Command{
Use: "list <template>",
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(i *serpent.Invocation) error {
// This is the only way to dynamically add the "archived"
// column if '--include-archived' is true.
// It does not make sense to show this column if the
// flag is false.
if includeArchived {
for _, opt := range i.Command.Options {
if opt.Flag == "column" {
if opt.ValueSource == serpent.ValueSourceDefault {
v, ok := opt.Value.(*serpent.EnumArray)
if ok {
// Add the extra new default column.
_ = v.Append("Archived")
}
}
break
}
}
}
return next(i)
}
},
),
Short: "List all the versions of the specified template",
Options: serpent.OptionSet{
{
Name: "include-archived",
Description: "Include archived versions in the result list.",
Flag: "include-archived",
Value: &includeArchived,
},
},
Handler: func(inv *serpent.Invocation) error {
client, err := r.InitClient(inv)
if err != nil {
return err
}
organization, err := orgContext.Selected(inv, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}
template, err := client.TemplateByName(inv.Context(), organization.ID, inv.Args[0])
if err != nil {
return xerrors.Errorf("get template by name: %w", err)
}
req := codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
IncludeArchived: includeArchived.Value(),
}
versions, err := client.TemplateVersionsByTemplate(inv.Context(), req)
if err != nil {
return xerrors.Errorf("get template versions by template: %w", err)
}
rows := templateVersionsToRows(template.ActiveVersionID, versions...)
out, err := formatter.Format(inv.Context(), rows)
if err != nil {
return xerrors.Errorf("render table: %w", err)
}
if out == "" {
cliui.Infof(inv.Stderr, "No template versions found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}
orgContext.AttachOptions(cmd)
formatter.AttachOptions(&cmd.Options)
return cmd
}
type templateVersionRow struct {
// For json format:
TemplateVersion codersdk.TemplateVersion `table:"-"`
// For table format:
ID string `json:"-" table:"id"`
Name string `json:"-" table:"name,default_sort"`
CreatedAt time.Time `json:"-" table:"created at"`
CreatedBy string `json:"-" table:"created by"`
Status string `json:"-" table:"status"`
Active string `json:"-" table:"active"`
Archived string `json:"-" table:"archived"`
}
// templateVersionsToRows converts a list of template versions to a list of rows
// for outputting.
func templateVersionsToRows(activeVersionID uuid.UUID, templateVersions ...codersdk.TemplateVersion) []templateVersionRow {
rows := make([]templateVersionRow, len(templateVersions))
for i, templateVersion := range templateVersions {
activeStatus := ""
if templateVersion.ID == activeVersionID {
activeStatus = cliui.Keyword("Active")
}
archivedStatus := ""
if templateVersion.Archived {
archivedStatus = pretty.Sprint(cliui.DefaultStyles.Warn, "Archived")
}
rows[i] = templateVersionRow{
TemplateVersion: templateVersion,
ID: templateVersion.ID.String(),
Name: templateVersion.Name,
CreatedAt: templateVersion.CreatedAt,
CreatedBy: templateVersion.CreatedBy.Username,
Status: strings.Title(string(templateVersion.Job.Status)),
Active: activeStatus,
Archived: archivedStatus,
}
}
return rows
}
func (r *RootCmd) templateVersionsPromote() *serpent.Command {
var (
templateName string
templateVersionName string
orgContext = NewOrganizationContext()
)
cmd := &serpent.Command{
Use: "promote --template=<template_name> --template-version=<template_version_name>",
Short: "Promote a template version to active.",
Long: "Promote an existing template version to be the active version for the specified template.",
Handler: func(inv *serpent.Invocation) error {
client, err := r.InitClient(inv)
if err != nil {
return err
}
organization, err := orgContext.Selected(inv, client)
if err != nil {
return err
}
template, err := client.TemplateByName(inv.Context(), organization.ID, templateName)
if err != nil {
return xerrors.Errorf("get template by name: %w", err)
}
version, err := client.TemplateVersionByName(inv.Context(), template.ID, templateVersionName)
if err != nil {
return xerrors.Errorf("get template version by name: %w", err)
}
err = client.UpdateActiveTemplateVersion(inv.Context(), template.ID, codersdk.UpdateActiveTemplateVersion{
ID: version.ID,
})
if err != nil {
return xerrors.Errorf("update active template version: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Successfully promoted version %q to active for template %q\n", templateVersionName, templateName)
return nil
},
}
cmd.Options = serpent.OptionSet{
{
Flag: "template",
FlagShorthand: "t",
Env: "CODER_TEMPLATE_NAME",
Description: "Specify the template name.",
Required: true,
Value: serpent.StringOf(&templateName),
},
{
Flag: "template-version",
Description: "Specify the template version name to promote.",
Env: "CODER_TEMPLATE_VERSION_NAME",
Required: true,
Value: serpent.StringOf(&templateVersionName),
},
}
orgContext.AttachOptions(cmd)
return cmd
}