package glance import ( "context" "encoding/json" "fmt" "html/template" "net" "net/http" "sort" "strings" "time" ) var dockerContainersWidgetTemplate = mustParseTemplate("docker-containers.html", "widget-base.html") type dockerContainersWidget struct { widgetBase `yaml:",inline"` HideByDefault bool `yaml:"hide-by-default"` SockPath string `yaml:"sock-path"` ReadableNames bool `yaml:"readable-names"` Containers dockerContainerList `yaml:"-"` ContainerMap map[string]dockerContainerConfig `yaml:"containers,omitempty"` } func (widget *dockerContainersWidget) initialize() error { widget.withTitle("Docker Containers").withCacheDuration(1 * time.Minute) if widget.SockPath == "" { widget.SockPath = "/var/run/docker.sock" } return nil } func (widget *dockerContainersWidget) update(ctx context.Context) { containers, err := fetchDockerContainers(widget.SockPath, widget.HideByDefault, widget.ReadableNames, widget.ContainerMap) if !widget.canContinueUpdateAfterHandlingErr(err) { return } containers.sortByStateIconThenTitle() widget.Containers = containers } func (widget *dockerContainersWidget) Render() template.HTML { return widget.renderTemplate(widget, dockerContainersWidgetTemplate) } const ( dockerContainerLabelHide = "glance.hide" dockerContainerLabelName = "glance.name" dockerContainerLabelURL = "glance.url" dockerContainerLabelDescription = "glance.description" dockerContainerLabelSameTab = "glance.same-tab" dockerContainerLabelIcon = "glance.icon" dockerContainerLabelID = "glance.id" dockerContainerLabelParent = "glance.parent" ) const ( dockerContainerStateIconOK = "ok" dockerContainerStateIconPaused = "paused" dockerContainerStateIconWarn = "warn" dockerContainerStateIconOther = "other" ) var dockerContainerStateIconPriorities = map[string]int{ dockerContainerStateIconWarn: 0, dockerContainerStateIconOther: 1, dockerContainerStateIconPaused: 2, dockerContainerStateIconOK: 3, } type dockerContainerJsonResponse struct { Names []string `json:"Names"` Image string `json:"Image"` State string `json:"State"` Status string `json:"Status"` Labels dockerContainerLabels `json:"Labels"` } type dockerContainerLabels map[string]string func (l *dockerContainerLabels) getOrDefault(label, def string) string { if l == nil { return def } v, ok := (*l)[label] if !ok { return def } if v == "" { return def } return v } type dockerContainer struct { Title string URL string SameTab bool Image string State string StateText string StateIcon string Description string Icon customIconField Children dockerContainerList } type dockerContainerConfig struct { dockerContainer `yaml:",inline"` Hide bool `yaml:"hide,omitempty"` } type dockerContainerList []dockerContainer func (containers dockerContainerList) sortByStateIconThenTitle() { p := &dockerContainerStateIconPriorities sort.SliceStable(containers, func(a, b int) bool { if containers[a].StateIcon != containers[b].StateIcon { return (*p)[containers[a].StateIcon] < (*p)[containers[b].StateIcon] } return strings.ToLower(containers[a].Title) < strings.ToLower(containers[b].Title) }) } func dockerContainerStateToStateIcon(state string) string { switch state { case "running": return dockerContainerStateIconOK case "paused": return dockerContainerStateIconPaused case "exited", "unhealthy", "dead": return dockerContainerStateIconWarn default: return dockerContainerStateIconOther } } func formatReadableName(name string) string { name = strings.NewReplacer("-", " ", "_", " ").Replace(name) words := strings.Fields(name) for i, word := range words { words[i] = strings.Title(word) } return strings.Join(words, " ") } func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames bool, containerOverrides map[string]dockerContainerConfig) (dockerContainerList, error) { containers, err := fetchAllDockerContainersFromSock(socketPath) if err != nil { return nil, fmt.Errorf("fetching containers: %w", err) } containers, children := groupDockerContainerChildren(containers, hideByDefault) dockerContainers := make(dockerContainerList, 0, len(containers)) for i := range containers { container := &containers[i] containerName := "" if len(container.Names) > 0 { containerName = strings.TrimLeft(container.Names[0], "/") } dc := dockerContainer{ Image: container.Image, State: strings.ToLower(container.State), StateText: strings.ToLower(container.Status), SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")), } if override, exists := containerOverrides[containerName]; exists { if override.Hide { continue } if override.Title != "" { dc.Title = override.Title } else { title := deriveDockerContainerTitle(container) if readableNames { title = formatReadableName(title) } dc.Title = title } dc.URL = override.URL dc.Description = override.Description if override.Icon != (customIconField{}) { dc.Icon = override.Icon } else { dc.Icon = newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker")) } } else { title := deriveDockerContainerTitle(container) if readableNames { title = formatReadableName(title) } dc.Title = title dc.URL = container.Labels.getOrDefault(dockerContainerLabelURL, "") dc.Description = container.Labels.getOrDefault(dockerContainerLabelDescription, "") dc.Icon = newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker")) } if idValue := container.Labels.getOrDefault(dockerContainerLabelID, ""); idValue != "" { if children, ok := children[idValue]; ok { for i := range children { child := &children[i] dc.Children = append(dc.Children, dockerContainer{ Title: deriveDockerContainerTitle(child), StateText: child.Status, StateIcon: dockerContainerStateToStateIcon(strings.ToLower(child.State)), }) } } } dc.Children.sortByStateIconThenTitle() stateIconSupersededByChild := false for i := range dc.Children { if dc.Children[i].StateIcon == dockerContainerStateIconWarn { dc.StateIcon = dockerContainerStateIconWarn stateIconSupersededByChild = true break } } if !stateIconSupersededByChild { dc.StateIcon = dockerContainerStateToStateIcon(dc.State) } dockerContainers = append(dockerContainers, dc) } return dockerContainers, nil } func deriveDockerContainerTitle(container *dockerContainerJsonResponse) string { if v := container.Labels.getOrDefault(dockerContainerLabelName, ""); v != "" { return v } return strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, "n/a"), "/") } func groupDockerContainerChildren( containers []dockerContainerJsonResponse, hideByDefault bool, ) ( []dockerContainerJsonResponse, map[string][]dockerContainerJsonResponse, ) { parents := make([]dockerContainerJsonResponse, 0, len(containers)) children := make(map[string][]dockerContainerJsonResponse) for i := range containers { container := &containers[i] if isDockerContainerHidden(container, hideByDefault) { continue } isParent := container.Labels.getOrDefault(dockerContainerLabelID, "") != "" parent := container.Labels.getOrDefault(dockerContainerLabelParent, "") if !isParent && parent != "" { children[parent] = append(children[parent], *container) } else { parents = append(parents, *container) } } return parents, children } func isDockerContainerHidden(container *dockerContainerJsonResponse, hideByDefault bool) bool { if v := container.Labels.getOrDefault(dockerContainerLabelHide, ""); v != "" { return stringToBool(v) } return hideByDefault } func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonResponse, error) { client := &http.Client{ Timeout: 5 * time.Second, Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", socketPath) }, }, } request, err := http.NewRequest("GET", "http://docker/containers/json?all=true", nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } response, err := client.Do(request) if err != nil { return nil, fmt.Errorf("sending request to socket: %w", err) } defer response.Body.Close() if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("non-200 response status: %s", response.Status) } var containers []dockerContainerJsonResponse if err := json.NewDecoder(response.Body).Decode(&containers); err != nil { return nil, fmt.Errorf("decoding response: %w", err) } return containers, nil } func (widget *dockerContainersWidget) GetContainerNames() ([]string, error) { containers, err := fetchAllDockerContainersFromSock(widget.SockPath) if err != nil { return nil, fmt.Errorf("fetching containers: %w", err) } names := make([]string, 0, len(containers)) for _, container := range containers { if !isDockerContainerHidden(&container, widget.HideByDefault) { // Get the clean container name without the leading '/' name := strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, ""), "/") if name != "" { names = append(names, name) } } } sort.Strings(names) return names, nil }