diff --git a/ci/integration/images_test.go b/ci/integration/images_test.go new file mode 100644 index 00000000..e796fd61 --- /dev/null +++ b/ci/integration/images_test.go @@ -0,0 +1,43 @@ +package integration + +import ( + "context" + "regexp" + "testing" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/pkg/tcli" +) + +func TestImagesCLI(t *testing.T) { + t.Parallel() + + run(t, "coder-cli-images-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { + headlessLogin(ctx, t, c) + + // Successfully output help. + c.Run(ctx, "coder images --help").Assert(t, + tcli.Success(), + tcli.StdoutMatches(regexp.QuoteMeta("Manage existing images and/or import new ones.")), + tcli.StderrEmpty(), + ) + + // OK - human output + c.Run(ctx, "coder images ls").Assert(t, + tcli.Success(), + ) + + imgs := []coder.Image{} + // OK - json output + c.Run(ctx, "coder images ls --output json").Assert(t, + tcli.Success(), + tcli.StdoutJSONUnmarshal(&imgs), + ) + + // Org not found + c.Run(ctx, "coder images ls --org doesntexist").Assert(t, + tcli.Error(), + tcli.StderrMatches(regexp.QuoteMeta("org name \"doesntexist\" not found\n\n")), + ) + }) +} diff --git a/coder-sdk/image.go b/coder-sdk/image.go index 78fad426..2e656ea8 100644 --- a/coder-sdk/image.go +++ b/coder-sdk/image.go @@ -8,19 +8,19 @@ import ( // Image describes a Coder Image. type Image struct { - ID string `json:"id"` - OrganizationID string `json:"organization_id"` - Repository string `json:"repository"` - Description string `json:"description"` - URL string `json:"url"` // User-supplied URL for image. - Registry *Registry `json:"registry"` - DefaultTag *ImageTag `json:"default_tag"` - DefaultCPUCores float32 `json:"default_cpu_cores"` - DefaultMemoryGB float32 `json:"default_memory_gb"` - DefaultDiskGB int `json:"default_disk_gb"` - Deprecated bool `json:"deprecated"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id" table:"-"` + OrganizationID string `json:"organization_id" table:"-"` + Repository string `json:"repository" table:"Repository"` + Description string `json:"description" table:"-"` + URL string `json:"url" table:"-"` // User-supplied URL for image. + Registry *Registry `json:"registry" table:"-"` + DefaultTag *ImageTag `json:"default_tag" table:"DefaultTag"` + DefaultCPUCores float32 `json:"default_cpu_cores" table:"DefaultCPUCores"` + DefaultMemoryGB float32 `json:"default_memory_gb" table:"DefaultMemoryGB"` + DefaultDiskGB int `json:"default_disk_gb" table:"DefaultDiskGB"` + Deprecated bool `json:"deprecated" table:"-"` + CreatedAt time.Time `json:"created_at" table:"-"` + UpdatedAt time.Time `json:"updated_at" table:"-"` } // NewRegistryRequest describes a docker registry used in importing an image. diff --git a/coder-sdk/tags.go b/coder-sdk/tags.go index 8c7282a9..f5384dc7 100644 --- a/coder-sdk/tags.go +++ b/coder-sdk/tags.go @@ -18,6 +18,10 @@ type ImageTag struct { CreatedAt time.Time `json:"created_at" table:"-"` } +func (i ImageTag) String() string { + return i.Tag +} + // OSRelease is the marshalled /etc/os-release file. type OSRelease struct { ID string `json:"id"` diff --git a/docs/coder.md b/docs/coder.md index e268d312..d83ee5eb 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -14,6 +14,7 @@ coder provides a CLI for working with an existing Coder Enterprise installation * [coder completion](coder_completion.md) - Generate completion script * [coder config-ssh](coder_config-ssh.md) - Configure SSH to access Coder environments * [coder envs](coder_envs.md) - Interact with Coder environments +* [coder images](coder_images.md) - Manage Coder images * [coder login](coder_login.md) - Authenticate this client for future operations * [coder logout](coder_logout.md) - Remove local authentication credentials if any exist * [coder sh](coder_sh.md) - Open a shell and execute commands in a Coder environment diff --git a/docs/coder_images.md b/docs/coder_images.md new file mode 100644 index 00000000..386041c7 --- /dev/null +++ b/docs/coder_images.md @@ -0,0 +1,26 @@ +## coder images + +Manage Coder images + +### Synopsis + +Manage existing images and/or import new ones. + +### Options + +``` + -h, --help help for images + --user string Specifies the user by email (default "me") +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation +* [coder images ls](coder_images_ls.md) - list all images available to the active user + diff --git a/docs/coder_images_ls.md b/docs/coder_images_ls.md new file mode 100644 index 00000000..bfb646d5 --- /dev/null +++ b/docs/coder_images_ls.md @@ -0,0 +1,31 @@ +## coder images ls + +list all images available to the active user + +### Synopsis + +List all Coder images available to the active user. + +``` +coder images ls [flags] +``` + +### Options + +``` + -h, --help help for ls + --org string organization name + --output string human | json (default "human") +``` + +### Options inherited from parent commands + +``` + --user string Specifies the user by email (default "me") + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder images](coder_images.md) - Manage Coder images + diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index e7b8aa63..433266f8 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -35,6 +35,7 @@ func Make() *cobra.Command { tokensCmd(), resourceCmd(), completionCmd(), + imgsCmd(), genDocsCmd(app), ) app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") diff --git a/internal/cmd/images.go b/internal/cmd/images.go new file mode 100644 index 00000000..8e9679ce --- /dev/null +++ b/internal/cmd/images.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "encoding/json" + "os" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/pkg/clog" + "cdr.dev/coder-cli/pkg/tablewriter" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +func imgsCmd() *cobra.Command { + var user string + + cmd := &cobra.Command{ + Use: "images", + Short: "Manage Coder images", + Long: "Manage existing images and/or import new ones.", + } + + cmd.PersistentFlags().StringVar(&user, "user", coder.Me, "Specifies the user by email") + cmd.AddCommand(lsImgsCommand(&user)) + return cmd +} + +func lsImgsCommand(user *string) *cobra.Command { + var ( + orgName string + outputFmt string + ) + + cmd := &cobra.Command{ + Use: "ls", + Short: "list all images available to the active user", + Long: "List all Coder images available to the active user.", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := newClient(ctx) + if err != nil { + return err + } + + imgs, err := getImgs(ctx, client, + getImgsConf{ + email: *user, + orgName: orgName, + }, + ) + + if err != nil { + return err + } + + if len(imgs) < 1 { + clog.LogInfo("no images found") + imgs = []coder.Image{} // ensures that json output still marshals + } + + switch outputFmt { + case jsonOutput: + enc := json.NewEncoder(os.Stdout) + // pretty print the json + enc.SetIndent("", "\t") + + if err := enc.Encode(imgs); err != nil { + return xerrors.Errorf("write images as JSON: %w", err) + } + return nil + case humanOutput: + err = tablewriter.WriteTable(len(imgs), func(i int) interface{} { + return imgs[i] + }) + if err != nil { + return xerrors.Errorf("write table: %w", err) + } + return nil + default: + return xerrors.Errorf("%q is not a supported value for --output", outputFmt) + } + }, + } + cmd.Flags().StringVar(&orgName, "org", "", "organization name") + cmd.Flags().StringVar(&outputFmt, "output", humanOutput, "human | json") + return cmd +}