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
This commit is contained in:
Ehab Younes
2026-07-16 13:00:32 +03:00
committed by GitHub
parent f997afa220
commit 35ade9e3d2
19 changed files with 1390 additions and 71 deletions
+39
View File
@@ -2,7 +2,10 @@ package testutil
import (
"archive/tar"
"archive/zip"
"bytes"
"errors"
"io"
"path/filepath"
"testing"
@@ -59,3 +62,39 @@ func CreateZip(t testing.TB, files map[string]string) []byte {
require.NoError(t, err)
return za
}
// Reads every entry of the in-memory tar into a map keyed by entry name.
func ReadTar(t testing.TB, data []byte) map[string][]byte {
t.Helper()
entries := make(map[string][]byte)
tr := tar.NewReader(bytes.NewReader(data))
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
return entries
}
require.NoError(t, err)
content, err := io.ReadAll(tr)
require.NoError(t, err)
entries[hdr.Name] = content
}
}
// Reads every entry of the in-memory zip into a map keyed by entry name.
func ReadZip(t testing.TB, data []byte) map[string][]byte {
t.Helper()
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
require.NoError(t, err)
entries := make(map[string][]byte, len(zr.File))
for _, file := range zr.File {
rc, err := file.Open()
require.NoError(t, err)
content, err := io.ReadAll(rc)
_ = rc.Close()
require.NoError(t, err)
entries[file.Name] = content
}
return entries
}