mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -122,6 +122,7 @@ type AgentConn interface {
|
||||
ReadFileLines(ctx context.Context, path string, offset, limit int64, limits ReadFileLinesLimits) (ReadFileLinesResponse, error)
|
||||
WriteFile(ctx context.Context, path string, reader io.Reader) error
|
||||
EditFiles(ctx context.Context, edits FileEditRequest) (FileEditResponse, error)
|
||||
BundleFiles(ctx context.Context, req BundleFilesRequest) ([]byte, error)
|
||||
SSH(ctx context.Context) (*gonet.TCPConn, error)
|
||||
SSHClient(ctx context.Context) (*ssh.Client, error)
|
||||
SSHClientOnPort(ctx context.Context, port uint16) (*ssh.Client, error)
|
||||
@@ -462,6 +463,73 @@ func (c *agentConn) DebugManifest(ctx context.Context) ([]byte, error) {
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// BundleFilesRequest configures a workspace-side file collection.
|
||||
type BundleFilesRequest struct {
|
||||
Paths []string `json:"paths"`
|
||||
}
|
||||
|
||||
// BundleFilesManifest is the manifest.json of the archive returned by
|
||||
// BundleFiles.
|
||||
type BundleFilesManifest struct {
|
||||
Requested []string `json:"requested"`
|
||||
Files []BundleFilesManifestEntry `json:"files"`
|
||||
Errors []BundleFilesManifestError `json:"errors"`
|
||||
Truncated bool `json:"truncated"`
|
||||
Limits BundleFilesLimits `json:"limits"`
|
||||
}
|
||||
|
||||
// BundleFilesManifestEntry describes one file collected into the archive.
|
||||
type BundleFilesManifestEntry struct {
|
||||
Requested string `json:"requested"`
|
||||
Path string `json:"path"`
|
||||
ArchivePath string `json:"archive_path"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"mod_time"`
|
||||
BytesWritten int64 `json:"bytes_written"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
// BundleFilesManifestError records a path that could not be collected.
|
||||
type BundleFilesManifestError struct {
|
||||
Requested string `json:"requested"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// BundleFilesLimits are the collection limits the agent applied.
|
||||
type BundleFilesLimits struct {
|
||||
MaxFiles int `json:"max_files"`
|
||||
MaxBytesPerFile int64 `json:"max_bytes_per_file"`
|
||||
MaxTotalBytes int64 `json:"max_total_bytes"`
|
||||
}
|
||||
|
||||
// bundleFilesResponseMaxBytes guards against a misbehaving agent; the
|
||||
// agent itself caps collection at 100 MiB.
|
||||
const bundleFilesResponseMaxBytes int64 = 110 * 1024 * 1024
|
||||
|
||||
// BundleFiles returns a tar archive of explicitly requested workspace
|
||||
// files.
|
||||
func (c *agentConn) BundleFiles(ctx context.Context, req BundleFilesRequest) ([]byte, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
res, err := c.apiRequest(ctx, http.MethodPost, "/api/v0/bundle-files", req)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("do request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, codersdk.ReadBodyAsError(res)
|
||||
}
|
||||
bs, err := io.ReadAll(io.LimitReader(res.Body, bundleFilesResponseMaxBytes+1))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read response body: %w", err)
|
||||
}
|
||||
if int64(len(bs)) > bundleFilesResponseMaxBytes {
|
||||
return nil, xerrors.Errorf("response exceeds %d bytes", bundleFilesResponseMaxBytes)
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// DebugLogsOption configures a DebugLogs request.
|
||||
type DebugLogsOption func(*debugLogsConfig)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user