diff --git a/coder b/coder new file mode 100755 index 00000000..b2f1e39f Binary files /dev/null and b/coder differ diff --git a/coder-sdk/env.go b/coder-sdk/env.go index 050db088..7792f4b1 100644 --- a/coder-sdk/env.go +++ b/coder-sdk/env.go @@ -338,3 +338,12 @@ func (c *DefaultClient) EnvironmentByID(ctx context.Context, id string) (*Enviro } return &env, nil } + +// EnvironmentsByWorkspaceProvider returns all environments that belong to a particular workspace provider. +func (c *DefaultClient) EnvironmentsByWorkspaceProvider(ctx context.Context, wpID string) ([]Environment, error) { + var envs []Environment + if err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+wpID+"/environments", nil, &envs); err != nil { + return nil, err + } + return envs, nil +} diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go index 82971340..235666a7 100644 --- a/coder-sdk/interface.go +++ b/coder-sdk/interface.go @@ -130,6 +130,9 @@ type Client interface { // EnvironmentByID get the details of an environment by its id. EnvironmentByID(ctx context.Context, id string) (*Environment, error) + // EnvironmentsByWorkspaceProvider returns environments that belong to a particular workspace provider. + EnvironmentsByWorkspaceProvider(ctx context.Context, wpID string) ([]Environment, error) + // ImportImage creates a new image and optionally a new registry. ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) diff --git a/docs/coder_envs_ls.md b/docs/coder_envs_ls.md index daa12f4a..e43aec24 100644 --- a/docs/coder_envs_ls.md +++ b/docs/coder_envs_ls.md @@ -13,9 +13,10 @@ coder envs ls [flags] ### Options ``` - -h, --help help for ls - -o, --output string human | json (default "human") - --user string Specify the user whose resources to target (default "me") + -h, --help help for ls + -o, --output string human | json (default "human") + -p, --provider string Filter environments by a particular workspace provider name. + --user string Specify the user whose resources to target (default "me") ``` ### Options inherited from parent commands diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go index 0b2e719c..ddba0a62 100644 --- a/internal/cmd/ceapi.go +++ b/internal/cmd/ceapi.go @@ -8,6 +8,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/internal/coderutil" "cdr.dev/coder-cli/pkg/clog" ) @@ -202,3 +203,36 @@ func getUserOrgs(ctx context.Context, client coder.Client, email string) ([]code } return lookupUserOrgs(u, orgs), nil } + +func getEnvsByProvider(ctx context.Context, client coder.Client, wpName, userEmail string) ([]coder.Environment, error) { + wp, err := coderutil.ProviderByName(ctx, client, wpName) + if err != nil { + return nil, err + } + + envs, err := client.EnvironmentsByWorkspaceProvider(ctx, wp.ID) + if err != nil { + return nil, err + } + + envs, err = filterEnvsByUser(ctx, client, userEmail, envs) + if err != nil { + return nil, err + } + return envs, nil +} + +func filterEnvsByUser(ctx context.Context, client coder.Client, userEmail string, envs []coder.Environment) ([]coder.Environment, error) { + user, err := client.UserByEmail(ctx, userEmail) + if err != nil { + return nil, xerrors.Errorf("get user: %w", err) + } + + var filteredEnvs []coder.Environment + for _, env := range envs { + if env.UserID == user.ID { + filteredEnvs = append(filteredEnvs, env) + } + } + return filteredEnvs, nil +} diff --git a/internal/cmd/cli_test.go b/internal/cmd/cli_test.go index d1c639dc..a15e2a70 100644 --- a/internal/cmd/cli_test.go +++ b/internal/cmd/cli_test.go @@ -114,6 +114,7 @@ func (r result) clogError(t *testing.T) clog.CLIError { return cliErr } +//nolint func execute(t *testing.T, in io.Reader, args ...string) result { cmd := Make() diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index 67739398..f021c507 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -51,6 +51,7 @@ func lsEnvsCommand() *cobra.Command { var ( outputFmt string user string + provider string ) cmd := &cobra.Command{ @@ -67,6 +68,12 @@ func lsEnvsCommand() *cobra.Command { if err != nil { return err } + if provider != "" { + envs, err = getEnvsByProvider(ctx, client, provider, user) + if err != nil { + return err + } + } if len(envs) < 1 { clog.LogInfo("no environments found") envs = []coder.Environment{} // ensures that json output still marshals @@ -94,6 +101,7 @@ func lsEnvsCommand() *cobra.Command { cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") cmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human | json") + cmd.Flags().StringVarP(&provider, "provider", "p", "", "Filter environments by a particular workspace provider name.") return cmd } diff --git a/internal/cmd/envs_test.go b/internal/cmd/envs_test.go index c7cc6451..46df3baf 100644 --- a/internal/cmd/envs_test.go +++ b/internal/cmd/envs_test.go @@ -16,3 +16,30 @@ func Test_envs_ls(t *testing.T) { var envs []coder.Environment res.stdoutUnmarshals(t, &envs) } + +//nolint +func Test_envs_ls_by_provider(t *testing.T) { + for _, test := range []struct { + name string + command []string + assert func(r result) + }{ + { + name: "simple list", + command: []string{"envs", "ls", "--provider", "built-in"}, + assert: func(r result) { r.success(t) }, + }, + { + name: "list as json", + command: []string{"envs", "ls", "--provider", "built-in", "--output", "json"}, + assert: func(r result) { + var envs []coder.Environment + r.stdoutUnmarshals(t, &envs) + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + test.assert(execute(t, nil, test.command...)) + }) + } +} diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index 01f4875c..30e4417e 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -28,6 +28,7 @@ type resourceTopOptions struct { user string org string sortBy string + provider string showEmptyGroups bool } @@ -41,11 +42,13 @@ func resourceTop() *cobra.Command { Example: `coder resources top --group org coder resources top --group org --verbose --org DevOps coder resources top --group user --verbose --user name@example.com +coder resources top --group provider --verbose --provider myprovider coder resources top --sort-by memory --show-empty`, } - cmd.Flags().StringVar(&options.group, "group", "user", "the grouping parameter (user|org)") + cmd.Flags().StringVar(&options.group, "group", "user", "the grouping parameter (user|org|provider)") cmd.Flags().StringVar(&options.user, "user", "", "filter by a user email") cmd.Flags().StringVar(&options.org, "org", "", "filter by the name of an organization") + cmd.Flags().StringVar(&options.provider, "provider", "", "filter by the name of a workspace provider") cmd.Flags().StringVar(&options.sortBy, "sort-by", "cpu", "field to sort aggregate groups and environments by (cpu|memory)") cmd.Flags().BoolVar(&options.showEmptyGroups, "show-empty", false, "show groups with zero active environments") @@ -84,6 +87,11 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ return xerrors.Errorf("get organizations: %w", err) } + providers, err := client.WorkspaceProviders(ctx) + if err != nil { + return xerrors.Errorf("get workspace providers: %w", err) + } + var groups []groupable var labeler envLabeler switch options.group { @@ -91,6 +99,8 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ groups, labeler = aggregateByUser(users, orgs, envs, *options) case "org": groups, labeler = aggregateByOrg(users, orgs, envs, *options) + case "provider": + groups, labeler = aggregateByProvider(providers.Kubernetes, orgs, envs, *options) default: return xerrors.Errorf("unknown --group %q", options.group) } @@ -143,6 +153,28 @@ func aggregateByOrg(users []coder.User, orgs []coder.Organization, envs []coder. return groups, userLabeler{userIDMap} } +func aggregateByProvider(providers []coder.KubernetesProvider, orgs []coder.Organization, envs []coder.Environment, options resourceTopOptions) ([]groupable, envLabeler) { + var groups []groupable + providerIDMap := make(map[string]coder.KubernetesProvider) + for _, p := range providers { + providerIDMap[p.ID] = p + } + providerEnvs := make(map[string][]coder.Environment, len(orgs)) + for _, e := range envs { + if options.provider != "" && providerIDMap[e.ResourcePoolID].Name != options.provider { + continue + } + providerEnvs[e.ResourcePoolID] = append(providerEnvs[e.ResourcePoolID], e) + } + for _, p := range providers { + if options.provider != "" && p.Name != options.provider { + continue + } + groups = append(groups, providerGrouping{provider: p, envs: providerEnvs[p.ID]}) + } + return groups, providerLabeler{providerIDMap} +} + // groupable specifies a structure capable of being an aggregation group of environments (user, org, all). type groupable interface { header() string @@ -179,6 +211,19 @@ func (o orgGrouping) header() string { return fmt.Sprintf("%s\t(%v member%s)", truncate(o.org.Name, 20, "..."), len(o.org.Members), plural) } +type providerGrouping struct { + provider coder.KubernetesProvider + envs []coder.Environment +} + +func (p providerGrouping) environments() []coder.Environment { + return p.envs +} + +func (p providerGrouping) header() string { + return fmt.Sprintf("%s\t", truncate(p.provider.Name, 20, "...")) +} + func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler, showEmptyGroups bool, sortBy string) error { tabwriter := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) defer func() { _ = tabwriter.Flush() }() @@ -287,6 +332,14 @@ func (u userLabeler) label(e coder.Environment) string { return fmt.Sprintf("[user: %s]", u.userMap[e.UserID].Email) } +type providerLabeler struct { + providerMap map[string]coder.KubernetesProvider +} + +func (p providerLabeler) label(e coder.Environment) string { + return fmt.Sprintf("[provider: %s]", p.providerMap[e.ResourcePoolID].Name) +} + func aggregateEnvResources(envs []coder.Environment) resources { var aggregate resources for _, e := range envs {