diff --git a/internal/cmd/envs_test.go b/internal/cmd/envs_test.go index bb5750e6..e80a93a5 100644 --- a/internal/cmd/envs_test.go +++ b/internal/cmd/envs_test.go @@ -134,6 +134,7 @@ func assertEnv(t *testing.T, name string, envs []coder.Environment) *coder.Envir var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) +//nolint:unparam func randString(length int) string { const charset = "abcdefghijklmnopqrstuvwxyz" b := make([]byte, length) diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index f33108ea..5611594f 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -99,9 +99,6 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ if err != nil { return xerrors.Errorf("get workspace providers: %w", err) } - - var groups []groupable - var labeler envLabeler data := entities{ providers: providers.Kubernetes, users: users, @@ -109,19 +106,27 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ envs: envs, images: images, } - switch options.group { - case "user": - groups, labeler = aggregateByUser(data, *options) - case "org": - groups, labeler = aggregateByOrg(data, *options) - case "provider": - groups, labeler = aggregateByProvider(data, *options) - default: - return xerrors.Errorf("unknown --group %q", options.group) - } + return presentEntites(cmd.OutOrStdout(), data, *options) + } +} - return printResourceTop(cmd.OutOrStdout(), groups, labeler, options.showEmptyGroups, options.sortBy) +func presentEntites(w io.Writer, data entities, options resourceTopOptions) error { + var ( + groups []groupable + labeler envLabeler + ) + switch options.group { + case "user": + groups, labeler = aggregateByUser(data, options) + case "org": + groups, labeler = aggregateByOrg(data, options) + case "provider": + groups, labeler = aggregateByProvider(data, options) + default: + return xerrors.Errorf("unknown --group %q", options.group) } + + return printResourceTop(w, groups, labeler, options.showEmptyGroups, options.sortBy) } type entities struct { @@ -241,7 +246,7 @@ func (o orgGrouping) environments() []coder.Environment { func (o orgGrouping) header() string { plural := "s" - if len(o.org.Members) < 2 { + if len(o.org.Members) == 1 { plural = "" } return fmt.Sprintf("%s\t(%v member%s)", truncate(o.org.Name, 20, "..."), len(o.org.Members), plural) diff --git a/internal/cmd/resourcemanager_test.go b/internal/cmd/resourcemanager_test.go new file mode 100644 index 00000000..50c9156c --- /dev/null +++ b/internal/cmd/resourcemanager_test.go @@ -0,0 +1,184 @@ +package cmd + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "testing" + + "cdr.dev/slog/sloggers/slogtest/assert" + + "cdr.dev/coder-cli/coder-sdk" +) + +var write = flag.Bool("write", false, "write to the golden files") + +func Test_resourceManager(t *testing.T) { + // TODO: cleanup + verbose = true + + const goldenFile = "resourcemanager_test.golden" + var buff bytes.Buffer + data := mockResourceTopEntities() + tests := []struct { + header string + data entities + options resourceTopOptions + }{ + { + header: "By User", + data: data, + options: resourceTopOptions{ + group: "user", + sortBy: "cpu", + }, + }, + { + header: "By Org", + data: data, + options: resourceTopOptions{ + group: "org", + sortBy: "cpu", + }, + }, + { + header: "By Provider", + data: data, + options: resourceTopOptions{ + group: "provider", + sortBy: "cpu", + }, + }, + { + header: "Sort By Memory", + data: data, + options: resourceTopOptions{ + group: "user", + sortBy: "memory", + }, + }, + } + + for _, tcase := range tests { + buff.WriteString(fmt.Sprintf("=== TEST: %s\n", tcase.header)) + err := presentEntites(&buff, tcase.data, tcase.options) + assert.Success(t, "present entities", err) + } + + assertGolden(t, goldenFile, buff.Bytes()) +} + +func assertGolden(t *testing.T, path string, output []byte) { + if *write { + err := ioutil.WriteFile(path, output, 0777) + assert.Success(t, "write file", err) + return + } + goldenContent, err := ioutil.ReadFile(path) + assert.Success(t, "read golden file", err) + assert.Equal(t, "golden content matches", string(goldenContent), string(output)) +} + +func mockResourceTopEntities() entities { + orgIDs := [...]string{randString(10), randString(10), randString(10)} + imageIDs := [...]string{randString(10), randString(10), randString(10)} + providerIDs := [...]string{randString(10), randString(10), randString(10)} + userIDs := [...]string{randString(10), randString(10), randString(10)} + envIDs := [...]string{randString(10), randString(10), randString(10), randString(10)} + + return entities{ + providers: []coder.KubernetesProvider{ + { + ID: providerIDs[0], + Name: "mars", + }, + { + ID: providerIDs[1], + Name: "underground", + }, + }, + users: []coder.User{ + { + ID: userIDs[0], + Name: "Random", + Email: "random@coder.com", + }, + { + ID: userIDs[1], + Name: "Second Random", + Email: "second-random@coder.com", + }, + }, + orgs: []coder.Organization{ + { + ID: orgIDs[0], + Name: "SpecialOrg", + + //! these should probably be fixed, but for now they are just for the count + Members: []coder.OrganizationUser{{}, {}}, + }, + { + ID: orgIDs[1], + Name: "NotSoSpecialOrg", + + //! these should probably be fixed, but for now they are just for the count + Members: []coder.OrganizationUser{{}, {}}, + }, + }, + envs: []coder.Environment{ + { + ID: envIDs[0], + ResourcePoolID: providerIDs[0], + ImageID: imageIDs[0], + OrganizationID: orgIDs[0], + UserID: userIDs[0], + Name: "dev-env", + ImageTag: "20.04", + CPUCores: 12.2, + MemoryGB: 64.4, + LatestStat: coder.EnvironmentStat{ + ContainerStatus: coder.EnvironmentOn, + }, + }, + { + ID: envIDs[1], + ResourcePoolID: providerIDs[1], + ImageID: imageIDs[1], + OrganizationID: orgIDs[1], + UserID: userIDs[1], + Name: "another-env", + ImageTag: "10.2", + CPUCores: 4, + MemoryGB: 16, + LatestStat: coder.EnvironmentStat{ + ContainerStatus: coder.EnvironmentOn, + }, + }, + { + ID: envIDs[2], + ResourcePoolID: providerIDs[1], + ImageID: imageIDs[1], + OrganizationID: orgIDs[1], + UserID: userIDs[1], + Name: "yet-another-env", + ImageTag: "10.2", + CPUCores: 100, + MemoryGB: 2, + LatestStat: coder.EnvironmentStat{ + ContainerStatus: coder.EnvironmentOn, + }, + }, + }, + images: map[string]*coder.Image{ + imageIDs[0]: { + Repository: "ubuntu", + OrganizationID: orgIDs[0], + }, + imageIDs[1]: { + Repository: "archlinux", + OrganizationID: orgIDs[0], + }, + }, + } +} diff --git a/internal/cmd/resourcemanager_test.golden b/internal/cmd/resourcemanager_test.golden new file mode 100755 index 00000000..0707bd1a --- /dev/null +++ b/internal/cmd/resourcemanager_test.golden @@ -0,0 +1,32 @@ +=== TEST: By User +Second Random (second-random@coder.com) [cpu: 104.0] [mem: 18.0 GB] + yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] + another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] + +Random (random@coder.com) [cpu: 12.2] [mem: 64.4 GB] + dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] + +=== TEST: By Org +NotSoSpecialOrg (2 members) [cpu: 104.0] [mem: 18.0 GB] + yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] + another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] + +SpecialOrg (2 members) [cpu: 12.2] [mem: 64.4 GB] + dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] [provider: mars] + +=== TEST: By Provider +underground [cpu: 104.0] [mem: 18.0 GB] + yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] + another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] + +mars [cpu: 12.2] [mem: 64.4 GB] + dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] + +=== TEST: Sort By Memory +Random (random@coder.com) [cpu: 12.2] [mem: 64.4 GB] + dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] + +Second Random (second-random@coder.com) [cpu: 104.0] [mem: 18.0 GB] + another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] + yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] +