glance/internal/glance/widget-docker-containers.go
2025-02-17 23:45:57 +00:00

273 lines
7.4 KiB
Go

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"`
Containers dockerContainerList `yaml:"-"`
}
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)
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 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 fetchDockerContainers(socketPath string, hideByDefault bool) (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]
dc := dockerContainer{
Title: deriveDockerContainerTitle(container),
URL: container.Labels.getOrDefault(dockerContainerLabelURL, ""),
Description: container.Labels.getOrDefault(dockerContainerLabelDescription, ""),
SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")),
Image: container.Image,
State: strings.ToLower(container.State),
StateText: strings.ToLower(container.Status),
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
}