Files
coder/cli/support_test.go
T
Ehab Younes 35ade9e3d2 feat: collect workspace logs in support bundles (#26694)
Add workspace-side file collection to `coder support bundle` via
repeatable --workspace-file flags. The agent resolves the requested
paths or globs inside the remote workspace and streams back a tar with
a manifest and the collected files; nothing is read from the machine
running the command.

- Add POST /api/v0/bundle-files to the agent's agentfiles package.
- Expand env vars in the agent's environment; paths must then be
  absolute or start with ~/ (the agent user's home directory).
- Support ** globs and tail oversized files.
- Record requested patterns, per-path errors, truncation, and the
  applied limits in a manifest.
- Unpack the archive into the bundle under agent/workspace_files/,
  recording dropped entries in collection_errors.txt.
- Write a manifest-only archive marking collection as unsupported for
  agents that predate the endpoint.
- Bound collection: 64 KB request body, 10000 files, 10 MiB per file,
  100 MiB total including archive overhead, 110 MiB client-side read
  cap, 5 minute timeout.

Closes #26020
2026-07-16 13:00:32 +03:00

692 lines
24 KiB
Go

package cli_test
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/ipn/ipnstate"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agentfiles"
"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/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/healthcheck"
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"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")
}
// Support bundle tests can share a single coderdtest instance.
var dc codersdk.DeploymentConfig
dc.Values = coderdtest.DeploymentValues(t)
dc.Values.Prometheus.Enable = true
secretValue := uuid.NewString()
seedSecretDeploymentOptions(t, &dc, secretValue)
// Use a mock healthcheck function to avoid flaky DERP health
// checks in CI. The DERP checker performs real network operations
// (portmapper gateway probing, STUN) that can hang for 60s+ on
// macOS CI runners. Since this test validates support bundle
// generation, not healthcheck correctness, a canned report is
// sufficient.
client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
DeploymentValues: dc.Values,
HealthcheckFunc: func(_ context.Context, _ string, _ *healthcheck.Progress) *healthsdk.HealthcheckReport {
return &healthsdk.HealthcheckReport{
Time: time.Now(),
Healthy: true,
Severity: health.SeverityOK,
}
},
})
t.Cleanup(func() { closer.Close() })
owner := coderdtest.CreateFirstUser(t, client)
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
// Set up test fixtures
setupCtx := testutil.Context(t, testutil.WaitLong)
workspaceWithAgent := setupSupportBundleTestFixture(setupCtx, t, api.Database, owner.OrganizationID, owner.UserID, func(agents []*proto.Agent) []*proto.Agent {
// This should not show up in the bundle output
agents[0].Env["SECRET_VALUE"] = secretValue
return agents
})
workspaceWithRotatedAgentLogs := setupSupportBundleTestFixture(setupCtx, t, api.Database, owner.OrganizationID, owner.UserID, func(agents []*proto.Agent) []*proto.Agent {
// This should not show up in the bundle output
agents[0].Env["SECRET_VALUE"] = secretValue
return agents
})
workspaceWithoutAgent := setupSupportBundleTestFixture(setupCtx, t, api.Database, owner.OrganizationID, owner.UserID, nil)
memberWorkspace := setupSupportBundleTestFixture(setupCtx, t, api.Database, owner.OrganizationID, member.ID, nil)
t.Run("WorkspaceWithAgent", func(t *testing.T) {
t.Parallel()
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, workspaceWithAgent.AgentToken, func(o *agent.Options) {
o.LogDir = tempDir
})
defer agt.Close()
coderdtest.NewWorkspaceAgentWaiter(t, client, workspaceWithAgent.Workspace.ID).Wait()
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
inv, root := clitest.New(t, "support", "bundle", workspaceWithAgent.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("WorkspaceWithRotatedAgentLogs", func(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
logPath := filepath.Join(tempDir, "coder-agent.log")
require.NoError(t, os.WriteFile(logPath, []byte("hello from the agent"), 0o600))
rotatedPath := filepath.Join(tempDir, "coder-agent-2026-05-18T00-00-00.000.log")
require.NoError(t, os.WriteFile(rotatedPath, []byte("rotated log"), 0o600))
oldRotatedPath := filepath.Join(tempDir, "coder-agent-2026-05-17T00-00-00.000.log")
require.NoError(t, os.WriteFile(oldRotatedPath, []byte("old rotated log"), 0o600))
now := time.Now()
require.NoError(t, os.Chtimes(rotatedPath, now, now))
oldRotatedTime := now.Add(-48 * time.Hour)
require.NoError(t, os.Chtimes(oldRotatedPath, oldRotatedTime, oldRotatedTime))
agt := agenttest.New(t, client.URL, workspaceWithRotatedAgentLogs.AgentToken, func(o *agent.Options) {
o.LogDir = tempDir
})
defer agt.Close()
coderdtest.NewWorkspaceAgentWaiter(t, client, workspaceWithRotatedAgentLogs.Workspace.ID).Wait()
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
inv, root := clitest.New(t, "support", "bundle", workspaceWithRotatedAgentLogs.Workspace.Name, "--output-file", path, "--yes")
//nolint: gocritic // requires owner privilege
clitest.SetupConfig(t, client, root)
ctx := testutil.Context(t, testutil.WaitLong)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
r, err := zip.OpenReader(path)
require.NoError(t, err, "open zip file")
defer r.Close()
found := false
for _, f := range r.File {
assertDoesNotContain(t, f, secretValue)
if f.Name != "agent/logs.txt" {
continue
}
found = true
bs := readBytesFromZip(t, f)
body := string(bs)
require.Contains(t, body, "hello from the agent")
require.Contains(t, body, "rotated log")
require.NotContains(t, body, "old rotated log")
}
require.True(t, found, "expected agent/logs.txt in bundle")
})
t.Run("NoWorkspace", func(t *testing.T) {
t.Parallel()
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("InferWorkspaceFromEnvByID", func(t *testing.T) {
t.Parallel()
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
// No workspace arg, but set env vars as if inside a workspace.
inv, root := clitest.New(t, "support", "bundle", "--output-file", path, "--yes")
inv.Environ.Set("CODER", "true")
inv.Environ.Set("CODER_WORKSPACE_ID", workspaceWithoutAgent.Workspace.ID.String())
inv.Environ.Set("CODER_WORKSPACE_AGENT_NAME", "dev")
//nolint: gocritic // requires owner privilege
clitest.SetupConfig(t, client, root)
err := inv.Run()
require.NoError(t, err)
// The workspace should be resolved, but there is no running agent.
assertBundleContents(t, path, true, false, []string{secretValue})
})
t.Run("InferWorkspaceFromEnvByName", func(t *testing.T) {
t.Parallel()
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
// No workspace arg and no CODER_WORKSPACE_ID; fall back to
// owner/name resolution for older agents.
inv, root := clitest.New(t, "support", "bundle", "--output-file", path, "--yes")
inv.Environ.Set("CODER", "true")
inv.Environ.Set("CODER_WORKSPACE_NAME", workspaceWithoutAgent.Workspace.Name)
inv.Environ.Set("CODER_WORKSPACE_OWNER_NAME", coderdtest.FirstUserParams.Username)
inv.Environ.Set("CODER_WORKSPACE_AGENT_NAME", "dev")
//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("NoAgent", func(t *testing.T) {
t.Parallel()
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
inv, root := clitest.New(t, "support", "bundle", workspaceWithoutAgent.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("MemberCanGenerateBundle", func(t *testing.T) {
t.Parallel()
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
inv, root := clitest.New(t, "support", "bundle", memberWorkspace.Workspace.Name, "--output-file", path, "--yes")
clitest.SetupConfig(t, memberClient, root)
err := inv.Run()
require.NoError(t, err)
r, err := zip.OpenReader(path)
require.NoError(t, err, "open zip file")
defer r.Close()
fileNames := make(map[string]struct{}, len(r.File))
for _, f := range r.File {
fileNames[f.Name] = struct{}{}
}
// These should always be present in the zip structure, even if
// the content is null/empty for non-admin users.
for _, name := range []string{
"deployment/buildinfo.json",
"deployment/config.json",
"workspace/workspace.json",
"logs.txt",
"cli_logs.txt",
"network/netcheck.json",
"network/interfaces.json",
} {
require.Contains(t, fileNames, name)
}
})
// This ensures that the CLI does not panic when trying to generate a support bundle
// against a fake server that returns an empty response for all requests. This essentially
// ensures that (almost) all of the support bundle generating code paths get a zero value.
t.Run("DontPanic", func(t *testing.T) {
t.Parallel()
for _, code := range []int{
http.StatusOK,
http.StatusUnauthorized,
http.StatusForbidden,
http.StatusNotFound,
http.StatusInternalServerError,
} {
t.Run(http.StatusText(code), func(t *testing.T) {
t.Parallel()
// Start up a fake server
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Logf("received request: %s %s", r.Method, r.URL)
switch r.URL.Path {
case "/api/v2/users/me":
resp := codersdk.User{}
w.WriteHeader(http.StatusOK)
assert.NoError(t, json.NewEncoder(w).Encode(resp))
case "/api/v2/authcheck":
// Fake auth check
resp := codersdk.AuthorizationResponse{
"Read DeploymentValues": true,
}
w.WriteHeader(http.StatusOK)
assert.NoError(t, json.NewEncoder(w).Encode(resp))
default:
// Simply return a blank response for everything else.
w.WriteHeader(code)
}
}))
defer srv.Close()
u, err := url.Parse(srv.URL)
require.NoError(t, err)
client := codersdk.New(u)
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
inv, root := clitest.New(t, "support", "bundle", "--url-override", srv.URL, "--output-file", path, "--yes")
clitest.SetupConfig(t, client, root)
err = inv.Run()
require.NoError(t, err)
})
}
})
}
func TestSupportBundleCollectsWorkspaceFiles(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("for some reason, windows fails to remove tempdirs sometimes")
}
var dc codersdk.DeploymentConfig
dc.Values = coderdtest.DeploymentValues(t)
dc.Values.Prometheus.Enable = true
secretValue := uuid.NewString()
seedSecretDeploymentOptions(t, &dc, secretValue)
client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
DeploymentValues: dc.Values,
HealthcheckFunc: func(_ context.Context, _ string, _ *healthcheck.Progress) *healthsdk.HealthcheckReport {
return &healthsdk.HealthcheckReport{
Time: time.Now(),
Healthy: true,
Severity: health.SeverityOK,
}
},
})
t.Cleanup(func() { closer.Close() })
owner := coderdtest.CreateFirstUser(t, client)
workspaceWithAgent := setupSupportBundleTestFixture(testutil.Context(t, testutil.WaitLong), t, api.Database, owner.OrganizationID, owner.UserID, func(agents []*proto.Agent) []*proto.Agent {
agents[0].Env["SECRET_VALUE"] = secretValue
return agents
})
// The agent resolves requested paths against $HOME (USERPROFILE on
// Windows). The agent log dir is separate so collection does not race
// live agent logs. The resolved dir matches the agent's canonicalized
// manifest paths (the macOS temp dir is a symlink).
home := testutil.TempDirResolved(t)
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
require.NoError(t, os.MkdirAll(filepath.Join(home, "testlogs", "nested"), 0o700))
require.NoError(t, os.WriteFile(filepath.Join(home, "testlogs", "server.log"), []byte("server log"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(home, "testlogs", "nested", "nested.log"), []byte("nested log"), 0o600))
logDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(logDir, "coder-agent.log"), []byte("hello from the agent"), 0o600))
agt := agenttest.New(t, client.URL, workspaceWithAgent.AgentToken, func(o *agent.Options) {
o.LogDir = logDir
})
defer agt.Close()
coderdtest.NewWorkspaceAgentWaiter(t, client, workspaceWithAgent.Workspace.ID).Wait()
d := t.TempDir()
bundlePath := filepath.Join(d, "bundle.zip")
// The exact path and the glob both match server.log to cover
// deduplication end to end.
inv, root := clitest.New(t,
"support", "bundle", workspaceWithAgent.Workspace.Name,
"--workspace-file", "$HOME/testlogs/server.log",
"--workspace-file", "$HOME/testlogs/**/*.log",
"--output-file", bundlePath,
"--yes",
)
// nolint: gocritic // requires owner privilege
clitest.SetupConfig(t, client, root)
err := inv.WithContext(testutil.Context(t, testutil.WaitLong)).Run()
require.NoError(t, err)
assertBundleContents(t, bundlePath, true, true, []string{secretValue})
entries := readZipEntries(t, bundlePath)
serverLogEntry := "agent/workspace_files/" + agentfiles.BundleFilesArchivePath(filepath.Join(home, "testlogs", "server.log"))
nestedLogEntry := "agent/workspace_files/" + agentfiles.BundleFilesArchivePath(filepath.Join(home, "testlogs", "nested", "nested.log"))
require.Equal(t, "server log", string(entries[serverLogEntry]))
require.Equal(t, "nested log", string(entries[nestedLogEntry]))
var manifest workspacesdk.BundleFilesManifest
require.NoError(t, json.Unmarshal(entries["agent/workspace_files/manifest.json"], &manifest))
require.Equal(t, []string{"$HOME/testlogs/server.log", "$HOME/testlogs/**/*.log"}, manifest.Requested)
require.Len(t, manifest.Files, 2, "server.log should be deduplicated across the exact path and the glob")
}
// 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...)
if strings.HasPrefix(f.Name, "agent/workspace_files/files/") {
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "workspace log file should not be empty")
continue
}
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/entitlements.json":
var v codersdk.Entitlements
decodeJSONFromZip(t, f, &v)
require.NotNil(t, v, "entitlements should not be nil")
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 "deployment/health_settings.json":
var v healthsdk.HealthSettings
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "health settings should not be empty")
case "deployment/stats.json":
var v codersdk.DeploymentStats
decodeJSONFromZip(t, f, &v)
require.NotNil(t, v, "deployment stats should not be nil")
case "deployment/workspaces.json":
var v codersdk.Workspace
decodeJSONFromZip(t, f, &v)
require.NotNil(t, v, "deployment workspaces should not be nil")
case "deployment/prometheus.txt":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "prometheus metrics should not be empty")
require.Contains(t, string(bs), "go_goroutines", "prometheus metrics should contain go runtime metrics")
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 "network/interfaces.json":
var v healthsdk.InterfacesReport
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "interfaces 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 {
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 "agent/workspace_files/manifest.json":
var v workspacesdk.BundleFilesManifest
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v.Requested, "workspace log file manifest should include requested paths")
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")
case "license-status.txt":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "license status 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")
defer rc.Close()
bs, err := io.ReadAll(rc)
require.NoError(t, err, "read bytes from zip")
return bs
}
// readZipEntries reads every entry of the zip at zipPath into memory.
func readZipEntries(t *testing.T, zipPath string) map[string][]byte {
t.Helper()
data, err := os.ReadFile(zipPath)
require.NoError(t, err, "read zip file")
return testutil.ReadZip(t, data)
}
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)
}
}
}
func setupSupportBundleTestFixture(
ctx context.Context,
t testing.TB,
db database.Store,
orgID, ownerID uuid.UUID,
withAgent func([]*proto.Agent) []*proto.Agent,
) dbfake.WorkspaceResponse {
t.Helper()
// nolint: gocritic // Used for seeding test data only.
ctx = dbauthz.AsSystemRestricted(ctx)
b := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: orgID,
OwnerID: ownerID,
})
if withAgent != nil {
b = b.WithAgent(withAgent)
}
r := b.Do()
_, 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)
if withAgent != nil {
res, err := db.GetWorkspaceResourcesByJobID(ctx, r.Build.JobID)
require.NoError(t, err)
var resIDs []uuid.UUID
for _, res := range res {
resIDs = append(resIDs, res.ID)
}
agents, err := db.GetWorkspaceAgentsByResourceIDs(ctx, resIDs)
require.NoError(t, err)
for _, agt := range agents {
_, err = db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
AgentID: agt.ID,
CreatedAt: dbtime.Now(),
Output: []string{"started up"},
Level: []database.LogLevel{database.LogLevelInfo},
LogSourceID: r.Build.JobID,
OutputLength: 10,
})
require.NoError(t, err)
}
}
return r
}