Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit acd0cd6

Browse files
authored
coder features list CLI command (#3533)
* AGPL Entitlements API Signed-off-by: Spike Curtis <[email protected]> * Generate typesGenerated.ts Signed-off-by: Spike Curtis <[email protected]> * AllFeatures -> FeatureNames Signed-off-by: Spike Curtis <[email protected]> * Features CLI command Signed-off-by: Spike Curtis <[email protected]> * Validate columns Signed-off-by: Spike Curtis <[email protected]> * Tests for features list CLI command Signed-off-by: Spike Curtis <[email protected]> * Drop empty EntitlementsRequest Signed-off-by: Spike Curtis <[email protected]> * Fix dump.sql generation Signed-off-by: Spike Curtis <[email protected]> Signed-off-by: Spike Curtis <[email protected]>
1 parent 5c898d0 commit acd0cd6

File tree

6 files changed

+215
-1
lines changed

6 files changed

+215
-1
lines changed

cli/cliui/table.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,19 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
301301

302302
return row, nil
303303
}
304+
305+
func ValidateColumns(all, given []string) error {
306+
for _, col := range given {
307+
found := false
308+
for _, c := range all {
309+
if strings.EqualFold(strings.ReplaceAll(col, "_", " "), c) {
310+
found = true
311+
break
312+
}
313+
}
314+
if !found {
315+
return fmt.Errorf("unknown column: %s", col)
316+
}
317+
}
318+
return nil
319+
}

cli/features.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/coder/coder/cli/cliui"
9+
"github.com/jedib0t/go-pretty/v6/table"
10+
11+
"github.com/spf13/cobra"
12+
"golang.org/x/xerrors"
13+
14+
"github.com/coder/coder/codersdk"
15+
)
16+
17+
var featureColumns = []string{"Name", "Entitlement", "Enabled", "Limit", "Actual"}
18+
19+
func features() *cobra.Command {
20+
cmd := &cobra.Command{
21+
Short: "List features",
22+
Use: "features",
23+
Aliases: []string{"feature"},
24+
}
25+
cmd.AddCommand(
26+
featuresList(),
27+
)
28+
return cmd
29+
}
30+
31+
func featuresList() *cobra.Command {
32+
var (
33+
columns []string
34+
outputFormat string
35+
)
36+
37+
cmd := &cobra.Command{
38+
Use: "list",
39+
Aliases: []string{"ls"},
40+
RunE: func(cmd *cobra.Command, args []string) error {
41+
err := cliui.ValidateColumns(featureColumns, columns)
42+
if err != nil {
43+
return err
44+
}
45+
client, err := createClient(cmd)
46+
if err != nil {
47+
return err
48+
}
49+
entitlements, err := client.Entitlements(cmd.Context())
50+
if err != nil {
51+
return err
52+
}
53+
54+
out := ""
55+
switch outputFormat {
56+
case "table", "":
57+
out = displayFeatures(columns, entitlements.Features)
58+
case "json":
59+
outBytes, err := json.Marshal(entitlements)
60+
if err != nil {
61+
return xerrors.Errorf("marshal users to JSON: %w", err)
62+
}
63+
64+
out = string(outBytes)
65+
default:
66+
return xerrors.Errorf(`unknown output format %q, only "table" and "json" are supported`, outputFormat)
67+
}
68+
69+
_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
70+
return err
71+
},
72+
}
73+
74+
cmd.Flags().StringArrayVarP(&columns, "column", "c", featureColumns,
75+
fmt.Sprintf("Specify a column to filter in the table. Available columns are: %s",
76+
strings.Join(featureColumns, ", ")))
77+
cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "Output format. Available formats are: table, json.")
78+
return cmd
79+
}
80+
81+
// displayFeatures will return a table displaying all features passed in.
82+
// filterColumns must be a subset of the feature fields and will determine which
83+
// columns to display
84+
func displayFeatures(filterColumns []string, features map[string]codersdk.Feature) string {
85+
tableWriter := cliui.Table()
86+
header := table.Row{}
87+
for _, h := range featureColumns {
88+
header = append(header, h)
89+
}
90+
tableWriter.AppendHeader(header)
91+
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, filterColumns))
92+
tableWriter.SortBy([]table.SortBy{{
93+
Name: "username",
94+
}})
95+
for name, feat := range features {
96+
tableWriter.AppendRow(table.Row{
97+
name,
98+
feat.Entitlement,
99+
feat.Enabled,
100+
intOrNil(feat.Limit),
101+
intOrNil(feat.Actual),
102+
})
103+
}
104+
return tableWriter.Render()
105+
}
106+
107+
func intOrNil(i *int64) string {
108+
if i == nil {
109+
return ""
110+
}
111+
return fmt.Sprintf("%d", *i)
112+
}

cli/features_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/cli/clitest"
12+
"github.com/coder/coder/coderd/coderdtest"
13+
"github.com/coder/coder/codersdk"
14+
"github.com/coder/coder/pty/ptytest"
15+
)
16+
17+
func TestFeaturesList(t *testing.T) {
18+
t.Parallel()
19+
t.Run("Table", func(t *testing.T) {
20+
t.Parallel()
21+
client := coderdtest.New(t, nil)
22+
coderdtest.CreateFirstUser(t, client)
23+
cmd, root := clitest.New(t, "features", "list")
24+
clitest.SetupConfig(t, client, root)
25+
pty := ptytest.New(t)
26+
cmd.SetIn(pty.Input())
27+
cmd.SetOut(pty.Output())
28+
errC := make(chan error)
29+
go func() {
30+
errC <- cmd.Execute()
31+
}()
32+
require.NoError(t, <-errC)
33+
pty.ExpectMatch("user_limit")
34+
pty.ExpectMatch("not_entitled")
35+
})
36+
t.Run("JSON", func(t *testing.T) {
37+
t.Parallel()
38+
39+
client := coderdtest.New(t, nil)
40+
coderdtest.CreateFirstUser(t, client)
41+
cmd, root := clitest.New(t, "features", "list", "-o", "json")
42+
clitest.SetupConfig(t, client, root)
43+
doneChan := make(chan struct{})
44+
45+
buf := bytes.NewBuffer(nil)
46+
cmd.SetOut(buf)
47+
go func() {
48+
defer close(doneChan)
49+
err := cmd.Execute()
50+
assert.NoError(t, err)
51+
}()
52+
53+
<-doneChan
54+
55+
var entitlements codersdk.Entitlements
56+
err := json.Unmarshal(buf.Bytes(), &entitlements)
57+
require.NoError(t, err, "unmarshal JSON output")
58+
assert.Len(t, entitlements.Features, 2)
59+
assert.Empty(t, entitlements.Warnings)
60+
assert.Equal(t, codersdk.EntitlementNotEntitled,
61+
entitlements.Features[codersdk.FeatureUserLimit].Entitlement)
62+
assert.Equal(t, codersdk.EntitlementNotEntitled,
63+
entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
64+
assert.False(t, entitlements.HasLicense)
65+
})
66+
}

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ func Root() *cobra.Command {
135135
versionCmd(),
136136
wireguardPortForward(),
137137
workspaceAgent(),
138+
features(),
138139
)
139140

140141
cmd.SetUsageTemplate(usageTemplate())

coderd/features.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func entitlements(rw http.ResponseWriter, _ *http.Request) {
1717
}
1818
httpapi.Write(rw, http.StatusOK, codersdk.Entitlements{
1919
Features: features,
20-
Warnings: nil,
20+
Warnings: []string{},
2121
HasLicense: false,
2222
})
2323
}

codersdk/features.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package codersdk
22

3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
)
8+
39
type Entitlement string
410

511
const (
@@ -27,3 +33,16 @@ type Entitlements struct {
2733
Warnings []string `json:"warnings"`
2834
HasLicense bool `json:"has_license"`
2935
}
36+
37+
func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {
38+
res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil)
39+
if err != nil {
40+
return Entitlements{}, err
41+
}
42+
defer res.Body.Close()
43+
if res.StatusCode != http.StatusOK {
44+
return Entitlements{}, readBodyAsError(res)
45+
}
46+
var ent Entitlements
47+
return ent, json.NewDecoder(res.Body).Decode(&ent)
48+
}

0 commit comments

Comments
 (0)