package cli import ( "context" "fmt" "io" "sync" "time" "github.com/docker/docker/api/types/container" "github.com/davis7dotsh/tx9/internal/box" "github.com/davis7dotsh/tx9/internal/docker") func showOverview(w io.Writer) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return withDockerContext(ctx, func(ctx context.Context, cli *docker.Client) error { boxes, err := box.List(ctx, cli) if err != nil { return fmt.Errorf("list boxes: %w", err) } overview, warnings := collectOverview(ctx, cli, boxes) printDockerWarnings(warnings) renderOverview(w, overview) return nil })} type overviewClient interface { ContainerInspect(context.Context, string) (container.InspectResponse, error) VolumeUsage(context.Context, ...string) (map[string]int64, []string, error)} // Disk usage can take most of the overview's deadline on a large host. Run// it alongside bounded container inspection so it cannot starve CPU/RAM// and dashboard metadata. Each executor is inspected only once.func collectOverview(ctx context.Context, cli overviewClient, boxes []box.Box) ([]overviewBox, []string) { entries := make([]overviewBox, len(boxes)) if len(boxes) == 0 { return entries, nil } volumeNames := make([]string, 0, len(boxes)*2) for _, b := range boxes { a, e := box.VolumeNames(b.Name) volumeNames = append(volumeNames, a, e) } var usage map[string]int64 var volumeWarnings []string var wg sync.WaitGroup wg.Go(func() { var err error usage, volumeWarnings, err = cli.VolumeUsage(ctx, volumeNames...) if err != nil { volumeWarnings = append(volumeWarnings, fmt.Sprintf("volume usage unavailable: %v", err)) } }) warnings := make([][]string, len(boxes)) jobs := make(chan int) for range min(4, len(boxes)) { wg.Go(func() { for i := range jobs { entries[i], warnings[i] = collectOverviewBox(ctx, cli, &boxes[i]) } }) } for i := range boxes { jobs <- i } close(jobs) wg.Wait() var allWarnings []string for i := range entries { a, e := box.VolumeNames(boxes[i].Name) if used, ok := usage[a]; ok { entries[i].AgentVolume.UsedBytes = used } if used, ok := usage[e]; ok { entries[i].ExecutorVolume.UsedBytes = used } allWarnings = append(allWarnings, warnings[i]...) } return entries, append(allWarnings, volumeWarnings...)} func collectOverviewBox(ctx context.Context, cli overviewClient, b *box.Box) (overviewBox, []string) { entry := overviewBox{ Name: b.Name, State: b.DerivedState(), ImageVersion: imageVersionDisplay(b.Version), Agent: overviewContainer{Missing: b.AgentID == ""}, Executor: overviewContainer{Missing: b.ExecutorID == ""}, AgentVolume: overviewVolume{UsedBytes: -1}, ExecutorVolume: overviewVolume{UsedBytes: -1}, } var warnings []string if resources, err := box.LoadResources(b.Name); err == nil { entry.AgentVolume.BudgetBytes = resources.AgentVolumeBudgetBytes entry.ExecutorVolume.BudgetBytes = resources.ExecutorVolumeBudgetBytes entry.AgentVolume.BudgetKnown = true entry.ExecutorVolume.BudgetKnown = true } else { warnings = append(warnings, fmt.Sprintf("%s resource settings unavailable: %v", b.Name, err)) } if _, err := populateOverviewContainer(ctx, cli, b.AgentID, &entry.Agent); err != nil { warnings = append(warnings, fmt.Sprintf("inspect %s agent: %v", b.Name, err)) } info, err := populateOverviewContainer(ctx, cli, b.ExecutorID, &entry.Executor) if err != nil { warnings = append(warnings, fmt.Sprintf("inspect %s executor: %v", b.Name, err)) } if entry.State == "running" && info.NetworkSettings != nil { if bindings := info.NetworkSettings.Ports["4788/tcp"]; len(bindings) > 0 { entry.DashboardURL = box.DashboardURL(bindings[0].HostPort, b.ExecutorWebBaseURL) } } return entry, warnings} func populateOverviewContainer(ctx context.Context, cli overviewClient, id string, target *overviewContainer) (container.InspectResponse, error) { if id == "" { return container.InspectResponse{}, nil } info, err := cli.ContainerInspect(ctx, id) if err != nil { return info, err } if info.ContainerJSONBase == nil || info.HostConfig == nil { return info, fmt.Errorf("Docker returned no host config") } target.CPUs = float64(info.HostConfig.NanoCPUs) / 1_000_000_000 target.MemoryBytes = info.HostConfig.Memory target.Inspected = true return info, nil}
package cli
import (
"context"
"fmt"
"io"
"sync"
"time"
"github.com/docker/docker/api/types/container"
"github.com/davis7dotsh/tx9/internal/box"
"github.com/davis7dotsh/tx9/internal/docker"
)
func showOverview(w io.Writer) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return withDockerContext(ctx, func(ctx context.Context, cli *docker.Client) error {
boxes, err := box.List(ctx, cli)
if err != nil {
return fmt.Errorf("list boxes: %w", err)
}
overview, warnings := collectOverview(ctx, cli, boxes)
printDockerWarnings(warnings)
renderOverview(w, overview)
return nil
})
}
type overviewClient interface {
ContainerInspect(context.Context, string) (container.InspectResponse, error)
VolumeUsage(context.Context, ...string) (map[string]int64, []string, error)
}
// Disk usage can take most of the overview's deadline on a large host. Run
// it alongside bounded container inspection so it cannot starve CPU/RAM
// and dashboard metadata. Each executor is inspected only once.
func collectOverview(ctx context.Context, cli overviewClient, boxes []box.Box) ([]overviewBox, []string) {
entries := make([]overviewBox, len(boxes))
if len(boxes) == 0 {
return entries, nil
}
volumeNames := make([]string, 0, len(boxes)*2)
for _, b := range boxes {
a, e := box.VolumeNames(b.Name)
volumeNames = append(volumeNames, a, e)
}
var usage map[string]int64
var volumeWarnings []string
var wg sync.WaitGroup
wg.Go(func() {
var err error
usage, volumeWarnings, err = cli.VolumeUsage(ctx, volumeNames...)
if err != nil {
volumeWarnings = append(volumeWarnings, fmt.Sprintf("volume usage unavailable: %v", err))
}
})
warnings := make([][]string, len(boxes))
jobs := make(chan int)
for range min(4, len(boxes)) {
wg.Go(func() {
for i := range jobs {
entries[i], warnings[i] = collectOverviewBox(ctx, cli, &boxes[i])
}
})
}
for i := range boxes {
jobs <- i
}
close(jobs)
wg.Wait()
var allWarnings []string
for i := range entries {
a, e := box.VolumeNames(boxes[i].Name)
if used, ok := usage[a]; ok {
entries[i].AgentVolume.UsedBytes = used
}
if used, ok := usage[e]; ok {
entries[i].ExecutorVolume.UsedBytes = used
}
allWarnings = append(allWarnings, warnings[i]...)
}
return entries, append(allWarnings, volumeWarnings...)
}
func collectOverviewBox(ctx context.Context, cli overviewClient, b *box.Box) (overviewBox, []string) {
entry := overviewBox{
Name: b.Name, State: b.DerivedState(), ImageVersion: imageVersionDisplay(b.Version),
Agent: overviewContainer{Missing: b.AgentID == ""}, Executor: overviewContainer{Missing: b.ExecutorID == ""},
AgentVolume: overviewVolume{UsedBytes: -1}, ExecutorVolume: overviewVolume{UsedBytes: -1},
}
var warnings []string
if resources, err := box.LoadResources(b.Name); err == nil {
entry.AgentVolume.BudgetBytes = resources.AgentVolumeBudgetBytes
entry.ExecutorVolume.BudgetBytes = resources.ExecutorVolumeBudgetBytes
entry.AgentVolume.BudgetKnown = true
entry.ExecutorVolume.BudgetKnown = true
} else {
warnings = append(warnings, fmt.Sprintf("%s resource settings unavailable: %v", b.Name, err))
}
if _, err := populateOverviewContainer(ctx, cli, b.AgentID, &entry.Agent); err != nil {
warnings = append(warnings, fmt.Sprintf("inspect %s agent: %v", b.Name, err))
}
info, err := populateOverviewContainer(ctx, cli, b.ExecutorID, &entry.Executor)
if err != nil {
warnings = append(warnings, fmt.Sprintf("inspect %s executor: %v", b.Name, err))
}
if entry.State == "running" && info.NetworkSettings != nil {
if bindings := info.NetworkSettings.Ports["4788/tcp"]; len(bindings) > 0 {
entry.DashboardURL = box.DashboardURL(bindings[0].HostPort, b.ExecutorWebBaseURL)
}
}
return entry, warnings
}
func populateOverviewContainer(ctx context.Context, cli overviewClient, id string, target *overviewContainer) (container.InspectResponse, error) {
if id == "" {
return container.InspectResponse{}, nil
}
info, err := cli.ContainerInspect(ctx, id)
if err != nil {
return info, err
}
if info.ContainerJSONBase == nil || info.HostConfig == nil {
return info, fmt.Errorf("Docker returned no host config")
}
target.CPUs = float64(info.HostConfig.NanoCPUs) / 1_000_000_000
target.MemoryBytes = info.HostConfig.Memory
target.Inspected = true
return info, nil
}