Update docker containers widget
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
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"
|
||||
dockerContainerLabelTitle = "glance.title"
|
||||
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"`
|
||||
Created int64 `json:"Created"`
|
||||
}
|
||||
|
||||
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
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type dockerContainerList []dockerContainer
|
||||
|
||||
func (containers dockerContainerList) sortByStateIconThenTitle() {
|
||||
sort.SliceStable(containers, func(a, b int) bool {
|
||||
p := &dockerContainerStateIconPriorities
|
||||
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")),
|
||||
Created: time.Unix(container.Created, 0),
|
||||
}
|
||||
|
||||
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(dockerContainerLabelTitle, ""); 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: 3 * 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
|
||||
}
|
||||
Reference in New Issue
Block a user