coder/cli/support_test.go

371 lines
12 KiB
Go

package cli_test
import (
"archive/zip"
"bytes"
"encoding/json"
"io"
"os"
"path/filepath"
"runtime"
"testing"
"time"
"tailscale.com/ipn/ipnstate"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/codersdk/healthsdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/testutil"
)
func TestSupportBundle(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("for some reason, windows fails to remove tempdirs sometimes")
}
t.Run("Workspace", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
var dc codersdk.DeploymentConfig
secretValue := uuid.NewString()
seedSecretDeploymentOptions(t, &dc, secretValue)
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
DeploymentValues: dc.Values,
})
owner := coderdtest.CreateFirstUser(t, client)
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
OrganizationID: owner.OrganizationID,
OwnerID: owner.UserID,
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
// This should not show up in the bundle output
agents[0].Env["SECRET_VALUE"] = secretValue
return agents
}).Do()
ws, err := client.Workspace(ctx, r.Workspace.ID)
require.NoError(t, err)
tempDir := t.TempDir()
logPath := filepath.Join(tempDir, "coder-agent.log")
require.NoError(t, os.WriteFile(logPath, []byte("hello from the agent"), 0o600))
agt := agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) {
o.LogDir = tempDir
})
defer agt.Close()
coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait()
// Insert a provisioner job log
_, err = db.InsertProvisionerJobLogs(ctx, database.InsertProvisionerJobLogsParams{
JobID: r.Build.JobID,
CreatedAt: []time.Time{dbtime.Now()},
Source: []database.LogSource{database.LogSourceProvisionerDaemon},
Level: []database.LogLevel{database.LogLevelInfo},
Stage: []string{"provision"},
Output: []string{"done"},
})
require.NoError(t, err)
// Insert an agent log
_, err = db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
AgentID: ws.LatestBuild.Resources[0].Agents[0].ID,
CreatedAt: dbtime.Now(),
Output: []string{"started up"},
Level: []database.LogLevel{database.LogLevelInfo},
LogSourceID: r.Build.JobID,
OutputLength: 10,
})
require.NoError(t, err)
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.NoError(t, err)
assertBundleContents(t, path, true, true, []string{secretValue})
})
t.Run("NoWorkspace", func(t *testing.T) {
t.Parallel()
var dc codersdk.DeploymentConfig
secretValue := uuid.NewString()
seedSecretDeploymentOptions(t, &dc, secretValue)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: dc.Values,
})
_ = coderdtest.CreateFirstUser(t, client)
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.NoError(t, err)
assertBundleContents(t, path, false, false, []string{secretValue})
})
t.Run("NoAgent", func(t *testing.T) {
t.Parallel()
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!
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.NoError(t, err)
assertBundleContents(t, path, true, false, []string{secretValue})
})
t.Run("NoPrivilege", func(t *testing.T) {
t.Parallel()
client, db := coderdtest.NewWithDatabase(t, nil)
user := coderdtest.CreateFirstUser(t, client)
memberClient, member := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
OrganizationID: user.OrganizationID,
OwnerID: member.ID,
}).WithAgent().Do()
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--yes")
clitest.SetupConfig(t, memberClient, root)
err := inv.Run()
require.ErrorContains(t, err, "failed authorization check")
})
}
// 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")
defer r.Close()
for _, f := range r.File {
assertDoesNotContain(t, f, badValues...)
switch f.Name {
case "deployment/buildinfo.json":
var v codersdk.BuildInfoResponse
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "deployment build info should not be empty")
case "deployment/config.json":
var v codersdk.DeploymentConfig
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "deployment config should not be empty")
case "deployment/experiments.json":
var v codersdk.Experiments
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, f, v, "experiments should not be empty")
case "deployment/health.json":
var v healthsdk.HealthcheckReport
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "health report should not be empty")
case "network/connection_info.json":
var v workspacesdk.AgentConnectionInfo
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "agent connection info should not be empty")
case "network/coordinator_debug.html":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "coordinator debug should not be empty")
case "network/tailnet_debug.html":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "tailnet debug should not be empty")
case "network/netcheck.json":
var v derphealth.Report
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "netcheck 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 "logs.txt":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "logs should not be empty")
case "cli_logs.txt":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "CLI logs should not be empty")
default:
require.Failf(t, "unexpected file in bundle", f.Name)
}
}
}
func decodeJSONFromZip(t *testing.T, f *zip.File, dest any) {
t.Helper()
rc, err := f.Open()
require.NoError(t, err, "open file from zip")
defer rc.Close()
require.NoError(t, json.NewDecoder(rc).Decode(&dest))
}
func readBytesFromZip(t *testing.T, f *zip.File) []byte {
t.Helper()
rc, err := f.Open()
require.NoError(t, err, "open file from zip")
bs, err := io.ReadAll(rc)
require.NoError(t, err, "read bytes from zip")
return bs
}
func assertDoesNotContain(t *testing.T, f *zip.File, vals ...string) {
t.Helper()
bs := readBytesFromZip(t, f)
for _, val := range vals {
if bytes.Contains(bs, []byte(val)) {
t.Fatalf("file %q should not contain value %q", f.Name, val)
}
}
}
func seedSecretDeploymentOptions(t *testing.T, dc *codersdk.DeploymentConfig, secretValue string) {
t.Helper()
if dc == nil {
dc = &codersdk.DeploymentConfig{}
}
for _, opt := range dc.Options {
if codersdk.IsSecretDeploymentOption(opt) {
opt.Value.Set(secretValue)
}
}
}