mirror of https://github.com/coder/coder.git
fix(cli): allow generating partial support bundles with no workspace or agent (#12933)
* fix(cli): allow generating partial support bundles with no workspace or agent * nolint control flag
This commit is contained in:
parent
a231b5aef5
commit
fad97a14f9
|
@ -13,6 +13,7 @@ import (
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
|
@ -114,32 +115,41 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
||||||
client.URL = u
|
client.URL = u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
wsID uuid.UUID
|
||||||
|
agtID uuid.UUID
|
||||||
|
)
|
||||||
|
|
||||||
if len(inv.Args) == 0 {
|
if len(inv.Args) == 0 {
|
||||||
return xerrors.Errorf("must specify workspace name")
|
cliLog.Warn(inv.Context(), "no workspace specified")
|
||||||
}
|
_, _ = fmt.Fprintln(inv.Stderr, "Warning: no workspace specified. This will result in incomplete information.")
|
||||||
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
} else {
|
||||||
if err != nil {
|
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
||||||
return xerrors.Errorf("invalid workspace: %w", err)
|
if err != nil {
|
||||||
}
|
return xerrors.Errorf("invalid workspace: %w", err)
|
||||||
cliLog.Debug(inv.Context(), "found workspace",
|
}
|
||||||
slog.F("workspace_name", ws.Name),
|
cliLog.Debug(inv.Context(), "found workspace",
|
||||||
slog.F("workspace_id", ws.ID),
|
slog.F("workspace_name", ws.Name),
|
||||||
)
|
slog.F("workspace_id", ws.ID),
|
||||||
|
)
|
||||||
|
wsID = ws.ID
|
||||||
|
agentName := ""
|
||||||
|
if len(inv.Args) > 1 {
|
||||||
|
agentName = inv.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
agentName := ""
|
agt, found := findAgent(agentName, ws.LatestBuild.Resources)
|
||||||
if len(inv.Args) > 1 {
|
if !found {
|
||||||
agentName = inv.Args[1]
|
cliLog.Warn(inv.Context(), "could not find agent in workspace", slog.F("agent_name", agentName))
|
||||||
|
} else {
|
||||||
|
cliLog.Debug(inv.Context(), "found workspace agent",
|
||||||
|
slog.F("agent_name", agt.Name),
|
||||||
|
slog.F("agent_id", agt.ID),
|
||||||
|
)
|
||||||
|
agtID = agt.ID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agt, found := findAgent(agentName, ws.LatestBuild.Resources)
|
|
||||||
if !found {
|
|
||||||
return xerrors.Errorf("could not find agent named %q for workspace", agentName)
|
|
||||||
}
|
|
||||||
cliLog.Debug(inv.Context(), "found workspace agent",
|
|
||||||
slog.F("agent_name", agt.Name),
|
|
||||||
slog.F("agent_id", agt.ID),
|
|
||||||
)
|
|
||||||
|
|
||||||
if outputPath == "" {
|
if outputPath == "" {
|
||||||
cwd, err := filepath.Abs(".")
|
cwd, err := filepath.Abs(".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -165,8 +175,8 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
||||||
Client: client,
|
Client: client,
|
||||||
// Support adds a sink so we don't need to supply one ourselves.
|
// Support adds a sink so we don't need to supply one ourselves.
|
||||||
Log: clientLog,
|
Log: clientLog,
|
||||||
WorkspaceID: ws.ID,
|
WorkspaceID: wsID,
|
||||||
AgentID: agt.ID,
|
AgentID: agtID,
|
||||||
}
|
}
|
||||||
|
|
||||||
bun, err := support.Run(inv.Context(), &deps)
|
bun, err := support.Run(inv.Context(), &deps)
|
||||||
|
|
|
@ -95,33 +95,50 @@ func TestSupportBundle(t *testing.T) {
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
err = inv.Run()
|
err = inv.Run()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assertBundleContents(t, path, secretValue)
|
assertBundleContents(t, path, true, true, []string{secretValue})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NoWorkspace", func(t *testing.T) {
|
t.Run("NoWorkspace", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
var dc codersdk.DeploymentConfig
|
||||||
|
secretValue := uuid.NewString()
|
||||||
|
seedSecretDeploymentOptions(t, &dc, secretValue)
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: dc.Values,
|
||||||
|
})
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
inv, root := clitest.New(t, "support", "bundle", "--yes")
|
|
||||||
|
d := t.TempDir()
|
||||||
|
path := filepath.Join(d, "bundle.zip")
|
||||||
|
inv, root := clitest.New(t, "support", "bundle", "--output-file", path, "--yes")
|
||||||
//nolint: gocritic // requires owner privilege
|
//nolint: gocritic // requires owner privilege
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
err := inv.Run()
|
err := inv.Run()
|
||||||
require.ErrorContains(t, err, "must specify workspace name")
|
require.NoError(t, err)
|
||||||
|
assertBundleContents(t, path, false, false, []string{secretValue})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NoAgent", func(t *testing.T) {
|
t.Run("NoAgent", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client, db := coderdtest.NewWithDatabase(t, nil)
|
var dc codersdk.DeploymentConfig
|
||||||
|
secretValue := uuid.NewString()
|
||||||
|
seedSecretDeploymentOptions(t, &dc, secretValue)
|
||||||
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: dc.Values,
|
||||||
|
})
|
||||||
admin := coderdtest.CreateFirstUser(t, client)
|
admin := coderdtest.CreateFirstUser(t, client)
|
||||||
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
|
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
|
||||||
OrganizationID: admin.OrganizationID,
|
OrganizationID: admin.OrganizationID,
|
||||||
OwnerID: admin.UserID,
|
OwnerID: admin.UserID,
|
||||||
}).Do() // without agent!
|
}).Do() // without agent!
|
||||||
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--yes")
|
d := t.TempDir()
|
||||||
|
path := filepath.Join(d, "bundle.zip")
|
||||||
|
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--output-file", path, "--yes")
|
||||||
//nolint: gocritic // requires owner privilege
|
//nolint: gocritic // requires owner privilege
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
err := inv.Run()
|
err := inv.Run()
|
||||||
require.ErrorContains(t, err, "could not find agent")
|
require.NoError(t, err)
|
||||||
|
assertBundleContents(t, path, true, false, []string{secretValue})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NoPrivilege", func(t *testing.T) {
|
t.Run("NoPrivilege", func(t *testing.T) {
|
||||||
|
@ -140,7 +157,8 @@ func TestSupportBundle(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertBundleContents(t *testing.T, path string, badValues ...string) {
|
// nolint:revive // It's a control flag, but this is just a test.
|
||||||
|
func assertBundleContents(t *testing.T, path string, wantWorkspace bool, wantAgent bool, badValues []string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
r, err := zip.OpenReader(path)
|
r, err := zip.OpenReader(path)
|
||||||
require.NoError(t, err, "open zip file")
|
require.NoError(t, err, "open zip file")
|
||||||
|
@ -173,64 +191,132 @@ func assertBundleContents(t *testing.T, path string, badValues ...string) {
|
||||||
case "network/netcheck.json":
|
case "network/netcheck.json":
|
||||||
var v workspacesdk.AgentConnectionInfo
|
var v workspacesdk.AgentConnectionInfo
|
||||||
decodeJSONFromZip(t, f, &v)
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantAgent || !wantWorkspace {
|
||||||
|
require.Empty(t, v, "expected connection info to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
require.NotEmpty(t, v, "connection info should not be empty")
|
require.NotEmpty(t, v, "connection info should not be empty")
|
||||||
case "workspace/workspace.json":
|
case "workspace/workspace.json":
|
||||||
var v codersdk.Workspace
|
var v codersdk.Workspace
|
||||||
decodeJSONFromZip(t, f, &v)
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantWorkspace {
|
||||||
|
require.Empty(t, v, "expected workspace to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
require.NotEmpty(t, v, "workspace should not be empty")
|
require.NotEmpty(t, v, "workspace should not be empty")
|
||||||
case "workspace/build_logs.txt":
|
case "workspace/build_logs.txt":
|
||||||
bs := readBytesFromZip(t, f)
|
bs := readBytesFromZip(t, f)
|
||||||
|
if !wantWorkspace || !wantAgent {
|
||||||
|
require.Empty(t, bs, "expected workspace build logs to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
require.Contains(t, string(bs), "provision done")
|
require.Contains(t, string(bs), "provision done")
|
||||||
case "agent/agent.json":
|
|
||||||
var v codersdk.WorkspaceAgent
|
|
||||||
decodeJSONFromZip(t, f, &v)
|
|
||||||
require.NotEmpty(t, v, "agent should not be empty")
|
|
||||||
case "agent/listening_ports.json":
|
|
||||||
var v codersdk.WorkspaceAgentListeningPortsResponse
|
|
||||||
decodeJSONFromZip(t, f, &v)
|
|
||||||
require.NotEmpty(t, v, "agent listening ports should not be empty")
|
|
||||||
case "agent/logs.txt":
|
|
||||||
bs := readBytesFromZip(t, f)
|
|
||||||
require.NotEmpty(t, bs, "logs should not be empty")
|
|
||||||
case "agent/agent_magicsock.html":
|
|
||||||
bs := readBytesFromZip(t, f)
|
|
||||||
require.NotEmpty(t, bs, "agent magicsock should not be empty")
|
|
||||||
case "agent/client_magicsock.html":
|
|
||||||
bs := readBytesFromZip(t, f)
|
|
||||||
require.NotEmpty(t, bs, "client magicsock should not be empty")
|
|
||||||
case "agent/manifest.json":
|
|
||||||
var v agentsdk.Manifest
|
|
||||||
decodeJSONFromZip(t, f, &v)
|
|
||||||
require.NotEmpty(t, v, "agent manifest should not be empty")
|
|
||||||
case "agent/peer_diagnostics.json":
|
|
||||||
var v *tailnet.PeerDiagnostics
|
|
||||||
decodeJSONFromZip(t, f, &v)
|
|
||||||
require.NotEmpty(t, v, "peer diagnostics should not be empty")
|
|
||||||
case "agent/ping_result.json":
|
|
||||||
var v *ipnstate.PingResult
|
|
||||||
decodeJSONFromZip(t, f, &v)
|
|
||||||
require.NotEmpty(t, v, "ping result should not be empty")
|
|
||||||
case "agent/prometheus.txt":
|
|
||||||
bs := readBytesFromZip(t, f)
|
|
||||||
require.NotEmpty(t, bs, "agent prometheus metrics should not be empty")
|
|
||||||
case "agent/startup_logs.txt":
|
|
||||||
bs := readBytesFromZip(t, f)
|
|
||||||
require.Contains(t, string(bs), "started up")
|
|
||||||
case "workspace/template.json":
|
case "workspace/template.json":
|
||||||
var v codersdk.Template
|
var v codersdk.Template
|
||||||
decodeJSONFromZip(t, f, &v)
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantWorkspace {
|
||||||
|
require.Empty(t, v, "expected workspace template to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
require.NotEmpty(t, v, "workspace template should not be empty")
|
require.NotEmpty(t, v, "workspace template should not be empty")
|
||||||
case "workspace/template_version.json":
|
case "workspace/template_version.json":
|
||||||
var v codersdk.TemplateVersion
|
var v codersdk.TemplateVersion
|
||||||
decodeJSONFromZip(t, f, &v)
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantWorkspace {
|
||||||
|
require.Empty(t, v, "expected workspace template version to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
require.NotEmpty(t, v, "workspace template version should not be empty")
|
require.NotEmpty(t, v, "workspace template version should not be empty")
|
||||||
case "workspace/parameters.json":
|
case "workspace/parameters.json":
|
||||||
var v []codersdk.WorkspaceBuildParameter
|
var v []codersdk.WorkspaceBuildParameter
|
||||||
decodeJSONFromZip(t, f, &v)
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantWorkspace {
|
||||||
|
require.Empty(t, v, "expected workspace parameters to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
require.NotNil(t, v, "workspace parameters should not be nil")
|
require.NotNil(t, v, "workspace parameters should not be nil")
|
||||||
case "workspace/template_file.zip":
|
case "workspace/template_file.zip":
|
||||||
bs := readBytesFromZip(t, f)
|
bs := readBytesFromZip(t, f)
|
||||||
|
if !wantWorkspace {
|
||||||
|
require.Empty(t, bs, "expected template file to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
require.NotNil(t, bs, "template file should not be nil")
|
require.NotNil(t, bs, "template file should not be nil")
|
||||||
|
case "agent/agent.json":
|
||||||
|
var v codersdk.WorkspaceAgent
|
||||||
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, v, "expected agent to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, v, "agent should not be empty")
|
||||||
|
case "agent/listening_ports.json":
|
||||||
|
var v codersdk.WorkspaceAgentListeningPortsResponse
|
||||||
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, v, "expected agent listening ports to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, v, "agent listening ports should not be empty")
|
||||||
|
case "agent/logs.txt":
|
||||||
|
bs := readBytesFromZip(t, f)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, bs, "expected agent logs to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, bs, "logs should not be empty")
|
||||||
|
case "agent/agent_magicsock.html":
|
||||||
|
bs := readBytesFromZip(t, f)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, bs, "expected agent magicsock to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, bs, "agent magicsock should not be empty")
|
||||||
|
case "agent/client_magicsock.html":
|
||||||
|
bs := readBytesFromZip(t, f)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, bs, "expected client magicsock to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, bs, "client magicsock should not be empty")
|
||||||
|
case "agent/manifest.json":
|
||||||
|
var v agentsdk.Manifest
|
||||||
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, v, "expected agent manifest to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, v, "agent manifest should not be empty")
|
||||||
|
case "agent/peer_diagnostics.json":
|
||||||
|
var v *tailnet.PeerDiagnostics
|
||||||
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, v, "expected peer diagnostics to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, v, "peer diagnostics should not be empty")
|
||||||
|
case "agent/ping_result.json":
|
||||||
|
var v *ipnstate.PingResult
|
||||||
|
decodeJSONFromZip(t, f, &v)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, v, "expected ping result to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, v, "ping result should not be empty")
|
||||||
|
case "agent/prometheus.txt":
|
||||||
|
bs := readBytesFromZip(t, f)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, bs, "expected agent prometheus metrics to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, bs, "agent prometheus metrics should not be empty")
|
||||||
|
case "agent/startup_logs.txt":
|
||||||
|
bs := readBytesFromZip(t, f)
|
||||||
|
if !wantAgent {
|
||||||
|
require.Empty(t, bs, "expected agent startup logs to be empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.Contains(t, string(bs), "started up")
|
||||||
case "logs.txt":
|
case "logs.txt":
|
||||||
bs := readBytesFromZip(t, f)
|
bs := readBytesFromZip(t, f)
|
||||||
require.NotEmpty(t, bs, "logs should not be empty")
|
require.NotEmpty(t, bs, "logs should not be empty")
|
||||||
|
|
Loading…
Reference in New Issue