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

Skip to content

Commit 7c40f86

Browse files
authored
feat(cli): include license status in support bundle (#18472)
Closes #18207 This PR adds license status to support bundle to help with troubleshooting license-related issues. - `license-status.txt`, is added to the support bundle. - it contains the same output as the `coder license list` command. - license output formatter logic has been extracted into a separate function. - this allows it to be reused both in the `coder license list` cmd and in the support bundle generation.
1 parent 2afd1a2 commit 7c40f86

File tree

6 files changed

+129
-72
lines changed

6 files changed

+129
-72
lines changed

cli/cliutil/license.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package cliutil
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/cli/cliui"
12+
"github.com/coder/coder/v2/codersdk"
13+
)
14+
15+
// NewLicenseFormatter returns a new license formatter.
16+
// The formatter will return a table and JSON output.
17+
func NewLicenseFormatter() *cliui.OutputFormatter {
18+
type tableLicense struct {
19+
ID int32 `table:"id,default_sort"`
20+
UUID uuid.UUID `table:"uuid" format:"uuid"`
21+
UploadedAt time.Time `table:"uploaded at" format:"date-time"`
22+
// Features is the formatted string for the license claims.
23+
// Used for the table view.
24+
Features string `table:"features"`
25+
ExpiresAt time.Time `table:"expires at" format:"date-time"`
26+
Trial bool `table:"trial"`
27+
}
28+
29+
return cliui.NewOutputFormatter(
30+
cliui.ChangeFormatterData(
31+
cliui.TableFormat([]tableLicense{}, []string{"ID", "UUID", "Expires At", "Uploaded At", "Features"}),
32+
func(data any) (any, error) {
33+
list, ok := data.([]codersdk.License)
34+
if !ok {
35+
return nil, xerrors.Errorf("invalid data type %T", data)
36+
}
37+
out := make([]tableLicense, 0, len(list))
38+
for _, lic := range list {
39+
var formattedFeatures string
40+
features, err := lic.FeaturesClaims()
41+
if err != nil {
42+
formattedFeatures = xerrors.Errorf("invalid license: %w", err).Error()
43+
} else {
44+
var strs []string
45+
if lic.AllFeaturesClaim() {
46+
// If all features are enabled, just include that
47+
strs = append(strs, "all features")
48+
} else {
49+
for k, v := range features {
50+
if v > 0 {
51+
// Only include claims > 0
52+
strs = append(strs, fmt.Sprintf("%s=%v", k, v))
53+
}
54+
}
55+
}
56+
formattedFeatures = strings.Join(strs, ", ")
57+
}
58+
// If this returns an error, a zero time is returned.
59+
exp, _ := lic.ExpiresAt()
60+
61+
out = append(out, tableLicense{
62+
ID: lic.ID,
63+
UUID: lic.UUID,
64+
UploadedAt: lic.UploadedAt,
65+
Features: formattedFeatures,
66+
ExpiresAt: exp,
67+
Trial: lic.Trial(),
68+
})
69+
}
70+
return out, nil
71+
}),
72+
cliui.ChangeFormatterData(cliui.JSONFormat(), func(data any) (any, error) {
73+
list, ok := data.([]codersdk.License)
74+
if !ok {
75+
return nil, xerrors.Errorf("invalid data type %T", data)
76+
}
77+
for i := range list {
78+
humanExp, err := list[i].ExpiresAt()
79+
if err == nil {
80+
list[i].Claims[codersdk.LicenseExpiryClaim+"_human"] = humanExp.Format(time.RFC3339)
81+
}
82+
}
83+
84+
return list, nil
85+
}),
86+
)
87+
}

cli/support.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"archive/zip"
55
"bytes"
6+
"context"
67
"encoding/base64"
78
"encoding/json"
89
"fmt"
@@ -13,6 +14,8 @@ import (
1314
"text/tabwriter"
1415
"time"
1516

17+
"github.com/coder/coder/v2/cli/cliutil"
18+
1619
"github.com/google/uuid"
1720
"golang.org/x/xerrors"
1821

@@ -48,6 +51,7 @@ var supportBundleBlurb = cliui.Bold("This will collect the following information
4851
- Agent details (with environment variable sanitized)
4952
- Agent network diagnostics
5053
- Agent logs
54+
- License status
5155
` + cliui.Bold("Note: ") +
5256
cliui.Wrap("While we try to sanitize sensitive data from support bundles, we cannot guarantee that they do not contain information that you or your organization may consider sensitive.\n") +
5357
cliui.Bold("Please confirm that you will:\n") +
@@ -302,6 +306,11 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
302306
return xerrors.Errorf("decode template zip from base64")
303307
}
304308

309+
licenseStatus, err := humanizeLicenses(src.Deployment.Licenses)
310+
if err != nil {
311+
return xerrors.Errorf("format license status: %w", err)
312+
}
313+
305314
// The below we just write as we have them:
306315
for k, v := range map[string]string{
307316
"agent/logs.txt": string(src.Agent.Logs),
@@ -315,6 +324,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
315324
"network/tailnet_debug.html": src.Network.TailnetDebug,
316325
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
317326
"workspace/template_file.zip": string(templateVersionBytes),
327+
"license-status.txt": licenseStatus,
318328
} {
319329
f, err := dest.Create(k)
320330
if err != nil {
@@ -359,3 +369,13 @@ func humanizeBuildLogs(ls []codersdk.ProvisionerJobLog) string {
359369
_ = tw.Flush()
360370
return buf.String()
361371
}
372+
373+
func humanizeLicenses(licenses []codersdk.License) (string, error) {
374+
formatter := cliutil.NewLicenseFormatter()
375+
376+
if len(licenses) == 0 {
377+
return "No licenses found", nil
378+
}
379+
380+
return formatter.Format(context.Background(), licenses)
381+
}

cli/support_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,9 @@ func assertBundleContents(t *testing.T, path string, wantWorkspace bool, wantAge
386386
case "cli_logs.txt":
387387
bs := readBytesFromZip(t, f)
388388
require.NotEmpty(t, bs, "CLI logs should not be empty")
389+
case "license-status.txt":
390+
bs := readBytesFromZip(t, f)
391+
require.NotEmpty(t, bs, "license status should not be empty")
389392
default:
390393
require.Failf(t, "unexpected file in bundle", f.Name)
391394
}

enterprise/cli/licenses.go

Lines changed: 2 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import (
88
"regexp"
99
"strconv"
1010
"strings"
11-
"time"
1211

13-
"github.com/google/uuid"
1412
"golang.org/x/xerrors"
1513

1614
"github.com/coder/coder/v2/cli/cliui"
15+
"github.com/coder/coder/v2/cli/cliutil"
1716
"github.com/coder/coder/v2/codersdk"
1817
"github.com/coder/serpent"
1918
)
@@ -137,76 +136,7 @@ func validJWT(s string) error {
137136
}
138137

139138
func (r *RootCmd) licensesList() *serpent.Command {
140-
type tableLicense struct {
141-
ID int32 `table:"id,default_sort"`
142-
UUID uuid.UUID `table:"uuid" format:"uuid"`
143-
UploadedAt time.Time `table:"uploaded at" format:"date-time"`
144-
// Features is the formatted string for the license claims.
145-
// Used for the table view.
146-
Features string `table:"features"`
147-
ExpiresAt time.Time `table:"expires at" format:"date-time"`
148-
Trial bool `table:"trial"`
149-
}
150-
151-
formatter := cliui.NewOutputFormatter(
152-
cliui.ChangeFormatterData(
153-
cliui.TableFormat([]tableLicense{}, []string{"ID", "UUID", "Expires At", "Uploaded At", "Features"}),
154-
func(data any) (any, error) {
155-
list, ok := data.([]codersdk.License)
156-
if !ok {
157-
return nil, xerrors.Errorf("invalid data type %T", data)
158-
}
159-
out := make([]tableLicense, 0, len(list))
160-
for _, lic := range list {
161-
var formattedFeatures string
162-
features, err := lic.FeaturesClaims()
163-
if err != nil {
164-
formattedFeatures = xerrors.Errorf("invalid license: %w", err).Error()
165-
} else {
166-
var strs []string
167-
if lic.AllFeaturesClaim() {
168-
// If all features are enabled, just include that
169-
strs = append(strs, "all features")
170-
} else {
171-
for k, v := range features {
172-
if v > 0 {
173-
// Only include claims > 0
174-
strs = append(strs, fmt.Sprintf("%s=%v", k, v))
175-
}
176-
}
177-
}
178-
formattedFeatures = strings.Join(strs, ", ")
179-
}
180-
// If this returns an error, a zero time is returned.
181-
exp, _ := lic.ExpiresAt()
182-
183-
out = append(out, tableLicense{
184-
ID: lic.ID,
185-
UUID: lic.UUID,
186-
UploadedAt: lic.UploadedAt,
187-
Features: formattedFeatures,
188-
ExpiresAt: exp,
189-
Trial: lic.Trial(),
190-
})
191-
}
192-
return out, nil
193-
}),
194-
cliui.ChangeFormatterData(cliui.JSONFormat(), func(data any) (any, error) {
195-
list, ok := data.([]codersdk.License)
196-
if !ok {
197-
return nil, xerrors.Errorf("invalid data type %T", data)
198-
}
199-
for i := range list {
200-
humanExp, err := list[i].ExpiresAt()
201-
if err == nil {
202-
list[i].Claims[codersdk.LicenseExpiryClaim+"_human"] = humanExp.Format(time.RFC3339)
203-
}
204-
}
205-
206-
return list, nil
207-
}),
208-
)
209-
139+
formatter := cliutil.NewLicenseFormatter()
210140
client := new(codersdk.Client)
211141
cmd := &serpent.Command{
212142
Use: "list",

support/support.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type Deployment struct {
4343
Config *codersdk.DeploymentConfig `json:"config"`
4444
Experiments codersdk.Experiments `json:"experiments"`
4545
HealthReport *healthsdk.HealthcheckReport `json:"health_report"`
46+
Licenses []codersdk.License `json:"licenses"`
4647
}
4748

4849
type Network struct {
@@ -138,6 +139,21 @@ func DeploymentInfo(ctx context.Context, client *codersdk.Client, log slog.Logge
138139
return nil
139140
})
140141

142+
eg.Go(func() error {
143+
licenses, err := client.Licenses(ctx)
144+
if err != nil {
145+
// Ignore 404 because AGPL doesn't have this endpoint
146+
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() != http.StatusNotFound {
147+
return xerrors.Errorf("fetch license status: %w", err)
148+
}
149+
}
150+
if licenses == nil {
151+
licenses = make([]codersdk.License, 0)
152+
}
153+
d.Licenses = licenses
154+
return nil
155+
})
156+
141157
if err := eg.Wait(); err != nil {
142158
log.Error(ctx, "fetch deployment information", slog.Error(err))
143159
}

support/support_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func TestRun(t *testing.T) {
6262
assertSanitizedDeploymentConfig(t, bun.Deployment.Config)
6363
assertNotNilNotEmpty(t, bun.Deployment.HealthReport, "deployment health report should be present")
6464
assertNotNilNotEmpty(t, bun.Deployment.Experiments, "deployment experiments should be present")
65+
require.NotNil(t, bun.Deployment.Licenses, "license status should be present")
6566
assertNotNilNotEmpty(t, bun.Network.ConnectionInfo, "agent connection info should be present")
6667
assertNotNilNotEmpty(t, bun.Network.CoordinatorDebug, "network coordinator debug should be present")
6768
assertNotNilNotEmpty(t, bun.Network.Netcheck, "network netcheck should be present")

0 commit comments

Comments
 (0)