Skip to content

Commit 6cec719

Browse files
committed
Merge branch 'main' into 13383-scp
2 parents 00e0a8a + 48ecee1 commit 6cec719

File tree

125 files changed

+3244
-1000
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+3244
-1000
lines changed

cli/cliui/output.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88
"strings"
99

10+
"github.com/jedib0t/go-pretty/v6/table"
1011
"golang.org/x/xerrors"
1112

1213
"github.com/coder/serpent"
@@ -143,7 +144,11 @@ func (f *tableFormat) AttachOptions(opts *serpent.OptionSet) {
143144

144145
// Format implements OutputFormat.
145146
func (f *tableFormat) Format(_ context.Context, data any) (string, error) {
146-
return DisplayTable(data, f.sort, f.columns)
147+
headers := make(table.Row, len(f.allColumns))
148+
for i, header := range f.allColumns {
149+
headers[i] = header
150+
}
151+
return renderTable(data, f.sort, headers, f.columns)
147152
}
148153

149154
type jsonFormat struct{}

cli/cliui/table.go

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ func Table() table.Writer {
2222
return tableWriter
2323
}
2424

25+
// This type can be supplied as part of a slice to DisplayTable
26+
// or to a `TableFormat` `Format` call to render a separator.
27+
// Leading separators are not supported and trailing separators
28+
// are ignored by the table formatter.
29+
// e.g. `[]any{someRow, TableSeparator, someRow}`
30+
type TableSeparator struct{}
31+
2532
// filterTableColumns returns configurations to hide columns
2633
// that are not provided in the array. If the array is empty,
2734
// no filtering will occur!
@@ -47,8 +54,12 @@ func filterTableColumns(header table.Row, columns []string) []table.ColumnConfig
4754
return columnConfigs
4855
}
4956

50-
// DisplayTable renders a table as a string. The input argument must be a slice
51-
// of structs. At least one field in the struct must have a `table:""` tag
57+
// DisplayTable renders a table as a string. The input argument can be:
58+
// - a struct slice.
59+
// - an interface slice, where the first element is a struct,
60+
// and all other elements are of the same type, or a TableSeparator.
61+
//
62+
// At least one field in the struct must have a `table:""` tag
5263
// containing the name of the column in the outputted table.
5364
//
5465
// If `sort` is not specified, the field with the `table:"$NAME,default_sort"`
@@ -66,11 +77,20 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
6677
v := reflect.Indirect(reflect.ValueOf(out))
6778

6879
if v.Kind() != reflect.Slice {
69-
return "", xerrors.Errorf("DisplayTable called with a non-slice type")
80+
return "", xerrors.New("DisplayTable called with a non-slice type")
81+
}
82+
var tableType reflect.Type
83+
if v.Type().Elem().Kind() == reflect.Interface {
84+
if v.Len() == 0 {
85+
return "", xerrors.New("DisplayTable called with empty interface slice")
86+
}
87+
tableType = reflect.Indirect(reflect.ValueOf(v.Index(0).Interface())).Type()
88+
} else {
89+
tableType = v.Type().Elem()
7090
}
7191

7292
// Get the list of table column headers.
73-
headersRaw, defaultSort, err := typeToTableHeaders(v.Type().Elem(), true)
93+
headersRaw, defaultSort, err := typeToTableHeaders(tableType, true)
7494
if err != nil {
7595
return "", xerrors.Errorf("get table headers recursively for type %q: %w", v.Type().Elem().String(), err)
7696
}
@@ -82,9 +102,8 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
82102
}
83103
headers := make(table.Row, len(headersRaw))
84104
for i, header := range headersRaw {
85-
headers[i] = header
105+
headers[i] = strings.ReplaceAll(header, "_", " ")
86106
}
87-
88107
// Verify that the given sort column and filter columns are valid.
89108
if sort != "" || len(filterColumns) != 0 {
90109
headersMap := make(map[string]string, len(headersRaw))
@@ -130,6 +149,11 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
130149
return "", xerrors.Errorf("specified sort column %q not found in table headers, available columns are %q", sort, strings.Join(headersRaw, `", "`))
131150
}
132151
}
152+
return renderTable(out, sort, headers, filterColumns)
153+
}
154+
155+
func renderTable(out any, sort string, headers table.Row, filterColumns []string) (string, error) {
156+
v := reflect.Indirect(reflect.ValueOf(out))
133157

134158
// Setup the table formatter.
135159
tw := Table()
@@ -143,15 +167,22 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
143167

144168
// Write each struct to the table.
145169
for i := 0; i < v.Len(); i++ {
170+
cur := v.Index(i).Interface()
171+
_, ok := cur.(TableSeparator)
172+
if ok {
173+
tw.AppendSeparator()
174+
continue
175+
}
146176
// Format the row as a slice.
147-
rowMap, err := valueToTableMap(v.Index(i))
177+
// ValueToTableMap does what `reflect.Indirect` does
178+
rowMap, err := valueToTableMap(reflect.ValueOf(cur))
148179
if err != nil {
149180
return "", xerrors.Errorf("get table row map %v: %w", i, err)
150181
}
151182

152183
rowSlice := make([]any, len(headers))
153-
for i, h := range headersRaw {
154-
v, ok := rowMap[h]
184+
for i, h := range headers {
185+
v, ok := rowMap[h.(string)]
155186
if !ok {
156187
v = nil
157188
}
@@ -188,25 +219,28 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
188219
// returned. If the table tag is malformed, an error is returned.
189220
//
190221
// The returned name is transformed from "snake_case" to "normal text".
191-
func parseTableStructTag(field reflect.StructField) (name string, defaultSort, recursive bool, skipParentName bool, err error) {
222+
func parseTableStructTag(field reflect.StructField) (name string, defaultSort, noSortOpt, recursive, skipParentName bool, err error) {
192223
tags, err := structtag.Parse(string(field.Tag))
193224
if err != nil {
194-
return "", false, false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
225+
return "", false, false, false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
195226
}
196227

197228
tag, err := tags.Get("table")
198229
if err != nil || tag.Name == "-" {
199230
// tags.Get only returns an error if the tag is not found.
200-
return "", false, false, false, nil
231+
return "", false, false, false, false, nil
201232
}
202233

203234
defaultSortOpt := false
235+
noSortOpt = false
204236
recursiveOpt := false
205237
skipParentNameOpt := false
206238
for _, opt := range tag.Options {
207239
switch opt {
208240
case "default_sort":
209241
defaultSortOpt = true
242+
case "nosort":
243+
noSortOpt = true
210244
case "recursive":
211245
recursiveOpt = true
212246
case "recursive_inline":
@@ -216,11 +250,11 @@ func parseTableStructTag(field reflect.StructField) (name string, defaultSort, r
216250
recursiveOpt = true
217251
skipParentNameOpt = true
218252
default:
219-
return "", false, false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
253+
return "", false, false, false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
220254
}
221255
}
222256

223-
return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, recursiveOpt, skipParentNameOpt, nil
257+
return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, noSortOpt, recursiveOpt, skipParentNameOpt, nil
224258
}
225259

226260
func isStructOrStructPointer(t reflect.Type) bool {
@@ -244,12 +278,16 @@ func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string,
244278

245279
headers := []string{}
246280
defaultSortName := ""
281+
noSortOpt := false
247282
for i := 0; i < t.NumField(); i++ {
248283
field := t.Field(i)
249-
name, defaultSort, recursive, skip, err := parseTableStructTag(field)
284+
name, defaultSort, noSort, recursive, skip, err := parseTableStructTag(field)
250285
if err != nil {
251286
return nil, "", xerrors.Errorf("parse struct tags for field %q in type %q: %w", field.Name, t.String(), err)
252287
}
288+
if requireDefault && noSort {
289+
noSortOpt = true
290+
}
253291

254292
if name == "" && (recursive && skip) {
255293
return nil, "", xerrors.Errorf("a name is required for the field %q. "+
@@ -292,8 +330,8 @@ func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string,
292330
headers = append(headers, name)
293331
}
294332

295-
if defaultSortName == "" && requireDefault {
296-
return nil, "", xerrors.Errorf("no field marked as default_sort in type %q", t.String())
333+
if defaultSortName == "" && requireDefault && !noSortOpt {
334+
return nil, "", xerrors.Errorf("no field marked as default_sort or nosort in type %q", t.String())
297335
}
298336

299337
return headers, defaultSortName, nil
@@ -320,7 +358,7 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
320358
for i := 0; i < val.NumField(); i++ {
321359
field := val.Type().Field(i)
322360
fieldVal := val.Field(i)
323-
name, _, recursive, skip, err := parseTableStructTag(field)
361+
name, _, _, recursive, skip, err := parseTableStructTag(field)
324362
if err != nil {
325363
return nil, xerrors.Errorf("parse struct tags for field %q in type %T: %w", field.Name, val, err)
326364
}

cli/cliui/table_test.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,42 @@ Alice 25
218218
compareTables(t, expected, out)
219219
})
220220

221+
// This test ensures we can display dynamically typed slices
222+
t.Run("Interfaces", func(t *testing.T) {
223+
t.Parallel()
224+
225+
in := []any{tableTest1{}}
226+
out, err := cliui.DisplayTable(in, "", nil)
227+
t.Log("rendered table:\n" + out)
228+
require.NoError(t, err)
229+
other := []tableTest1{{}}
230+
expected, err := cliui.DisplayTable(other, "", nil)
231+
require.NoError(t, err)
232+
compareTables(t, expected, out)
233+
})
234+
235+
t.Run("WithSeparator", func(t *testing.T) {
236+
t.Parallel()
237+
expected := `
238+
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
239+
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
240+
-------------------------------------------------------------------------------------------------------------------------------------------------------------
241+
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
242+
-------------------------------------------------------------------------------------------------------------------------------------------------------------
243+
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
244+
`
245+
246+
var inlineIn []any
247+
for _, v := range in {
248+
inlineIn = append(inlineIn, v)
249+
inlineIn = append(inlineIn, cliui.TableSeparator{})
250+
}
251+
out, err := cliui.DisplayTable(inlineIn, "", nil)
252+
t.Log("rendered table:\n" + out)
253+
require.NoError(t, err)
254+
compareTables(t, expected, out)
255+
})
256+
221257
// This test ensures that safeties against invalid use of `table` tags
222258
// causes errors (even without data).
223259
t.Run("Errors", func(t *testing.T) {
@@ -255,14 +291,6 @@ Alice 25
255291
_, err := cliui.DisplayTable(in, "", nil)
256292
require.Error(t, err)
257293
})
258-
259-
t.Run("WithData", func(t *testing.T) {
260-
t.Parallel()
261-
262-
in := []any{tableTest1{}}
263-
_, err := cliui.DisplayTable(in, "", nil)
264-
require.Error(t, err)
265-
})
266294
})
267295

268296
t.Run("NotStruct", func(t *testing.T) {

cli/netcheck_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/json"
66
"testing"
77

8-
"github.com/stretchr/testify/assert"
98
"github.com/stretchr/testify/require"
109

1110
"github.com/coder/coder/v2/cli/clitest"
@@ -30,7 +29,8 @@ func TestNetcheck(t *testing.T) {
3029
var report healthsdk.DERPHealthReport
3130
require.NoError(t, json.Unmarshal(b, &report))
3231

33-
assert.True(t, report.Healthy)
32+
// We do not assert that the report is healthy, just that
33+
// it has the expected number of reports per region.
3434
require.Len(t, report.Regions, 1+1) // 1 built-in region + 1 test-managed STUN region
3535
for _, v := range report.Regions {
3636
require.Len(t, v.NodeReports, len(v.Region.Nodes))

cli/organizationroles.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func (r *RootCmd) organizationRoles() *serpent.Command {
3636
func (r *RootCmd) showOrganizationRoles() *serpent.Command {
3737
formatter := cliui.NewOutputFormatter(
3838
cliui.ChangeFormatterData(
39-
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "org_permissions", "user_permissions"}),
39+
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}),
4040
func(data any) (any, error) {
4141
inputs, ok := data.([]codersdk.AssignableRoles)
4242
if !ok {
@@ -103,7 +103,7 @@ func (r *RootCmd) showOrganizationRoles() *serpent.Command {
103103
func (r *RootCmd) editOrganizationRole() *serpent.Command {
104104
formatter := cliui.NewOutputFormatter(
105105
cliui.ChangeFormatterData(
106-
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "org_permissions", "user_permissions"}),
106+
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}),
107107
func(data any) (any, error) {
108108
typed, _ := data.(codersdk.Role)
109109
return []roleTableRow{roleToTableView(typed)}, nil
@@ -391,6 +391,6 @@ type roleTableRow struct {
391391
OrganizationID string `table:"organization_id"`
392392
SitePermissions string ` table:"site_permissions"`
393393
// map[<org_id>] -> Permissions
394-
OrganizationPermissions string `table:"org_permissions"`
394+
OrganizationPermissions string `table:"organization_permissions"`
395395
UserPermissions string `table:"user_permissions"`
396396
}

cli/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*co
169169
EmailDomain: vals.OIDC.EmailDomain,
170170
AllowSignups: vals.OIDC.AllowSignups.Value(),
171171
UsernameField: vals.OIDC.UsernameField.String(),
172+
NameField: vals.OIDC.NameField.String(),
172173
EmailField: vals.OIDC.EmailField.String(),
173174
AuthURLParams: vals.OIDC.AuthURLParams.Value,
174175
IgnoreUserInfo: vals.OIDC.IgnoreUserInfo.Value(),

cli/server_createadminuser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
222222
UserID: newUser.ID,
223223
CreatedAt: dbtime.Now(),
224224
UpdatedAt: dbtime.Now(),
225-
Roles: []string{rbac.RoleOrgAdmin(org.ID)},
225+
Roles: []string{rbac.ScopedRoleOrgAdmin(org.ID)},
226226
})
227227
if err != nil {
228228
return xerrors.Errorf("insert organization member: %w", err)

cli/server_createadminuser_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestServerCreateAdminUser(t *testing.T) {
7171
orgIDs2 := make(map[uuid.UUID]struct{}, len(orgMemberships))
7272
for _, membership := range orgMemberships {
7373
orgIDs2[membership.OrganizationID] = struct{}{}
74-
assert.Equal(t, []string{rbac.RoleOrgAdmin(membership.OrganizationID)}, membership.Roles, "user is not org admin")
74+
assert.Equal(t, []string{rbac.ScopedRoleOrgAdmin(membership.OrganizationID)}, membership.Roles, "user is not org admin")
7575
}
7676

7777
require.Equal(t, orgIDs, orgIDs2, "user is not in all orgs")

0 commit comments

Comments
 (0)