From fad97a14f96668b5dd01a32141f51aacd057c5e9 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 11 Apr 2024 10:09:10 +0100 Subject: [PATCH] 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 --- cli/support.go | 58 ++++++++------- cli/support_test.go | 172 +++++++++++++++++++++++++++++++++----------- 2 files changed, 163 insertions(+), 67 deletions(-) diff --git a/cli/support.go b/cli/support.go index 2e87b01479..88372278c1 100644 --- a/cli/support.go +++ b/cli/support.go @@ -13,6 +13,7 @@ import ( "text/tabwriter" "time" + "github.com/google/uuid" "golang.org/x/xerrors" "cdr.dev/slog" @@ -114,32 +115,41 @@ func (r *RootCmd) supportBundle() *serpent.Command { client.URL = u } + var ( + wsID uuid.UUID + agtID uuid.UUID + ) + 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), - ) + 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] + } - agentName := "" - if len(inv.Args) > 1 { - agentName = inv.Args[1] + 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 + } } - 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 == "" { cwd, err := filepath.Abs(".") if err != nil { @@ -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) diff --git a/cli/support_test.go b/cli/support_test.go index 7f2fce53e4..c40119c474 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -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) { @@ -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") @@ -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 "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": 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 "logs.txt": bs := readBytesFromZip(t, f) require.NotEmpty(t, bs, "logs should not be empty")