Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

feature: support filtering by workspace provider when listing environ… #282

Merged
merged 6 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added coder
Binary file not shown.
9 changes: 9 additions & 0 deletions coder-sdk/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions coder-sdk/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 4 additions & 3 deletions docs/coder_envs_ls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions internal/cmd/ceapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions internal/cmd/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
8 changes: 8 additions & 0 deletions internal/cmd/envs.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func lsEnvsCommand() *cobra.Command {
var (
outputFmt string
user string
provider string
)

cmd := &cobra.Command{
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
27 changes: 27 additions & 0 deletions internal/cmd/envs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...))
})
}
}
55 changes: 54 additions & 1 deletion internal/cmd/resourcemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type resourceTopOptions struct {
user string
org string
sortBy string
provider string
showEmptyGroups bool
}

Expand All @@ -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")

Expand Down Expand Up @@ -84,13 +87,20 @@ 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 {
case "user":
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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() }()
Expand Down Expand Up @@ -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 {
Expand Down