From d138de860ba4cbb5fe87ae3928f4668d4093277a Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 16 Oct 2020 22:22:31 -0500 Subject: [PATCH 1/6] Initial prototype of resources command --- coder-sdk/env.go | 12 +++- coder-sdk/org.go | 43 ++++++++++--- internal/cmd/ceapi.go | 6 +- internal/cmd/cmd.go | 1 + internal/cmd/resourcemanager.go | 106 ++++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 12 deletions(-) create mode 100644 internal/cmd/resourcemanager.go diff --git a/coder-sdk/env.go b/coder-sdk/env.go index 36214493..e506f0d7 100644 --- a/coder-sdk/env.go +++ b/coder-sdk/env.go @@ -21,7 +21,7 @@ type Environment struct { UserID string `json:"user_id" tab:"-"` LastBuiltAt time.Time `json:"last_built_at" tab:"-"` CPUCores float32 `json:"cpu_cores" tab:"CPUCores"` - MemoryGB int `json:"memory_gb" tab:"MemoryGB"` + MemoryGB float32 `json:"memory_gb" tab:"MemoryGB"` DiskGB int `json:"disk_gb" tab:"DiskGB"` GPUs int `json:"gpus" tab:"GPUs"` Updating bool `json:"updating" tab:"Updating"` @@ -93,6 +93,16 @@ func (c Client) CreateEnvironment(ctx context.Context, orgID string, req CreateE return &env, nil } +// ListEnvironments lists environments returned by the given filter. +// TODO: add the filter options +func (c Client) ListEnvironments(ctx context.Context) ([]Environment, error) { + var envs []Environment + if err := c.requestBody(ctx, http.MethodGet, "/api/environments", nil, &envs); err != nil { + return nil, err + } + return envs, nil +} + // EnvironmentsByOrganization gets the list of environments owned by the given user. func (c Client) EnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) { var envs []Environment diff --git a/coder-sdk/org.go b/coder-sdk/org.go index 10158c40..e3fe4583 100644 --- a/coder-sdk/org.go +++ b/coder-sdk/org.go @@ -3,20 +3,47 @@ package coder import ( "context" "net/http" + "time" ) -// Org describes an Organization in Coder -type Org struct { - ID string `json:"id"` - Name string `json:"name"` - Members []User `json:"members"` +// Organization describes an Organization in Coder +type Organization struct { + ID string `json:"id"` + Name string `json:"name"` + Members []OrganizationUser `json:"members"` } -// Orgs gets all Organizations -func (c Client) Orgs(ctx context.Context) ([]Org, error) { - var orgs []Org +// OrganizationUser user wraps the basic User type and adds data specific to the user's membership of an organization +type OrganizationUser struct { + User + OrganizationRoles []OrganizationRole `json:"organization_roles"` + RolesUpdatedAt time.Time `json:"roles_updated_at"` +} + +// OrganizationRole defines an organization OrganizationRole +type OrganizationRole string + +// The OrganizationRole enum values +const ( + RoleOrgMember OrganizationRole = "organization-member" + RoleOrgAdmin OrganizationRole = "organization-admin" + RoleOrgManager OrganizationRole = "organization-manager" +) + +// Organizations gets all Organizations +func (c Client) Organizations(ctx context.Context) ([]Organization, error) { + var orgs []Organization if err := c.requestBody(ctx, http.MethodGet, "/api/orgs", nil, &orgs); err != nil { return nil, err } return orgs, nil } + +// OrgMembers get all members of the given organization +func (c Client) OrgMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) { + var members []OrganizationUser + if err := c.requestBody(ctx, http.MethodGet, "/api/orgs/"+orgID+"/members", nil, &members); err != nil { + return nil, err + } + return members, nil +} diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go index dfce6598..2da0dd25 100644 --- a/internal/cmd/ceapi.go +++ b/internal/cmd/ceapi.go @@ -12,9 +12,9 @@ import ( // Helpers for working with the Coder Enterprise API. // lookupUserOrgs gets a list of orgs the user is apart of. -func lookupUserOrgs(user *coder.User, orgs []coder.Org) []coder.Org { +func lookupUserOrgs(user *coder.User, orgs []coder.Organization) []coder.Organization { // NOTE: We don't know in advance how many orgs the user is in so we can't pre-alloc. - var userOrgs []coder.Org + var userOrgs []coder.Organization for _, org := range orgs { for _, member := range org.Members { @@ -36,7 +36,7 @@ func getEnvs(ctx context.Context, client *coder.Client, email string) ([]coder.E return nil, xerrors.Errorf("get user: %w", err) } - orgs, err := client.Orgs(ctx) + orgs, err := client.Organizations(ctx) if err != nil { return nil, xerrors.Errorf("get orgs: %w", err) } diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 6f13dddc..b9e48b95 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -24,6 +24,7 @@ func Make() *cobra.Command { makeEnvsCommand(), makeSyncCmd(), makeURLCmd(), + makeResourceCmd(), completionCmd, genDocs(app), ) diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go new file mode 100644 index 00000000..ef984c12 --- /dev/null +++ b/internal/cmd/resourcemanager.go @@ -0,0 +1,106 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "cdr.dev/coder-cli/coder-sdk" + "github.com/spf13/cobra" +) + +func makeResourceCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "resources", + Short: "manager Coder resources with platform-level context (users, organizations, environments)", + } + cmd.AddCommand(resourceTop) + return cmd +} + +var resourceTop = &cobra.Command{ + Use: "top", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := newClient() + if err != nil { + return err + } + + envs, err := client.ListEnvironments(ctx) + if err != nil { + return err + } + + userEnvs := make(map[string][]coder.Environment) + for _, e := range envs { + userEnvs[e.UserID] = append(userEnvs[e.UserID], e) + } + + users, err := client.Users(ctx) + if err != nil { + return err + } + + orgs := make(map[string]coder.Organization) + orglist, err := client.Organizations(ctx) + if err != nil { + return err + } + for _, o := range orglist { + orgs[o.ID] = o + } + + tabwriter := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) + for _, u := range users { + _, _ = fmt.Fprintf(tabwriter, "%s\t(%s)\t%s", u.Name, u.Email, aggregateEnvResources(userEnvs[u.ID])) + if len(userEnvs[u.ID]) > 0 { + _, _ = fmt.Fprintf(tabwriter, "\f") + } + for _, env := range userEnvs[u.ID] { + _, _ = fmt.Fprintf(tabwriter, "\t") + _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, orgs)) + } + fmt.Fprint(tabwriter, "\n") + } + _ = tabwriter.Flush() + + return nil + }, +} + +func resourcesFromEnv(env coder.Environment) resources { + return resources{ + cpuAllocation: env.CPUCores, + cpuUtilization: env.LatestStat.CPUUsage, + memAllocation: env.MemoryGB, + memUtilization: env.LatestStat.MemoryUsage, + } +} + +func fmtEnvResources(env coder.Environment, orgs map[string]coder.Organization) string { + return fmt.Sprintf("%s\t%s\t[org: %s]", env.Name, resourcesFromEnv(env), orgs[env.OrganizationID].Name) +} + +func aggregateEnvResources(envs []coder.Environment) resources { + var aggregate resources + for _, e := range envs { + aggregate.cpuAllocation += e.CPUCores + aggregate.cpuUtilization += e.LatestStat.CPUUsage + aggregate.memAllocation += e.MemoryGB + aggregate.memUtilization += e.LatestStat.MemoryUsage + } + return aggregate +} + +type resources struct { + cpuAllocation float32 + cpuUtilization float32 + memAllocation float32 + memUtilization float32 +} + +func (a resources) String() string { + return fmt.Sprintf("[cpu: alloc=%.1fvCPU, util=%.1f]\t[mem: alloc=%.1fGB, util=%.1f]", a.cpuAllocation, a.cpuUtilization, a.memAllocation, a.memUtilization) +} From 1f60e711ce332b03626ce371bd2a6062107f2c94 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 16 Oct 2020 22:55:37 -0500 Subject: [PATCH 2/6] Add resource sorting by CPU allocation --- internal/cmd/cmd.go | 4 + internal/cmd/resourcemanager.go | 126 ++++++++++++++++++++------------ 2 files changed, 82 insertions(+), 48 deletions(-) diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index b9e48b95..ca6ecdad 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -7,6 +7,9 @@ import ( "github.com/spf13/cobra/doc" ) +// verbose is a global flag for specifying that a command should give verbose output +var verbose bool = false + // Make constructs the "coder" root command func Make() *cobra.Command { app := &cobra.Command{ @@ -28,6 +31,7 @@ func Make() *cobra.Command { completionCmd, genDocs(app), ) + app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") return app } diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index ef984c12..e9993576 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "sort" "text/tabwriter" "cdr.dev/coder-cli/coder-sdk" @@ -14,60 +15,81 @@ func makeResourceCmd() *cobra.Command { Use: "resources", Short: "manager Coder resources with platform-level context (users, organizations, environments)", } - cmd.AddCommand(resourceTop) + cmd.AddCommand(resourceTop()) return cmd } -var resourceTop = &cobra.Command{ - Use: "top", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - client, err := newClient() - if err != nil { - return err - } - - envs, err := client.ListEnvironments(ctx) - if err != nil { - return err - } - - userEnvs := make(map[string][]coder.Environment) - for _, e := range envs { - userEnvs[e.UserID] = append(userEnvs[e.UserID], e) - } - - users, err := client.Users(ctx) - if err != nil { - return err - } - - orgs := make(map[string]coder.Organization) - orglist, err := client.Organizations(ctx) - if err != nil { - return err - } - for _, o := range orglist { - orgs[o.ID] = o - } - - tabwriter := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) - for _, u := range users { - _, _ = fmt.Fprintf(tabwriter, "%s\t(%s)\t%s", u.Name, u.Email, aggregateEnvResources(userEnvs[u.ID])) - if len(userEnvs[u.ID]) > 0 { - _, _ = fmt.Fprintf(tabwriter, "\f") +func resourceTop() *cobra.Command { + cmd := &cobra.Command{ + Use: "top", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := newClient() + if err != nil { + return err + } + + envs, err := client.ListEnvironments(ctx) + if err != nil { + return err } - for _, env := range userEnvs[u.ID] { - _, _ = fmt.Fprintf(tabwriter, "\t") - _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, orgs)) + + userEnvs := make(map[string][]coder.Environment) + for _, e := range envs { + userEnvs[e.UserID] = append(userEnvs[e.UserID], e) } - fmt.Fprint(tabwriter, "\n") - } - _ = tabwriter.Flush() - return nil - }, + users, err := client.Users(ctx) + if err != nil { + return err + } + + orgs := make(map[string]coder.Organization) + orglist, err := client.Organizations(ctx) + if err != nil { + return err + } + for _, o := range orglist { + orgs[o.ID] = o + } + + tabwriter := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) + var userResources []aggregatedUser + for _, u := range users { + // truncate user names to ensure tabwriter doesn't push our entire table too far + u.Name = truncate(u.Name, 20, "...") + userResources = append(userResources, aggregatedUser{User: u, resources: aggregateEnvResources(userEnvs[u.ID])}) + } + sort.Slice(userResources, func(i, j int) bool { + return userResources[i].cpuAllocation > userResources[j].cpuAllocation + }) + + for _, u := range userResources { + _, _ = fmt.Fprintf(tabwriter, "%s\t(%s)\t%s", u.Name, u.Email, u.resources) + if verbose { + if len(userEnvs[u.ID]) > 0 { + _, _ = fmt.Fprintf(tabwriter, "\f") + } + for _, env := range userEnvs[u.ID] { + _, _ = fmt.Fprintf(tabwriter, "\t") + _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, orgs)) + } + } + fmt.Fprint(tabwriter, "\n") + } + _ = tabwriter.Flush() + + return nil + }, + } + + return cmd +} + +type aggregatedUser struct { + coder.User + resources } func resourcesFromEnv(env coder.Environment) resources { @@ -104,3 +126,11 @@ type resources struct { func (a resources) String() string { return fmt.Sprintf("[cpu: alloc=%.1fvCPU, util=%.1f]\t[mem: alloc=%.1fGB, util=%.1f]", a.cpuAllocation, a.cpuUtilization, a.memAllocation, a.memUtilization) } + +// truncate the given string and replace the removed chars with some replacement (ex: "...") +func truncate(str string, max int, replace string) string { + if len(str) <= max { + return str + } + return str[:max+1] + replace +} From b1088fbc4646b1d3a1b4f914afaea588e5eddb7a Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 16 Oct 2020 23:03:24 -0500 Subject: [PATCH 3/6] Generate docs with new verbose flag --- docs/coder.md | 4 +++- docs/coder_completion.md | 6 ++++++ docs/coder_config-ssh.md | 6 ++++++ docs/coder_envs.md | 6 ++++++ docs/coder_envs_ls.md | 1 + docs/coder_envs_stop.md | 1 + docs/coder_login.md | 6 ++++++ docs/coder_logout.md | 6 ++++++ docs/coder_resources.md | 24 ++++++++++++++++++++++++ docs/coder_resources_top.md | 27 +++++++++++++++++++++++++++ docs/coder_secrets.md | 6 ++++++ docs/coder_secrets_create.md | 1 + docs/coder_secrets_ls.md | 1 + docs/coder_secrets_rm.md | 1 + docs/coder_secrets_view.md | 1 + docs/coder_sh.md | 6 ++++++ docs/coder_sync.md | 6 ++++++ docs/coder_urls.md | 6 ++++++ docs/coder_urls_create.md | 6 ++++++ docs/coder_urls_ls.md | 6 ++++++ docs/coder_urls_rm.md | 6 ++++++ docs/coder_users.md | 6 ++++++ docs/coder_users_ls.md | 6 ++++++ 23 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 docs/coder_resources.md create mode 100644 docs/coder_resources_top.md diff --git a/docs/coder.md b/docs/coder.md index 378a9c66..2cce8ff9 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -9,7 +9,8 @@ coder provides a CLI for working with an existing Coder Enterprise installation ### Options ``` - -h, --help help for coder + -h, --help help for coder + -v, --verbose show verbose output ``` ### SEE ALSO @@ -19,6 +20,7 @@ coder provides a CLI for working with an existing Coder Enterprise installation * [coder envs](coder_envs.md) - Interact with Coder environments * [coder login](coder_login.md) - Authenticate this client for future operations * [coder logout](coder_logout.md) - Remove local authentication credentials if any exist +* [coder resources](coder_resources.md) - manager Coder resources with platform-level context (users, organizations, environments) * [coder secrets](coder_secrets.md) - Interact with Coder Secrets * [coder sh](coder_sh.md) - Open a shell and execute commands in a Coder environment * [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder environment diff --git a/docs/coder_completion.md b/docs/coder_completion.md index 39b862af..44f5519a 100644 --- a/docs/coder_completion.md +++ b/docs/coder_completion.md @@ -58,6 +58,12 @@ MacOS: -h, --help help for completion ``` +### 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 diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md index 41b697ef..b9f1e882 100644 --- a/docs/coder_config-ssh.md +++ b/docs/coder_config-ssh.md @@ -18,6 +18,12 @@ coder config-ssh [flags] --remove remove the auto-generated Coder Enterprise ssh config ``` +### 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 diff --git a/docs/coder_envs.md b/docs/coder_envs.md index 0cde6725..5730eb52 100644 --- a/docs/coder_envs.md +++ b/docs/coder_envs.md @@ -13,6 +13,12 @@ Perform operations on the Coder environments owned by the active user. --user string Specify the user whose resources to target (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 diff --git a/docs/coder_envs_ls.md b/docs/coder_envs_ls.md index d3535af8..49cd04dd 100644 --- a/docs/coder_envs_ls.md +++ b/docs/coder_envs_ls.md @@ -21,6 +21,7 @@ coder envs ls [flags] ``` --user string Specify the user whose resources to target (default "me") + -v, --verbose show verbose output ``` ### SEE ALSO diff --git a/docs/coder_envs_stop.md b/docs/coder_envs_stop.md index d5b522a5..3ed3b2ee 100644 --- a/docs/coder_envs_stop.md +++ b/docs/coder_envs_stop.md @@ -20,6 +20,7 @@ coder envs stop [environment_name] [flags] ``` --user string Specify the user whose resources to target (default "me") + -v, --verbose show verbose output ``` ### SEE ALSO diff --git a/docs/coder_login.md b/docs/coder_login.md index e943f275..bd3d9fb6 100644 --- a/docs/coder_login.md +++ b/docs/coder_login.md @@ -16,6 +16,12 @@ coder login [Coder Enterprise URL eg. https://my.coder.domain/] [flags] -h, --help help for login ``` +### 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 diff --git a/docs/coder_logout.md b/docs/coder_logout.md index 22bce303..a41aa009 100644 --- a/docs/coder_logout.md +++ b/docs/coder_logout.md @@ -16,6 +16,12 @@ coder logout [flags] -h, --help help for logout ``` +### 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 diff --git a/docs/coder_resources.md b/docs/coder_resources.md new file mode 100644 index 00000000..4b7ff0fc --- /dev/null +++ b/docs/coder_resources.md @@ -0,0 +1,24 @@ +## coder resources + +manager Coder resources with platform-level context (users, organizations, environments) + +### Synopsis + +manager Coder resources with platform-level context (users, organizations, environments) + +### Options + +``` + -h, --help help for resources +``` + +### 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 resources top](coder_resources_top.md) - diff --git a/docs/coder_resources_top.md b/docs/coder_resources_top.md new file mode 100644 index 00000000..e82913fc --- /dev/null +++ b/docs/coder_resources_top.md @@ -0,0 +1,27 @@ +## coder resources top + + + +### Synopsis + + + +``` +coder resources top [flags] +``` + +### Options + +``` + -h, --help help for top +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder resources](coder_resources.md) - manager Coder resources with platform-level context (users, organizations, environments) diff --git a/docs/coder_secrets.md b/docs/coder_secrets.md index ebdd1af2..b8178fd5 100644 --- a/docs/coder_secrets.md +++ b/docs/coder_secrets.md @@ -13,6 +13,12 @@ Interact with secrets objects owned by the active user. --user string Specify the user whose resources to target (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 diff --git a/docs/coder_secrets_create.md b/docs/coder_secrets_create.md index c10a771e..3dfa1140 100644 --- a/docs/coder_secrets_create.md +++ b/docs/coder_secrets_create.md @@ -32,6 +32,7 @@ coder secrets create aws-credentials --from-file ./credentials.json ``` --user string Specify the user whose resources to target (default "me") + -v, --verbose show verbose output ``` ### SEE ALSO diff --git a/docs/coder_secrets_ls.md b/docs/coder_secrets_ls.md index 40408e8e..e3e0ebc0 100644 --- a/docs/coder_secrets_ls.md +++ b/docs/coder_secrets_ls.md @@ -20,6 +20,7 @@ coder secrets ls [flags] ``` --user string Specify the user whose resources to target (default "me") + -v, --verbose show verbose output ``` ### SEE ALSO diff --git a/docs/coder_secrets_rm.md b/docs/coder_secrets_rm.md index d58dc6f0..c8877d82 100644 --- a/docs/coder_secrets_rm.md +++ b/docs/coder_secrets_rm.md @@ -26,6 +26,7 @@ coder secrets rm mysql-password mysql-user ``` --user string Specify the user whose resources to target (default "me") + -v, --verbose show verbose output ``` ### SEE ALSO diff --git a/docs/coder_secrets_view.md b/docs/coder_secrets_view.md index e5a9770a..60fcaa4d 100644 --- a/docs/coder_secrets_view.md +++ b/docs/coder_secrets_view.md @@ -26,6 +26,7 @@ coder secrets view mysql-password ``` --user string Specify the user whose resources to target (default "me") + -v, --verbose show verbose output ``` ### SEE ALSO diff --git a/docs/coder_sh.md b/docs/coder_sh.md index 6c88f203..8bc0d959 100644 --- a/docs/coder_sh.md +++ b/docs/coder_sh.md @@ -22,6 +22,12 @@ coder sh backend-env -h, --help help for sh ``` +### 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 diff --git a/docs/coder_sync.md b/docs/coder_sync.md index 03ca7a37..91098662 100644 --- a/docs/coder_sync.md +++ b/docs/coder_sync.md @@ -17,6 +17,12 @@ coder sync [local directory] [:] [flags] --init do initial transfer and exit ``` +### 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 diff --git a/docs/coder_urls.md b/docs/coder_urls.md index df4c3c70..75f361f4 100644 --- a/docs/coder_urls.md +++ b/docs/coder_urls.md @@ -12,6 +12,12 @@ Interact with environment DevURLs -h, --help help for urls ``` +### 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 diff --git a/docs/coder_urls_create.md b/docs/coder_urls_create.md index 7afc8d8b..5d613f56 100644 --- a/docs/coder_urls_create.md +++ b/docs/coder_urls_create.md @@ -18,6 +18,12 @@ coder urls create [env_name] [port] [--access ] [--name ] [flags] --name string DevURL name ``` +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + ### SEE ALSO * [coder urls](coder_urls.md) - Interact with environment DevURLs diff --git a/docs/coder_urls_ls.md b/docs/coder_urls_ls.md index 1d01c2e5..876210e9 100644 --- a/docs/coder_urls_ls.md +++ b/docs/coder_urls_ls.md @@ -17,6 +17,12 @@ coder urls ls [environment_name] [flags] -o, --output string human|json (default "human") ``` +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + ### SEE ALSO * [coder urls](coder_urls.md) - Interact with environment DevURLs diff --git a/docs/coder_urls_rm.md b/docs/coder_urls_rm.md index 2b69e2bb..e19ccf30 100644 --- a/docs/coder_urls_rm.md +++ b/docs/coder_urls_rm.md @@ -16,6 +16,12 @@ coder urls rm [environment_name] [port] [flags] -h, --help help for rm ``` +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + ### SEE ALSO * [coder urls](coder_urls.md) - Interact with environment DevURLs diff --git a/docs/coder_users.md b/docs/coder_users.md index 6482d76e..59a8c779 100644 --- a/docs/coder_users.md +++ b/docs/coder_users.md @@ -12,6 +12,12 @@ Interact with Coder user accounts -h, --help help for users ``` +### 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 diff --git a/docs/coder_users_ls.md b/docs/coder_users_ls.md index 6cf7ccd1..99b00db2 100644 --- a/docs/coder_users_ls.md +++ b/docs/coder_users_ls.md @@ -24,6 +24,12 @@ coder users ls -o json | jq .[] | jq -r .email -o, --output string human | json (default "human") ``` +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + ### SEE ALSO * [coder users](coder_users.md) - Interact with Coder user accounts From af2ac67ec59c81b1eb880fff57a476946c65147d Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 16 Oct 2020 23:12:31 -0500 Subject: [PATCH 4/6] Hide resources command from docs --- ci/steps/gendocs.sh | 5 ++- coder-sdk/env.go | 6 +-- docs/coder.md | 1 - docs/coder_resources.md | 24 ---------- docs/coder_resources_top.md | 27 ------------ internal/cmd/resourcemanager.go | 78 ++++++++++++++++++--------------- 6 files changed, 49 insertions(+), 92 deletions(-) delete mode 100644 docs/coder_resources.md delete mode 100644 docs/coder_resources_top.md diff --git a/ci/steps/gendocs.sh b/ci/steps/gendocs.sh index 64a3776a..9e31b626 100755 --- a/ci/steps/gendocs.sh +++ b/ci/steps/gendocs.sh @@ -7,15 +7,16 @@ echo "Generating docs..." cd "$(dirname "$0")" cd ../../ +rm -rf ./docs +mkdir ./docs go run ./cmd/coder gen-docs ./docs # remove cobra footer from each file for filename in ./docs/*.md; do trimmed=$(head -n -1 "$filename") - echo "$trimmed" > $filename + echo "$trimmed" >$filename done - if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then echo "Documentation needs generation:" git -c color.ui=always status | grep --color=no '\e\[31m' diff --git a/coder-sdk/env.go b/coder-sdk/env.go index e506f0d7..8fd42838 100644 --- a/coder-sdk/env.go +++ b/coder-sdk/env.go @@ -93,9 +93,9 @@ func (c Client) CreateEnvironment(ctx context.Context, orgID string, req CreateE return &env, nil } -// ListEnvironments lists environments returned by the given filter. -// TODO: add the filter options -func (c Client) ListEnvironments(ctx context.Context) ([]Environment, error) { +// Environments lists environments returned by the given filter. +// TODO: add the filter options, explore performance issues +func (c Client) Environments(ctx context.Context) ([]Environment, error) { var envs []Environment if err := c.requestBody(ctx, http.MethodGet, "/api/environments", nil, &envs); err != nil { return nil, err diff --git a/docs/coder.md b/docs/coder.md index 2cce8ff9..844267d5 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -20,7 +20,6 @@ coder provides a CLI for working with an existing Coder Enterprise installation * [coder envs](coder_envs.md) - Interact with Coder environments * [coder login](coder_login.md) - Authenticate this client for future operations * [coder logout](coder_logout.md) - Remove local authentication credentials if any exist -* [coder resources](coder_resources.md) - manager Coder resources with platform-level context (users, organizations, environments) * [coder secrets](coder_secrets.md) - Interact with Coder Secrets * [coder sh](coder_sh.md) - Open a shell and execute commands in a Coder environment * [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder environment diff --git a/docs/coder_resources.md b/docs/coder_resources.md deleted file mode 100644 index 4b7ff0fc..00000000 --- a/docs/coder_resources.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder resources - -manager Coder resources with platform-level context (users, organizations, environments) - -### Synopsis - -manager Coder resources with platform-level context (users, organizations, environments) - -### Options - -``` - -h, --help help for resources -``` - -### 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 resources top](coder_resources_top.md) - diff --git a/docs/coder_resources_top.md b/docs/coder_resources_top.md deleted file mode 100644 index e82913fc..00000000 --- a/docs/coder_resources_top.md +++ /dev/null @@ -1,27 +0,0 @@ -## coder resources top - - - -### Synopsis - - - -``` -coder resources top [flags] -``` - -### Options - -``` - -h, --help help for top -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder resources](coder_resources.md) - manager Coder resources with platform-level context (users, organizations, environments) diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index e9993576..55df2f57 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -2,18 +2,21 @@ package cmd import ( "fmt" + "io" "os" "sort" "text/tabwriter" "cdr.dev/coder-cli/coder-sdk" "github.com/spf13/cobra" + "golang.org/x/xerrors" ) func makeResourceCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "resources", - Short: "manager Coder resources with platform-level context (users, organizations, environments)", + Use: "resources", + Short: "manager Coder resources with platform-level context (users, organizations, environments)", + Hidden: true, } cmd.AddCommand(resourceTop()) return cmd @@ -24,15 +27,16 @@ func resourceTop() *cobra.Command { Use: "top", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient() if err != nil { return err } - envs, err := client.ListEnvironments(ctx) + // NOTE: it's not worth parrallelizing these calls yet given that this specific endpoint + // takes about 20x times longer than the other two + envs, err := client.Environments(ctx) if err != nil { - return err + return xerrors.Errorf("get environments %w", err) } userEnvs := make(map[string][]coder.Environment) @@ -42,44 +46,19 @@ func resourceTop() *cobra.Command { users, err := client.Users(ctx) if err != nil { - return err + return xerrors.Errorf("get users: %w", err) } - orgs := make(map[string]coder.Organization) + orgIDMap := make(map[string]coder.Organization) orglist, err := client.Organizations(ctx) if err != nil { - return err + return xerrors.Errorf("get organizations: %w", err) } for _, o := range orglist { - orgs[o.ID] = o - } - - tabwriter := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) - var userResources []aggregatedUser - for _, u := range users { - // truncate user names to ensure tabwriter doesn't push our entire table too far - u.Name = truncate(u.Name, 20, "...") - userResources = append(userResources, aggregatedUser{User: u, resources: aggregateEnvResources(userEnvs[u.ID])}) - } - sort.Slice(userResources, func(i, j int) bool { - return userResources[i].cpuAllocation > userResources[j].cpuAllocation - }) - - for _, u := range userResources { - _, _ = fmt.Fprintf(tabwriter, "%s\t(%s)\t%s", u.Name, u.Email, u.resources) - if verbose { - if len(userEnvs[u.ID]) > 0 { - _, _ = fmt.Fprintf(tabwriter, "\f") - } - for _, env := range userEnvs[u.ID] { - _, _ = fmt.Fprintf(tabwriter, "\t") - _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, orgs)) - } - } - fmt.Fprint(tabwriter, "\n") + orgIDMap[o.ID] = o } - _ = tabwriter.Flush() + printResourceTop(os.Stdout, users, orgIDMap, userEnvs) return nil }, } @@ -87,6 +66,35 @@ func resourceTop() *cobra.Command { return cmd } +func printResourceTop(writer io.Writer, users []coder.User, orgIDMap map[string]coder.Organization, userEnvs map[string][]coder.Environment) { + tabwriter := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) + defer func() { _ = tabwriter.Flush() }() + + var userResources []aggregatedUser + for _, u := range users { + // truncate user names to ensure tabwriter doesn't push our entire table too far + u.Name = truncate(u.Name, 20, "...") + userResources = append(userResources, aggregatedUser{User: u, resources: aggregateEnvResources(userEnvs[u.ID])}) + } + sort.Slice(userResources, func(i, j int) bool { + return userResources[i].cpuAllocation > userResources[j].cpuAllocation + }) + + for _, u := range userResources { + _, _ = fmt.Fprintf(tabwriter, "%s\t(%s)\t%s", u.Name, u.Email, u.resources) + if verbose { + if len(userEnvs[u.ID]) > 0 { + _, _ = fmt.Fprintf(tabwriter, "\f") + } + for _, env := range userEnvs[u.ID] { + _, _ = fmt.Fprintf(tabwriter, "\t") + _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, orgIDMap)) + } + } + _, _ = fmt.Fprint(tabwriter, "\n") + } +} + type aggregatedUser struct { coder.User resources From eff753a4652e263689bd08e31df3fddd91621f76 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Sat, 17 Oct 2020 01:03:05 -0500 Subject: [PATCH 5/6] Abstract env resource grouping and label properly --- internal/cmd/resourcemanager.go | 134 ++++++++++++++++++++++++++------ 1 file changed, 111 insertions(+), 23 deletions(-) diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index 55df2f57..2395d007 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -23,6 +23,7 @@ func makeResourceCmd() *cobra.Command { } func resourceTop() *cobra.Command { + var group string cmd := &cobra.Command{ Use: "top", RunE: func(cmd *cobra.Command, args []string) error { @@ -34,14 +35,16 @@ func resourceTop() *cobra.Command { // NOTE: it's not worth parrallelizing these calls yet given that this specific endpoint // takes about 20x times longer than the other two - envs, err := client.Environments(ctx) + allEnvs, err := client.Environments(ctx) if err != nil { return xerrors.Errorf("get environments %w", err) } - - userEnvs := make(map[string][]coder.Environment) - for _, e := range envs { - userEnvs[e.UserID] = append(userEnvs[e.UserID], e) + // only include environments whose last status was "ON" + envs := make([]coder.Environment, 0) + for _, e := range allEnvs { + if e.LatestStat.ContainerStatus == coder.EnvironmentOn { + envs = append(envs, e) + } } users, err := client.Users(ctx) @@ -49,54 +52,119 @@ func resourceTop() *cobra.Command { return xerrors.Errorf("get users: %w", err) } - orgIDMap := make(map[string]coder.Organization) - orglist, err := client.Organizations(ctx) + orgs, err := client.Organizations(ctx) if err != nil { return xerrors.Errorf("get organizations: %w", err) } - for _, o := range orglist { - orgIDMap[o.ID] = o + + var groups []groupable + var labeler envLabeler + switch group { + case "user": + userEnvs := make(map[string][]coder.Environment, len(users)) + for _, e := range envs { + userEnvs[e.UserID] = append(userEnvs[e.UserID], e) + } + for _, u := range users { + groups = append(groups, userGrouping{user: u, envs: userEnvs[u.ID]}) + } + orgIDMap := make(map[string]coder.Organization) + for _, o := range orgs { + orgIDMap[o.ID] = o + } + labeler = orgLabeler{orgIDMap} + case "org": + orgEnvs := make(map[string][]coder.Environment, len(orgs)) + for _, e := range envs { + orgEnvs[e.OrganizationID] = append(orgEnvs[e.OrganizationID], e) + } + for _, o := range orgs { + groups = append(groups, orgGrouping{org: o, envs: orgEnvs[o.ID]}) + } + userIDMap := make(map[string]coder.User) + for _, u := range users { + userIDMap[u.ID] = u + } + labeler = userLabeler{userIDMap} + default: + return xerrors.Errorf("unknown --group %q", group) } - printResourceTop(os.Stdout, users, orgIDMap, userEnvs) + printResourceTop(os.Stdout, groups, labeler) return nil }, } + cmd.Flags().StringVar(&group, "group", "user", "the grouping parameter (user|org)") return cmd } -func printResourceTop(writer io.Writer, users []coder.User, orgIDMap map[string]coder.Organization, userEnvs map[string][]coder.Environment) { +// groupable specifies a structure capable of being an aggregation group of environments (user, org, all) +type groupable interface { + header() string + environments() []coder.Environment +} + +type userGrouping struct { + user coder.User + envs []coder.Environment +} + +func (u userGrouping) environments() []coder.Environment { + return u.envs +} + +func (u userGrouping) header() string { + return fmt.Sprintf("%s\t(%s)", truncate(u.user.Name, 20, "..."), u.user.Email) +} + +type orgGrouping struct { + org coder.Organization + envs []coder.Environment +} + +func (o orgGrouping) environments() []coder.Environment { + return o.envs +} + +func (o orgGrouping) header() string { + plural := "s" + if len(o.org.Members) < 2 { + plural = "" + } + return fmt.Sprintf("%s\t(%v member%s)", truncate(o.org.Name, 20, "..."), len(o.org.Members), plural) +} + +func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler) { tabwriter := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) defer func() { _ = tabwriter.Flush() }() - var userResources []aggregatedUser - for _, u := range users { + var userResources []aggregatedResources + for _, group := range groups { // truncate user names to ensure tabwriter doesn't push our entire table too far - u.Name = truncate(u.Name, 20, "...") - userResources = append(userResources, aggregatedUser{User: u, resources: aggregateEnvResources(userEnvs[u.ID])}) + userResources = append(userResources, aggregatedResources{groupable: group, resources: aggregateEnvResources(group.environments())}) } sort.Slice(userResources, func(i, j int) bool { return userResources[i].cpuAllocation > userResources[j].cpuAllocation }) for _, u := range userResources { - _, _ = fmt.Fprintf(tabwriter, "%s\t(%s)\t%s", u.Name, u.Email, u.resources) + _, _ = fmt.Fprintf(tabwriter, "%s\t%s", u.header(), u.resources) if verbose { - if len(userEnvs[u.ID]) > 0 { + if len(u.environments()) > 0 { _, _ = fmt.Fprintf(tabwriter, "\f") } - for _, env := range userEnvs[u.ID] { + for _, env := range u.environments() { _, _ = fmt.Fprintf(tabwriter, "\t") - _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, orgIDMap)) + _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, labeler)) } } _, _ = fmt.Fprint(tabwriter, "\n") } } -type aggregatedUser struct { - coder.User +type aggregatedResources struct { + groupable resources } @@ -109,8 +177,28 @@ func resourcesFromEnv(env coder.Environment) resources { } } -func fmtEnvResources(env coder.Environment, orgs map[string]coder.Organization) string { - return fmt.Sprintf("%s\t%s\t[org: %s]", env.Name, resourcesFromEnv(env), orgs[env.OrganizationID].Name) +func fmtEnvResources(env coder.Environment, labeler envLabeler) string { + return fmt.Sprintf("%s\t%s\t%s", env.Name, resourcesFromEnv(env), labeler.label(env)) +} + +type envLabeler interface { + label(coder.Environment) string +} + +type orgLabeler struct { + orgMap map[string]coder.Organization +} + +func (o orgLabeler) label(e coder.Environment) string { + return fmt.Sprintf("[org: %s]", o.orgMap[e.OrganizationID].Name) +} + +type userLabeler struct { + userMap map[string]coder.User +} + +func (u userLabeler) label(e coder.Environment) string { + return fmt.Sprintf("[user: %s]", u.userMap[e.UserID].Email) } func aggregateEnvResources(envs []coder.Environment) resources { From 1a1b6e30aca49859466cd1d025f74981770ccd5e Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Sun, 18 Oct 2020 16:19:58 -0500 Subject: [PATCH 6/6] Remove resource top utilization metrics --- internal/cmd/resourcemanager.go | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index 2395d007..75676105 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -15,7 +15,7 @@ import ( func makeResourceCmd() *cobra.Command { cmd := &cobra.Command{ Use: "resources", - Short: "manager Coder resources with platform-level context (users, organizations, environments)", + Short: "manage Coder resources with platform-level context (users, organizations, environments)", Hidden: true, } cmd.AddCommand(resourceTop()) @@ -141,8 +141,10 @@ func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler) var userResources []aggregatedResources for _, group := range groups { - // truncate user names to ensure tabwriter doesn't push our entire table too far - userResources = append(userResources, aggregatedResources{groupable: group, resources: aggregateEnvResources(group.environments())}) + userResources = append( + userResources, + aggregatedResources{groupable: group, resources: aggregateEnvResources(group.environments())}, + ) } sort.Slice(userResources, func(i, j int) bool { return userResources[i].cpuAllocation > userResources[j].cpuAllocation @@ -220,7 +222,30 @@ type resources struct { } func (a resources) String() string { - return fmt.Sprintf("[cpu: alloc=%.1fvCPU, util=%.1f]\t[mem: alloc=%.1fGB, util=%.1f]", a.cpuAllocation, a.cpuUtilization, a.memAllocation, a.memUtilization) + return fmt.Sprintf( + "[cpu: alloc=%.1fvCPU]\t[mem: alloc=%.1fGB]", + a.cpuAllocation, a.memAllocation, + ) + + // TODO@cmoog: consider adding the utilization info once a historical average is considered or implemented + // return fmt.Sprintf( + // "[cpu: alloc=%.1fvCPU, util=%s]\t[mem: alloc=%.1fGB, util=%s]", + // a.cpuAllocation, a.cpuUtilPercentage(), a.memAllocation, a.memUtilPercentage(), + // ) +} + +func (a resources) cpuUtilPercentage() string { + if a.cpuAllocation == 0 { + return "N/A" + } + return fmt.Sprintf("%.1f%%", a.cpuUtilization/a.cpuAllocation*100) +} + +func (a resources) memUtilPercentage() string { + if a.memAllocation == 0 { + return "N/A" + } + return fmt.Sprintf("%.1f%%", a.memUtilization/a.memAllocation*100) } // truncate the given string and replace the removed chars with some replacement (ex: "...")