From 1a959c6e9a375747e2514dd8dd3baf3ff955ddb8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 10 Jul 2023 13:59:32 -0400 Subject: [PATCH 1/8] feat: add table format to 'coder license ls' --- codersdk/licenses.go | 8 ++++---- enterprise/cli/licenses.go | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 388055fd86bad..65a269fbca449 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -16,14 +16,14 @@ type AddLicenseRequest struct { } type License struct { - ID int32 `json:"id"` - UUID uuid.UUID `json:"uuid" format:"uuid"` - UploadedAt time.Time `json:"uploaded_at" format:"date-time"` + ID int32 `json:"id" table:"id,default_sort"` + UUID uuid.UUID `json:"uuid" table:"uuid" format:"uuid"` + UploadedAt time.Time `json:"uploaded_at" table:"uploaded_at" format:"date-time"` // Claims are the JWT claims asserted by the license. Here we use // a generic string map to ensure that all data from the server is // parsed verbatim, not just the fields this version of Coder // understands. - Claims map[string]interface{} `json:"claims"` + Claims map[string]interface{} `json:"claims" table:"claims"` } // Features provides the feature claims in license. diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go index 1ed12669ae9b0..f433cb1a1f0db 100644 --- a/enterprise/cli/licenses.go +++ b/enterprise/cli/licenses.go @@ -136,6 +136,11 @@ func validJWT(s string) error { } func (r *RootCmd) licensesList() *clibase.Cmd { + formatter := cliui.NewOutputFormatter( + cliui.TableFormat([]codersdk.License{}, []string{"UUID", "Claims", "Uploaded At"}), + cliui.JSONFormat(), + ) + client := new(codersdk.Client) cmd := &clibase.Cmd{ Use: "list", @@ -163,11 +168,16 @@ func (r *RootCmd) licensesList() *clibase.Cmd { licenses[i].Claims = newClaims } - enc := json.NewEncoder(inv.Stdout) - enc.SetIndent("", " ") - return enc.Encode(licenses) + out, err := formatter.Format(inv.Context(), licenses) + if err != nil { + return err + } + + _, err = fmt.Fprintln(inv.Stdout, out) + return err }, } + formatter.AttachOptions(&cmd.Options) return cmd } From 6fa7d8db2975f86673f420fa289e1cd43b106abb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 11 Jul 2023 09:18:12 -0400 Subject: [PATCH 2/8] feat: license expires_at to table view --- codersdk/licenses.go | 34 ++++++++++++++++ enterprise/cli/licenses.go | 79 +++++++++++++++++++++----------------- 2 files changed, 78 insertions(+), 35 deletions(-) diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 65a269fbca449..7b53c7bebf481 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -11,6 +11,10 @@ import ( "golang.org/x/xerrors" ) +const ( + LicenseExpiryClaim = "license_expires" +) + type AddLicenseRequest struct { License string `json:"license" validate:"required"` } @@ -26,6 +30,36 @@ type License struct { Claims map[string]interface{} `json:"claims" table:"claims"` } +// ExpiresAt returns the expiration time of the license. +// If the claim is missing or has an unexpected type, an error is returned. +func (l *License) ExpiresAt() (time.Time, error) { + expClaim, ok := l.Claims[LicenseExpiryClaim] + if !ok { + return time.Time{}, xerrors.New("license_expires claim is missing") + } + + // The claim could be a unix timestamp or a RFC3339 formatted string. + // Everything is already an interface{}, so we need to do some type + // assertions to figure out what we're dealing with. + if unix, ok := expClaim.(json.Number); ok { + i64, err := unix.Int64() + if err != nil { + return time.Time{}, xerrors.Errorf("license_expires claim is not a valid unix timestamp: %w", err) + } + return time.Unix(i64, 0), nil + } + + if str, ok := expClaim.(string); ok { + t, err := time.Parse(time.RFC3339, str) + if err != nil { + return time.Time{}, xerrors.Errorf("license_expires claim is not a valid RFC3339 timestamp: %w", err) + } + return t, nil + } + + return time.Time{}, xerrors.Errorf("license_expires claim has unexpected type %T", expClaim) +} + // Features provides the feature claims in license. func (l *License) Features() (map[FeatureName]int64, error) { strMap, ok := l.Claims["features"].(map[string]interface{}) diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go index f433cb1a1f0db..a740fa834f08b 100644 --- a/enterprise/cli/licenses.go +++ b/enterprise/cli/licenses.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" "github.com/coder/coder/codersdk" + "github.com/google/uuid" ) var jwtRegexp = regexp.MustCompile(`^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$`) @@ -136,8 +137,50 @@ func validJWT(s string) error { } func (r *RootCmd) licensesList() *clibase.Cmd { + type tableLicense struct { + ID int32 `table:"id,default_sort"` + UUID uuid.UUID `table:"uuid" format:"uuid"` + UploadedAt time.Time `table:"uploaded_at" format:"date-time"` + // Claims is the formatted string for the license claims. + // Used for the table view. + Claims string `table:"claims"` + ExpiresAt time.Time `table:"expires_at" format:"date-time"` + } + formatter := cliui.NewOutputFormatter( - cliui.TableFormat([]codersdk.License{}, []string{"UUID", "Claims", "Uploaded At"}), + cliui.ChangeFormatterData( + cliui.TableFormat([]tableLicense{}, []string{"UUID", "Expires At", "Uploaded At", "Claims"}), + func(data any) (any, error) { + list, ok := data.([]codersdk.License) + if !ok { + return nil, xerrors.Errorf("invalid data type %T", data) + } + out := make([]tableLicense, 0, len(list)) + for _, lic := range list { + var claims string + features, err := lic.Features() + if err != nil { + claims = xerrors.Errorf("invalid license: %w", err).Error() + } else { + var strs []string + for k, v := range features { + strs = append(strs, fmt.Sprintf("%s=%v", k, v)) + } + claims = strings.Join(strs, ", ") + } + // If this errors a zero time is returned. + exp, _ := lic.ExpiresAt() + + out = append(out, tableLicense{ + ID: lic.ID, + UUID: lic.UUID, + UploadedAt: lic.UploadedAt, + Claims: claims, + ExpiresAt: exp, + }) + } + return out, nil + }), cliui.JSONFormat(), ) @@ -160,14 +203,6 @@ func (r *RootCmd) licensesList() *clibase.Cmd { licenses = make([]codersdk.License, 0) } - for i, license := range licenses { - newClaims, err := convertLicenseExpireTime(license.Claims) - if err != nil { - return err - } - licenses[i].Claims = newClaims - } - out, err := formatter.Format(inv.Context(), licenses) if err != nil { return err @@ -206,29 +241,3 @@ func (r *RootCmd) licenseDelete() *clibase.Cmd { } return cmd } - -func convertLicenseExpireTime(licenseClaims map[string]interface{}) (map[string]interface{}, error) { - if licenseClaims["license_expires"] != nil { - licenseExpiresNumber, ok := licenseClaims["license_expires"].(json.Number) - if !ok { - return licenseClaims, xerrors.Errorf("could not convert license_expires to json.Number") - } - - licenseExpires, err := licenseExpiresNumber.Int64() - if err != nil { - return licenseClaims, xerrors.Errorf("could not convert license_expires to int64: %w", err) - } - - t := time.Unix(licenseExpires, 0) - rfc3339Format := t.Format(time.RFC3339) - - claimsCopy := make(map[string]interface{}, len(licenseClaims)) - for k, v := range licenseClaims { - claimsCopy[k] = v - } - - claimsCopy["license_expires"] = rfc3339Format - return claimsCopy, nil - } - return licenseClaims, nil -} From a05b88eabb300bf07deccd7980c7c3e0e13e836e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 11 Jul 2023 09:28:25 -0400 Subject: [PATCH 3/8] Add back human expiration to json --- codersdk/licenses.go | 16 ++++------------ enterprise/cli/licenses.go | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 7b53c7bebf481..07784c78db798 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -20,9 +20,9 @@ type AddLicenseRequest struct { } type License struct { - ID int32 `json:"id" table:"id,default_sort"` - UUID uuid.UUID `json:"uuid" table:"uuid" format:"uuid"` - UploadedAt time.Time `json:"uploaded_at" table:"uploaded_at" format:"date-time"` + ID int32 `json:"id"` + UUID uuid.UUID `json:"uuid" format:"uuid"` + UploadedAt time.Time `json:"uploaded_at" format:"date-time"` // Claims are the JWT claims asserted by the license. Here we use // a generic string map to ensure that all data from the server is // parsed verbatim, not just the fields this version of Coder @@ -38,7 +38,7 @@ func (l *License) ExpiresAt() (time.Time, error) { return time.Time{}, xerrors.New("license_expires claim is missing") } - // The claim could be a unix timestamp or a RFC3339 formatted string. + // This claim should be a unix timestamp. // Everything is already an interface{}, so we need to do some type // assertions to figure out what we're dealing with. if unix, ok := expClaim.(json.Number); ok { @@ -49,14 +49,6 @@ func (l *License) ExpiresAt() (time.Time, error) { return time.Unix(i64, 0), nil } - if str, ok := expClaim.(string); ok { - t, err := time.Parse(time.RFC3339, str) - if err != nil { - return time.Time{}, xerrors.Errorf("license_expires claim is not a valid RFC3339 timestamp: %w", err) - } - return t, nil - } - return time.Time{}, xerrors.Errorf("license_expires claim has unexpected type %T", expClaim) } diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go index a740fa834f08b..f8e3f18572e35 100644 --- a/enterprise/cli/licenses.go +++ b/enterprise/cli/licenses.go @@ -181,7 +181,20 @@ func (r *RootCmd) licensesList() *clibase.Cmd { } return out, nil }), - cliui.JSONFormat(), + cliui.ChangeFormatterData(cliui.JSONFormat(), func(data any) (any, error) { + list, ok := data.([]codersdk.License) + if !ok { + return nil, xerrors.Errorf("invalid data type %T", data) + } + for i := range list { + humanExp, err := list[i].ExpiresAt() + if err == nil { + list[i].Claims[codersdk.LicenseExpiryClaim+"_human"] = humanExp.Format(time.RFC3339) + } + } + + return list, nil + }), ) client := new(codersdk.Client) From 0de5426fe09fa3fd5b57204576490fbd7e363eda Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 11 Jul 2023 09:34:34 -0400 Subject: [PATCH 4/8] Parse all_features --- codersdk/licenses.go | 13 +++++++++++-- enterprise/cli/licenses.go | 21 ++++++++++++--------- enterprise/coderd/licenses_test.go | 6 +++--- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 07784c78db798..12170b8519988 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -52,8 +52,17 @@ func (l *License) ExpiresAt() (time.Time, error) { return time.Time{}, xerrors.Errorf("license_expires claim has unexpected type %T", expClaim) } -// Features provides the feature claims in license. -func (l *License) Features() (map[FeatureName]int64, error) { +func (l *License) AllFeaturesClaim() bool { + if all, ok := l.Claims["all_features"].(bool); ok { + return all + } + return false +} + +// FeaturesClaims provides the feature claims in license. +// This only returns the explicit claims. If checking for actual usage, +// also check `AllFeaturesClaim`. +func (l *License) FeaturesClaims() (map[FeatureName]int64, error) { strMap, ok := l.Claims["features"].(map[string]interface{}) if !ok { return nil, xerrors.New("features key is unexpected type") diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go index f8e3f18572e35..85f2155a1b928 100644 --- a/enterprise/cli/licenses.go +++ b/enterprise/cli/licenses.go @@ -141,15 +141,15 @@ func (r *RootCmd) licensesList() *clibase.Cmd { ID int32 `table:"id,default_sort"` UUID uuid.UUID `table:"uuid" format:"uuid"` UploadedAt time.Time `table:"uploaded_at" format:"date-time"` - // Claims is the formatted string for the license claims. + // Features is the formatted string for the license claims. // Used for the table view. - Claims string `table:"claims"` + Features string `table:"features"` ExpiresAt time.Time `table:"expires_at" format:"date-time"` } formatter := cliui.NewOutputFormatter( cliui.ChangeFormatterData( - cliui.TableFormat([]tableLicense{}, []string{"UUID", "Expires At", "Uploaded At", "Claims"}), + cliui.TableFormat([]tableLicense{}, []string{"UUID", "Expires At", "Uploaded At", "Features"}), func(data any) (any, error) { list, ok := data.([]codersdk.License) if !ok { @@ -157,25 +157,28 @@ func (r *RootCmd) licensesList() *clibase.Cmd { } out := make([]tableLicense, 0, len(list)) for _, lic := range list { - var claims string - features, err := lic.Features() + var formattedFeatures string + features, err := lic.FeaturesClaims() if err != nil { - claims = xerrors.Errorf("invalid license: %w", err).Error() + formattedFeatures = xerrors.Errorf("invalid license: %w", err).Error() } else { var strs []string + if lic.AllFeaturesClaim() { + strs = append(strs, "all") + } for k, v := range features { strs = append(strs, fmt.Sprintf("%s=%v", k, v)) } - claims = strings.Join(strs, ", ") + formattedFeatures = strings.Join(strs, ", ") } - // If this errors a zero time is returned. + // If this returns an error, a zero time is returned. exp, _ := lic.ExpiresAt() out = append(out, tableLicense{ ID: lic.ID, UUID: lic.UUID, UploadedAt: lic.UploadedAt, - Claims: claims, + Features: formattedFeatures, ExpiresAt: exp, }) } diff --git a/enterprise/coderd/licenses_test.go b/enterprise/coderd/licenses_test.go index 4c0595cc12fa8..26eb950d0afb1 100644 --- a/enterprise/coderd/licenses_test.go +++ b/enterprise/coderd/licenses_test.go @@ -33,7 +33,7 @@ func TestPostLicense(t *testing.T) { assert.GreaterOrEqual(t, respLic.ID, int32(0)) // just a couple spot checks for sanity assert.Equal(t, "testing", respLic.Claims["account_id"]) - features, err := respLic.Features() + features, err := respLic.FeaturesClaims() require.NoError(t, err) assert.EqualValues(t, 1, features[codersdk.FeatureAuditLog]) }) @@ -105,7 +105,7 @@ func TestGetLicense(t *testing.T) { assert.Equal(t, int32(1), licenses[0].ID) assert.Equal(t, "testing", licenses[0].Claims["account_id"]) - features, err := licenses[0].Features() + features, err := licenses[0].FeaturesClaims() require.NoError(t, err) assert.Equal(t, map[codersdk.FeatureName]int64{ codersdk.FeatureAuditLog: 1, @@ -117,7 +117,7 @@ func TestGetLicense(t *testing.T) { assert.Equal(t, "testing2", licenses[1].Claims["account_id"]) assert.Equal(t, true, licenses[1].Claims["trial"]) - features, err = licenses[1].Features() + features, err = licenses[1].FeaturesClaims() require.NoError(t, err) assert.Equal(t, map[codersdk.FeatureName]int64{ codersdk.FeatureUserLimit: 200, From d185ff86d2bf97aa0bb4a42a5d4f1a80f333586f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 11 Jul 2023 09:36:34 -0400 Subject: [PATCH 5/8] Add trial to parsed fields --- codersdk/licenses.go | 7 +++++++ enterprise/cli/licenses.go | 2 ++ 2 files changed, 9 insertions(+) diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 12170b8519988..56bf63a9e6a43 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -52,6 +52,13 @@ func (l *License) ExpiresAt() (time.Time, error) { return time.Time{}, xerrors.Errorf("license_expires claim has unexpected type %T", expClaim) } +func (l *License) Trail() bool { + if trail, ok := l.Claims["trail"].(bool); ok { + return trail + } + return false +} + func (l *License) AllFeaturesClaim() bool { if all, ok := l.Claims["all_features"].(bool); ok { return all diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go index 85f2155a1b928..baf8bf3bda4c4 100644 --- a/enterprise/cli/licenses.go +++ b/enterprise/cli/licenses.go @@ -145,6 +145,7 @@ func (r *RootCmd) licensesList() *clibase.Cmd { // Used for the table view. Features string `table:"features"` ExpiresAt time.Time `table:"expires_at" format:"date-time"` + Trial bool `table:"trial"` } formatter := cliui.NewOutputFormatter( @@ -180,6 +181,7 @@ func (r *RootCmd) licensesList() *clibase.Cmd { UploadedAt: lic.UploadedAt, Features: formattedFeatures, ExpiresAt: exp, + Trial: lic.Trail(), }) } return out, nil From 74d99c2f70325fb577dbc03e8382765f63074fcf Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 11 Jul 2023 09:52:21 -0400 Subject: [PATCH 6/8] make gen --- docs/cli/licenses_list.md | 22 ++++++++++++++++++- .../coder_licenses_list_--help.golden | 10 ++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/cli/licenses_list.md b/docs/cli/licenses_list.md index 670eae648104f..88b524dcea336 100644 --- a/docs/cli/licenses_list.md +++ b/docs/cli/licenses_list.md @@ -11,5 +11,25 @@ Aliases: ## Usage ```console -coder licenses list +coder licenses list [flags] ``` + +## Options + +### -c, --column + +| | | +| ------- | ------------------------------------------------- | +| Type | string-array | +| Default | UUID,Expires At,Uploaded At,Features | + +Columns to display in table output. Available columns: id, uuid, uploaded at, features, expires at, trial. + +### -o, --output + +| | | +| ------- | ------------------- | +| Type | string | +| Default | table | + +Output format. Available formats: table, json. diff --git a/enterprise/cli/testdata/coder_licenses_list_--help.golden b/enterprise/cli/testdata/coder_licenses_list_--help.golden index b04256e1d1839..7ccab7ae23f8d 100644 --- a/enterprise/cli/testdata/coder_licenses_list_--help.golden +++ b/enterprise/cli/testdata/coder_licenses_list_--help.golden @@ -1,8 +1,16 @@ -Usage: coder licenses list +Usage: coder licenses list [flags] List licenses (including expired) Aliases: ls +Options + -c, --column string-array (default: UUID,Expires At,Uploaded At,Features) + Columns to display in table output. Available columns: id, uuid, + uploaded at, features, expires at, trial. + + -o, --output string (default: table) + Output format. Available formats: table, json. + --- Run `coder --help` for a list of global options. From 3385ee90c6435f169dd27946f929e8130e2f475c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 11 Jul 2023 10:01:41 -0400 Subject: [PATCH 7/8] Fix unit tests --- enterprise/cli/licenses_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/enterprise/cli/licenses_test.go b/enterprise/cli/licenses_test.go index fc4fb2bdc04ab..3428c7437c7aa 100644 --- a/enterprise/cli/licenses_test.go +++ b/enterprise/cli/licenses_test.go @@ -143,7 +143,7 @@ func TestLicensesListFake(t *testing.T) { expectedLicenseExpires := time.Date(2024, 4, 6, 16, 53, 35, 0, time.UTC) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - inv := setupFakeLicenseServerTest(t, "licenses", "list") + inv := setupFakeLicenseServerTest(t, "licenses", "list", "-o", "json") stdout := new(bytes.Buffer) inv.Stdout = stdout errC := make(chan error) @@ -159,9 +159,9 @@ func TestLicensesListFake(t *testing.T) { assert.Equal(t, "claim1", licenses[0].Claims["h1"]) assert.Equal(t, int32(5), licenses[1].ID) assert.Equal(t, "claim2", licenses[1].Claims["h2"]) - expiresClaim := licenses[0].Claims["license_expires"] + expiresClaim := licenses[0].Claims["license_expires_human"] expiresString, ok := expiresClaim.(string) - require.True(t, ok, "license_expires claim is not a string") + require.True(t, ok, "license_expires_human claim is not a string") assert.NotEmpty(t, expiresClaim) expiresTime, err := time.Parse(time.RFC3339, expiresString) require.NoError(t, err) @@ -177,7 +177,7 @@ func TestLicensesListReal(t *testing.T) { coderdtest.CreateFirstUser(t, client) inv, conf := newCLI( t, - "licenses", "list", + "licenses", "list", "-o", "json", ) stdout := new(bytes.Buffer) inv.Stdout = stdout From a4e475c97d549ea17d965b42475b0bdf8e8e78e5 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 12 Jul 2023 07:50:50 -0400 Subject: [PATCH 8/8] fix typo, exclude 0 claims --- codersdk/licenses.go | 2 +- enterprise/cli/licenses.go | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 56bf63a9e6a43..d7634c72bf4ff 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -52,7 +52,7 @@ func (l *License) ExpiresAt() (time.Time, error) { return time.Time{}, xerrors.Errorf("license_expires claim has unexpected type %T", expClaim) } -func (l *License) Trail() bool { +func (l *License) Trial() bool { if trail, ok := l.Claims["trail"].(bool); ok { return trail } diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go index baf8bf3bda4c4..4258081df3e24 100644 --- a/enterprise/cli/licenses.go +++ b/enterprise/cli/licenses.go @@ -165,10 +165,15 @@ func (r *RootCmd) licensesList() *clibase.Cmd { } else { var strs []string if lic.AllFeaturesClaim() { - strs = append(strs, "all") - } - for k, v := range features { - strs = append(strs, fmt.Sprintf("%s=%v", k, v)) + // If all features are enabled, just include that + strs = append(strs, "all features") + } else { + for k, v := range features { + if v > 0 { + // Only include claims > 0 + strs = append(strs, fmt.Sprintf("%s=%v", k, v)) + } + } } formattedFeatures = strings.Join(strs, ", ") } @@ -181,7 +186,7 @@ func (r *RootCmd) licensesList() *clibase.Cmd { UploadedAt: lic.UploadedAt, Features: formattedFeatures, ExpiresAt: exp, - Trial: lic.Trail(), + Trial: lic.Trial(), }) } return out, nil pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy