134 lines · 4.3 KB · Go
RawDownload
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}