Skip to content

Commit def98b7

Browse files
author
Lev Brouk
committed
inspect-build: dashboard CLI for V4/V5 templates
Single render pipeline (HEADER / IMAGE / MAPPINGS / BUILDS / DATA / HEATMAP / METADATA) with a human TTY dashboard and JSON output. HEATMAP shows per-2MiB-chunk compression and cold-fetch fanout, mirroring block.Chunker.locateChunk. JSON is header-dump style: frame tables embedded under builds[], self carries fetches per frame. Flags: -build / -template / -storage / -memfile / -rootfs, -human / -json, -decimal, -expand=mappings,frames,metadata,all, -range=offset:size, -validate (fetch + decompress every frame via the production read path and verify checksum), -recursive (walk the ancestor chain). The pre-redesign header-dump tool is preserved in main.go behind --old; the new dispatcher lives at the top of main() and the new entry point is runInspect() in inspect.go.
1 parent 842b007 commit def98b7

9 files changed

Lines changed: 3000 additions & 97 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"log"
7+
"os"
8+
"slices"
9+
"strconv"
10+
"strings"
11+
12+
"golang.org/x/term"
13+
14+
"github.com/e2b-dev/infra/packages/orchestrator/cmd/internal/cmdutil"
15+
"github.com/e2b-dev/infra/packages/shared/pkg/storage"
16+
)
17+
18+
var expandSections = []string{sectionMappings, sectionFrames, sectionMetadata, sectionAll}
19+
20+
// runInspect is the entry point for the redesigned dashboard CLI. main() in
21+
// main.go dispatches here unless --old selects the legacy header-dump tool.
22+
func runInspect() {
23+
build := flag.String("build", "", "build ID")
24+
template := flag.String("template", "", "template ID or alias (requires E2B_API_KEY)")
25+
storagePath := flag.String("storage", ".local-build", "storage: local path or gs://bucket")
26+
memfile := flag.Bool("memfile", false, "inspect memfile artifact (default)")
27+
rootfs := flag.Bool("rootfs", false, "inspect rootfs artifact")
28+
human := flag.Bool("human", false, "force the human dashboard")
29+
jsonOut := flag.Bool("json", false, "force JSON output")
30+
decimal := flag.Bool("decimal", false, "human mode: exact offsets/sizes in decimal, not hex")
31+
expand := flag.String("expand", "", "sections to show in full: "+strings.Join(expandSections, ","))
32+
rangeArg := flag.String("range", "", "limit expanded mapping/frame lists to offset:size")
33+
validate := flag.Bool("validate", false, "fetch+decompress every frame via the production read path and verify the checksum")
34+
recursive := flag.Bool("recursive", false, "also inspect the full ancestor chain")
35+
flag.Parse()
36+
37+
// Keep the standard log enabled — the tool reports fatal errors via
38+
// log.Fatal; only the zap/OTEL/LaunchDarkly noise is suppressed.
39+
cmdutil.SuppressNoisyLogsKeepStdLog()
40+
41+
buildID, artifact := resolveTarget(*build, *template, *memfile, *rootfs)
42+
ctx := context.Background()
43+
44+
if *validate {
45+
if err := runValidate(ctx, *storagePath, buildID, artifact, *recursive); err != nil {
46+
log.Fatalf("validate: %s", err)
47+
}
48+
49+
return
50+
}
51+
52+
vw := view{
53+
decimal: *decimal,
54+
expand: parseExpand(*expand),
55+
rng: parseRange(*rangeArg),
56+
width: detectTermWidth(os.Stdout),
57+
}
58+
59+
chain, err := gatherChain(ctx, *storagePath, buildID, artifact, *recursive)
60+
if err != nil {
61+
log.Fatalf("%s", err)
62+
}
63+
64+
if *human || (!*jsonOut && isTTY(os.Stdout)) {
65+
renderHuman(os.Stdout, chain, vw)
66+
67+
return
68+
}
69+
if err := renderJSON(os.Stdout, jsonValue(chain, vw, *recursive)); err != nil {
70+
log.Fatal(err)
71+
}
72+
}
73+
74+
// jsonValue builds the JSON payload: a dependency-ordered array of reports for
75+
// --recursive, or the single report otherwise.
76+
func jsonValue(chain []*report, vw view, recursive bool) any {
77+
if !recursive {
78+
return filterReport(chain[0], vw)
79+
}
80+
81+
out := make([]*report, len(chain))
82+
for i, r := range chain {
83+
out[i] = filterReport(r, vw)
84+
}
85+
86+
return out
87+
}
88+
89+
// resolveTarget validates the target flags and returns the build ID to inspect
90+
// and the artifact name (memfile or rootfs).
91+
func resolveTarget(build, template string, memfile, rootfs bool) (buildID, artifact string) {
92+
switch {
93+
case build == "" && template == "":
94+
log.Fatal("specify -build or -template")
95+
case build != "" && template != "":
96+
log.Fatal("specify either -build or -template, not both")
97+
}
98+
99+
buildID = build
100+
if template != "" {
101+
resolved, err := cmdutil.ResolveTemplateID(template)
102+
if err != nil {
103+
log.Fatalf("resolve template: %s", err)
104+
}
105+
buildID = resolved
106+
}
107+
108+
if memfile && rootfs {
109+
log.Fatal("specify either -memfile or -rootfs, not both")
110+
}
111+
artifact = storage.MemfileName
112+
if rootfs {
113+
artifact = storage.RootfsName
114+
}
115+
116+
return buildID, artifact
117+
}
118+
119+
// parseExpand parses the comma-separated --expand list into a section set.
120+
func parseExpand(s string) map[string]bool {
121+
expand := map[string]bool{}
122+
if s == "" {
123+
return expand
124+
}
125+
for name := range strings.SplitSeq(s, ",") {
126+
name = strings.TrimSpace(name)
127+
if !slices.Contains(expandSections, name) {
128+
log.Fatalf("--expand: unknown section %q (valid: %s)", name, strings.Join(expandSections, ", "))
129+
}
130+
expand[name] = true
131+
}
132+
133+
return expand
134+
}
135+
136+
// parseRange parses the --range offset:size value; both numbers accept 0x hex.
137+
func parseRange(s string) span {
138+
if s == "" {
139+
return span{}
140+
}
141+
142+
offStr, sizeStr, ok := strings.Cut(s, ":")
143+
if !ok {
144+
log.Fatalf("--range: expected offset:size, got %q", s)
145+
}
146+
offset, err := strconv.ParseUint(strings.TrimSpace(offStr), 0, 64)
147+
if err != nil {
148+
log.Fatalf("--range: %s", err)
149+
}
150+
size, err := strconv.ParseUint(strings.TrimSpace(sizeStr), 0, 64)
151+
if err != nil {
152+
log.Fatalf("--range: %s", err)
153+
}
154+
155+
return span{set: true, start: offset, end: offset + size}
156+
}
157+
158+
func isTTY(f *os.File) bool {
159+
fi, err := f.Stat()
160+
161+
return err == nil && fi.Mode()&os.ModeCharDevice != 0
162+
}
163+
164+
// detectTermWidth queries the terminal for its column count; falls back to a
165+
// sensible default when stdout is piped or the syscall fails.
166+
func detectTermWidth(f *os.File) int {
167+
if w, _, err := term.GetSize(int(f.Fd())); err == nil && w > 20 {
168+
return w
169+
}
170+
171+
return 100
172+
}

‎packages/orchestrator/cmd/inspect-build/main.go‎

Lines changed: 16 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@ package main
33
import (
44
"bytes"
55
"context"
6-
"encoding/json"
7-
"errors"
86
"flag"
97
"fmt"
10-
"io"
118
"log"
12-
"net/http"
139
"os"
1410
"slices"
15-
"strings"
1611
"unsafe"
1712

1813
"github.com/e2b-dev/infra/packages/orchestrator/cmd/internal/cmdutil"
@@ -22,6 +17,21 @@ import (
2217
const nilUUID = "00000000-0000-0000-0000-000000000000"
2318

2419
func main() {
20+
// Default to the redesigned dashboard CLI (runInspect, see inspect.go);
21+
// --old keeps the original header-dump tool implemented below.
22+
runLegacy := false
23+
for _, name := range []string{"--old", "-old"} {
24+
if i := slices.Index(os.Args, name); i != -1 {
25+
os.Args = slices.Delete(os.Args, i, i+1)
26+
runLegacy = true
27+
}
28+
}
29+
if !runLegacy {
30+
runInspect()
31+
32+
return
33+
}
34+
2535
build := flag.String("build", "", "build ID")
2636
template := flag.String("template", "", "template ID or alias (requires E2B_API_KEY)")
2737
storagePath := flag.String("storage", ".local-build", "storage: local path or gs://bucket")
@@ -38,7 +48,7 @@ func main() {
3848
log.Fatal("specify either -build or -template, not both")
3949
}
4050
if *template != "" {
41-
resolvedBuild, err := resolveTemplateID(*template)
51+
resolvedBuild, err := cmdutil.ResolveTemplateID(*template)
4252
if err != nil {
4353
log.Fatalf("failed to resolve template: %s", err)
4454
}
@@ -224,94 +234,3 @@ func inspectData(ctx context.Context, storagePath, buildID, dataFile string, h *
224234

225235
reader.Close()
226236
}
227-
228-
// templateInfo represents a template from the E2B API.
229-
type templateInfo struct {
230-
TemplateID string `json:"templateID"`
231-
BuildID string `json:"buildID"`
232-
Aliases []string `json:"aliases"`
233-
Names []string `json:"names"`
234-
}
235-
236-
// resolveTemplateID fetches the build ID for a template from the E2B API.
237-
// Input can be a template ID, alias, or full name (e.g., "e2b/base").
238-
func resolveTemplateID(input string) (string, error) {
239-
apiKey := os.Getenv("E2B_API_KEY")
240-
if apiKey == "" {
241-
return "", errors.New("E2B_API_KEY environment variable required for -template flag")
242-
}
243-
244-
// Determine API URL
245-
apiURL := "https://api.e2b.dev/templates"
246-
if domain := os.Getenv("E2B_DOMAIN"); domain != "" {
247-
apiURL = fmt.Sprintf("https://api.%s/templates", domain)
248-
}
249-
250-
// Make HTTP request
251-
ctx := context.Background()
252-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
253-
if err != nil {
254-
return "", fmt.Errorf("failed to create request: %w", err)
255-
}
256-
req.Header.Set("X-API-Key", apiKey)
257-
258-
resp, err := http.DefaultClient.Do(req)
259-
if err != nil {
260-
return "", fmt.Errorf("failed to fetch templates: %w", err)
261-
}
262-
defer resp.Body.Close()
263-
264-
if resp.StatusCode != http.StatusOK {
265-
body, _ := io.ReadAll(resp.Body)
266-
267-
return "", fmt.Errorf("API returned %d: %s", resp.StatusCode, string(body))
268-
}
269-
270-
// Parse response
271-
var templates []templateInfo
272-
if err := json.NewDecoder(resp.Body).Decode(&templates); err != nil {
273-
return "", fmt.Errorf("failed to parse API response: %w", err)
274-
}
275-
276-
// Find matching template
277-
var match *templateInfo
278-
var availableAliases []string
279-
280-
for i := range templates {
281-
t := &templates[i]
282-
283-
// Collect aliases for error message
284-
availableAliases = append(availableAliases, t.Aliases...)
285-
286-
// Match by template ID
287-
if t.TemplateID == input {
288-
match = t
289-
290-
break
291-
}
292-
293-
// Match by alias
294-
if slices.Contains(t.Aliases, input) {
295-
match = t
296-
297-
break
298-
}
299-
300-
// Match by full name (e.g., "e2b/base")
301-
if slices.Contains(t.Names, input) {
302-
match = t
303-
304-
break
305-
}
306-
}
307-
308-
if match == nil {
309-
return "", fmt.Errorf("template %q not found. Available aliases: %s", input, strings.Join(availableAliases, ", "))
310-
}
311-
312-
if match.BuildID == "" || match.BuildID == nilUUID {
313-
return "", fmt.Errorf("template %q has no successful build", input)
314-
}
315-
316-
return match.BuildID, nil
317-
}

0 commit comments

Comments
 (0)