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: "...") pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy