Skip to content

fix(cli): allow generating partial support bundles with no workspace or agent #12933

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions cli/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"text/tabwriter"
"time"

"github.com/google/uuid"
"golang.org/x/xerrors"

"cdr.dev/slog"
Expand Down Expand Up @@ -114,31 +115,40 @@ func (r *RootCmd) supportBundle() *serpent.Command {
client.URL = u
}

if len(inv.Args) == 0 {
return xerrors.Errorf("must specify workspace name")
}
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("invalid workspace: %w", err)
}
cliLog.Debug(inv.Context(), "found workspace",
slog.F("workspace_name", ws.Name),
slog.F("workspace_id", ws.ID),
var (
wsID uuid.UUID
agtID uuid.UUID
)

agentName := ""
if len(inv.Args) > 1 {
agentName = inv.Args[1]
}
if len(inv.Args) == 0 {
cliLog.Warn(inv.Context(), "no workspace specified")
_, _ = fmt.Fprintln(inv.Stderr, "Warning: no workspace specified. This will result in incomplete information.")
} else {
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("invalid workspace: %w", err)
}
cliLog.Debug(inv.Context(), "found workspace",
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]
}

agt, found := findAgent(agentName, ws.LatestBuild.Resources)
if !found {
return xerrors.Errorf("could not find agent named %q for workspace", agentName)
agt, found := findAgent(agentName, ws.LatestBuild.Resources)
if !found {
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
}
}
cliLog.Debug(inv.Context(), "found workspace agent",
slog.F("agent_name", agt.Name),
slog.F("agent_id", agt.ID),
)

if outputPath == "" {
cwd, err := filepath.Abs(".")
Expand All @@ -165,8 +175,8 @@ func (r *RootCmd) supportBundle() *serpent.Command {
Client: client,
// Support adds a sink so we don't need to supply one ourselves.
Log: clientLog,
WorkspaceID: ws.ID,
AgentID: agt.ID,
WorkspaceID: wsID,
AgentID: agtID,
}

bun, err := support.Run(inv.Context(), &deps)
Expand Down
132 changes: 109 additions & 23 deletions cli/support_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,33 +95,50 @@ func TestSupportBundle(t *testing.T) {
clitest.SetupConfig(t, client, root)
err = inv.Run()
require.NoError(t, err)
assertBundleContents(t, path, secretValue)
assertBundleContents(t, path, true, true, []string{secretValue})
})

t.Run("NoWorkspace", func(t *testing.T) {
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)
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
clitest.SetupConfig(t, client, root)
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.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)
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
OrganizationID: admin.OrganizationID,
OwnerID: admin.UserID,
}).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
clitest.SetupConfig(t, client, root)
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) {
Expand All @@ -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()
r, err := zip.OpenReader(path)
require.NoError(t, err, "open zip file")
Expand Down Expand Up @@ -173,64 +191,132 @@ func assertBundleContents(t *testing.T, path string, badValues ...string) {
case "network/netcheck.json":
var v workspacesdk.AgentConnectionInfo
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")
case "workspace/workspace.json":
var v codersdk.Workspace
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")
case "workspace/build_logs.txt":
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")
case "workspace/template.json":
var v codersdk.Template
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")
case "workspace/template_version.json":
var v codersdk.TemplateVersion
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")
case "workspace/parameters.json":
var v []codersdk.WorkspaceBuildParameter
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")
case "workspace/template_file.zip":
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")
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 "workspace/template.json":
var v codersdk.Template
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "workspace template should not be empty")
case "workspace/template_version.json":
var v codersdk.TemplateVersion
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "workspace template version should not be empty")
case "workspace/parameters.json":
var v []codersdk.WorkspaceBuildParameter
decodeJSONFromZip(t, f, &v)
require.NotNil(t, v, "workspace parameters should not be nil")
case "workspace/template_file.zip":
bs := readBytesFromZip(t, f)
require.NotNil(t, bs, "template file should not be nil")
case "logs.txt":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "logs should not be empty")
Expand Down
Loading