fix: Group subcommands for cognitive ease (#1351)

This commit is contained in:
Kyle Carberry 2022-05-09 17:42:02 -05:00 committed by GitHub
parent 20caee1502
commit ddb9631d7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 89 additions and 271 deletions

View File

@ -18,11 +18,12 @@ The default schedule is at 09:00 in your local timezone (TZ env, UTC by default)
func autostart() *cobra.Command {
autostartCmd := &cobra.Command{
Use: "autostart enable <workspace>",
Short: "schedule a workspace to automatically start at a regular time",
Long: autostartDescriptionLong,
Example: "coder autostart enable my-workspace --minute 30 --hour 9 --days 1-5 --tz Europe/Dublin",
Hidden: true,
Annotations: workspaceCommand,
Use: "autostart enable <workspace>",
Short: "schedule a workspace to automatically start at a regular time",
Long: autostartDescriptionLong,
Example: "coder autostart enable my-workspace --minute 30 --hour 9 --days 1-5 --tz Europe/Dublin",
Hidden: true,
}
autostartCmd.AddCommand(autostartEnable())

View File

@ -36,7 +36,9 @@ func configSSH() *cobra.Command {
skipProxyCommand bool
)
cmd := &cobra.Command{
Use: "config-ssh",
Annotations: workspaceCommand,
Use: "config-ssh",
Short: "Populate your SSH config with Host entries for all of your workspaces",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -20,8 +20,9 @@ func create() *cobra.Command {
templateName string
)
cmd := &cobra.Command{
Use: "create [name]",
Short: "Create a workspace from a template",
Annotations: workspaceCommand,
Use: "create [name]",
Short: "Create a workspace from a template",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -13,9 +13,11 @@ import (
// nolint
func delete() *cobra.Command {
return &cobra.Command{
Use: "delete <workspace>",
Aliases: []string{"rm"},
Args: cobra.ExactArgs(1),
Annotations: workspaceCommand,
Use: "delete <workspace>",
Short: "Delete a workspace",
Aliases: []string{"rm"},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -13,8 +13,10 @@ import (
func list() *cobra.Command {
return &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Annotations: workspaceCommand,
Use: "list",
Short: "List all workspaces",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -37,8 +37,9 @@ func init() {
func login() *cobra.Command {
return &cobra.Command{
Use: "login <url>",
Args: cobra.ExactArgs(1),
Use: "login <url>",
Short: "Authenticate with a Coder deployment",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
rawURL := args[0]

View File

@ -1,75 +0,0 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
)
func parameterCreate() *cobra.Command {
var (
name string
value string
scheme string
)
cmd := &cobra.Command{
Use: "create <scope> [name]",
Aliases: []string{"mk"},
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
scopeName := ""
if len(args) >= 2 {
scopeName = args[1]
}
scope, scopeID, err := parseScopeAndID(cmd.Context(), client, organization, args[0], scopeName)
if err != nil {
return err
}
scheme, err := parseParameterScheme(scheme)
if err != nil {
return err
}
_, err = client.CreateParameter(cmd.Context(), scope, scopeID, codersdk.CreateParameterRequest{
Name: name,
SourceValue: value,
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: scheme,
})
if err != nil {
return err
}
_, _ = fmt.Printf("Created!\n")
return nil
},
}
cmd.Flags().StringVarP(&name, "name", "n", "", "Name for a parameter.")
_ = cmd.MarkFlagRequired("name")
cmd.Flags().StringVarP(&value, "value", "v", "", "Value for a parameter.")
_ = cmd.MarkFlagRequired("value")
cmd.Flags().StringVarP(&scheme, "scheme", "s", "var", `Scheme for the parameter ("var" or "env").`)
return cmd
}
func parseParameterScheme(scheme string) (database.ParameterDestinationScheme, error) {
switch scheme {
case "env":
return database.ParameterDestinationSchemeEnvironmentVariable, nil
case "var":
return database.ParameterDestinationSchemeProvisionerVariable, nil
}
return database.ParameterDestinationSchemeNone, xerrors.Errorf("scheme %q not recognized", scheme)
}

View File

@ -1,13 +0,0 @@
package cli
import "github.com/spf13/cobra"
func parameterDelete() *cobra.Command {
return &cobra.Command{
Use: "delete",
Aliases: []string{"rm"},
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
}

View File

@ -1,50 +0,0 @@
package cli
import (
"fmt"
"text/tabwriter"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
func parameterList() *cobra.Command {
return &cobra.Command{
Use: "list <scope> <scope-id>",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
name := ""
if len(args) >= 2 {
name = args[1]
}
scope, scopeID, err := parseScopeAndID(cmd.Context(), client, organization, args[0], name)
if err != nil {
return err
}
params, err := client.Parameters(cmd.Context(), scope, scopeID)
if err != nil {
return err
}
writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 4, ' ', 0)
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\n",
color.HiBlackString("Parameter"),
color.HiBlackString("Created"),
color.HiBlackString("Scheme"))
for _, param := range params {
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\n",
color.New(color.FgHiCyan).Sprint(param.Name),
color.WhiteString(param.UpdatedAt.Format("January 2, 2006")),
color.New(color.FgHiWhite).Sprint(param.DestinationScheme))
}
return writer.Flush()
},
}
}

View File

@ -1,78 +0,0 @@
package cli
import (
"context"
"github.com/google/uuid"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/codersdk"
)
func parameters() *cobra.Command {
cmd := &cobra.Command{
Use: "parameters",
Aliases: []string{"params"},
}
cmd.AddCommand(parameterCreate(), parameterList(), parameterDelete())
return cmd
}
func parseScopeAndID(ctx context.Context, client *codersdk.Client, organization codersdk.Organization, rawScope string, name string) (codersdk.ParameterScope, uuid.UUID, error) {
scope, err := parseParameterScope(rawScope)
if err != nil {
return scope, uuid.Nil, err
}
var scopeID uuid.UUID
switch scope {
case codersdk.ParameterOrganization:
if name == "" {
scopeID = organization.ID
} else {
org, err := client.OrganizationByName(ctx, codersdk.Me, name)
if err != nil {
return scope, uuid.Nil, err
}
scopeID = org.ID
}
case codersdk.ParameterTemplate:
template, err := client.TemplateByName(ctx, organization.ID, name)
if err != nil {
return scope, uuid.Nil, err
}
scopeID = template.ID
case codersdk.ParameterUser:
uid, _ := uuid.Parse(name)
user, err := client.User(ctx, uid)
if err != nil {
return scope, uuid.Nil, err
}
scopeID = user.ID
case codersdk.ParameterWorkspace:
workspace, err := client.WorkspaceByOwnerAndName(ctx, organization.ID, codersdk.Me, name)
if err != nil {
return scope, uuid.Nil, err
}
scopeID = workspace.ID
}
return scope, scopeID, nil
}
func parseParameterScope(scope string) (codersdk.ParameterScope, error) {
switch scope {
case string(codersdk.ParameterOrganization):
return codersdk.ParameterOrganization, nil
case string(codersdk.ParameterTemplate):
return codersdk.ParameterTemplate, nil
case string(codersdk.ParameterUser):
return codersdk.ParameterUser, nil
case string(codersdk.ParameterWorkspace):
return codersdk.ParameterWorkspace, nil
}
return codersdk.ParameterOrganization, xerrors.Errorf("no scope found by name %q", scope)
}

View File

@ -14,6 +14,7 @@ func publickey() *cobra.Command {
return &cobra.Command{
Use: "publickey",
Aliases: []string{"pubkey"},
Short: "Output your public key for Git operations",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -3,10 +3,8 @@ package cli
import (
"net/url"
"os"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/kirsle/configdir"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
@ -20,6 +18,12 @@ import (
var (
caret = cliui.Styles.Prompt.String()
// Applied as annotations to workspace commands
// so they display in a separated "help" section.
workspaceCommand = map[string]string{
"workspaces": " ",
}
)
const (
@ -38,33 +42,14 @@ func Root() *cobra.Command {
Version: buildinfo.Version(),
SilenceErrors: true,
SilenceUsage: true,
Long: `
` + lipgloss.NewStyle().Underline(true).Render("Self-hosted developer workspaces on your infra") + `
Long: `Coder A tool for provisioning self-hosted development environments.
`,
Example: cliui.Styles.Paragraph.Render(`Start Coder in "dev" mode. This dev-mode requires no further setup, and your local `+cliui.Styles.Code.Render("coder")+` CLI will be authenticated to talk to it. This makes it easy to experiment with Coder.`) + `
Example: ` Start Coder in "dev" mode. This dev-mode requires no further setup, and your local ` + cliui.Styles.Code.Render("coder") + ` CLI will be authenticated to talk to it. This makes it easy to experiment with Coder.
` + cliui.Styles.Code.Render("$ coder server --dev") + `
` + cliui.Styles.Code.Render("$ coder server --dev") + `
` + cliui.Styles.Paragraph.Render("Get started by creating a template from an example.") + `
` + cliui.Styles.Code.Render("$ coder templates init"),
Get started by creating a template from an example.
` + cliui.Styles.Code.Render("$ coder templates init"),
}
// Customizes the color of headings to make subcommands
// more visually appealing.
header := cliui.Styles.Placeholder
cmd.SetUsageTemplate(strings.NewReplacer(
`Usage:`, header.Render("Usage:"),
`Examples:`, header.Render("Examples:"),
`Available Commands:`, header.Render("Commands:"),
`Global Flags:`, header.Render("Global Flags:"),
`Flags:`, header.Render("Flags:"),
`Additional help topics:`, header.Render("Additional help:"),
).Replace(cmd.UsageTemplate()))
cmd.SetVersionTemplate(versionTemplate())
cmd.AddCommand(
autostart(),
@ -75,7 +60,6 @@ func Root() *cobra.Command {
gitssh(),
list(),
login(),
parameters(),
publickey(),
server(),
show(),
@ -90,6 +74,9 @@ func Root() *cobra.Command {
workspaceAgent(),
)
cmd.SetUsageTemplate(usageTemplate())
cmd.SetVersionTemplate(versionTemplate())
cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.")
cmd.PersistentFlags().String(varToken, "", "Specify an authentication token.")
cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.")
@ -188,6 +175,27 @@ func isTTY(cmd *cobra.Command) bool {
return isatty.IsTerminal(file.Fd())
}
func usageTemplate() string {
// Customizes the color of headings to make subcommands
// more visually appealing.
header := cliui.Styles.Placeholder
return `{{if .HasExample}}` + header.Render("Get Started:") + `
{{.Example}}
{{end}}{{if .HasAvailableLocalFlags}}` + header.Render("Flags:") + `
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableSubCommands}}
` + header.Render("Commands:") + `{{range .Commands}}{{if and .IsAvailableCommand (eq (len .Annotations) 0)}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .HasParent }}
` + header.Render("Workspace Commands:") + `{{range .Commands}}{{if and .IsAvailableCommand (ne (index .Annotations "workspaces") "")}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}
Use "{{.CommandPath}} [command] --help" for more information about a command.
`
}
func versionTemplate() string {
template := `Coder {{printf "%s" .Version}}`
buildTime, valid := buildinfo.Time()

View File

@ -89,7 +89,8 @@ func server() *cobra.Command {
)
root := &cobra.Command{
Use: "server",
Use: "server",
Short: "Start a Coder server",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Make(sloghuman.Sink(os.Stderr))
if verbose {

View File

@ -10,8 +10,10 @@ import (
func show() *cobra.Command {
return &cobra.Command{
Use: "show",
Args: cobra.ExactArgs(1),
Annotations: workspaceCommand,
Use: "show",
Short: "Show details of a workspace's resources and agents",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -26,8 +26,10 @@ func ssh() *cobra.Command {
stdio bool
)
cmd := &cobra.Command{
Use: "ssh <workspace>",
Args: cobra.MinimumNArgs(1),
Annotations: workspaceCommand,
Use: "ssh <workspace>",
Short: "SSH into a workspace",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -12,8 +12,10 @@ import (
func start() *cobra.Command {
return &cobra.Command{
Use: "start <workspace>",
Args: cobra.ExactArgs(1),
Annotations: workspaceCommand,
Use: "start <workspace>",
Short: "Build a workspace with the start state",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -13,7 +13,8 @@ import (
func state() *cobra.Command {
cmd := &cobra.Command{
Use: "state",
Use: "state",
Short: "Manually manage Terraform state to fix broken workspaces",
}
cmd.AddCommand(statePull(), statePush())
return cmd

View File

@ -12,8 +12,10 @@ import (
func stop() *cobra.Command {
return &cobra.Command{
Use: "stop <workspace>",
Args: cobra.ExactArgs(1),
Annotations: workspaceCommand,
Use: "stop <workspace>",
Short: "Build a workspace with the stop state",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -8,6 +8,7 @@ import (
func templates() *cobra.Command {
cmd := &cobra.Command{
Use: "templates",
Short: "Create, manage, and deploy templates",
Aliases: []string{"template"},
Example: `
- Create a template for developers to create workspaces

View File

@ -4,7 +4,9 @@ import "github.com/spf13/cobra"
func tunnel() *cobra.Command {
return &cobra.Command{
Use: "tunnel",
Annotations: workspaceCommand,
Use: "tunnel",
Short: "Forward ports to your local machine",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},

View File

@ -11,7 +11,9 @@ import (
func update() *cobra.Command {
return &cobra.Command{
Use: "update",
Annotations: workspaceCommand,
Use: "update",
Short: "Update a workspace to the latest template version",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {

View File

@ -4,7 +4,8 @@ import "github.com/spf13/cobra"
func users() *cobra.Command {
cmd := &cobra.Command{
Use: "users",
Short: "Create, remove, and list users",
Use: "users",
}
cmd.AddCommand(userCreate(), userList())
return cmd