From a1c5068b53d00b3574c2282a522c0ec73d88327d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 10 Apr 2024 21:02:31 +0100 Subject: [PATCH 1/2] fix(cli): allow generating partial support bundles with no workspace or agent --- cli/support.go | 56 +++++++++++-------- cli/support_test.go | 131 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 141 insertions(+), 46 deletions(-) diff --git a/cli/support.go b/cli/support.go index 2e87b0147942a..88372278c1269 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,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(".") @@ -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 7f2fce53e4836..09de55e018145 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,7 @@ func TestSupportBundle(t *testing.T) { }) } -func assertBundleContents(t *testing.T, path string, badValues ...string) { +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 +190,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") From 7d24a99a8e0d8ba58a63e8107ee06056a12a1265 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 11 Apr 2024 09:49:44 +0100 Subject: [PATCH 2/2] nolint control flag --- cli/support_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/support_test.go b/cli/support_test.go index 09de55e018145..c40119c474d7c 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -157,6 +157,7 @@ func TestSupportBundle(t *testing.T) { }) } +// 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)