Files
coder/testutil/archive.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

101 lines
2.4 KiB
Go

package testutil
import (
"archive/tar"
"archive/zip"
"bytes"
"errors"
"io"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/archive"
)
// Creates an in-memory tar of the files provided.
// Files in the archive are written with nobody
// owner/group, and -rw-rw-rw- permissions.
func CreateTar(t testing.TB, files map[string]string) []byte {
var buffer bytes.Buffer
writer := tar.NewWriter(&buffer)
// Keep track of directories previously added.
addedDirs := make(map[string]bool)
for path, content := range files {
// Add parent directories if they don't exist
dir := filepath.Dir(path)
if dir != "." && !addedDirs[dir] {
err := writer.WriteHeader(&tar.Header{
Name: dir + "/", // Directory names must end with /
Mode: 0o755,
Typeflag: tar.TypeDir,
})
require.NoError(t, err)
addedDirs[dir] = true
}
err := writer.WriteHeader(&tar.Header{
Name: path,
Size: int64(len(content)),
Uid: 65534, // nobody
Gid: 65534, // nogroup
Mode: 0o666, // -rw-rw-rw-
})
require.NoError(t, err)
_, err = writer.Write([]byte(content))
require.NoError(t, err)
}
err := writer.Flush()
require.NoError(t, err)
return buffer.Bytes()
}
// Creates an in-memory zip of the files provided.
// Uses archive.CreateZipFromTar under the hood.
func CreateZip(t testing.TB, files map[string]string) []byte {
ta := CreateTar(t, files)
tr := tar.NewReader(bytes.NewReader(ta))
za, err := archive.CreateZipFromTar(tr, int64(len(ta)))
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
}