mirror of
https://github.com/Xevion/glance.git
synced 2026-01-31 10:24:27 -06:00
380 lines
9.9 KiB
Go
380 lines
9.9 KiB
Go
package glance
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var dockerContainersWidgetTemplate = mustParseTemplate("docker-containers.html", "widget-base.html")
|
|
|
|
type dockerContainersWidget struct {
|
|
widgetBase `yaml:",inline"`
|
|
HideByDefault bool `yaml:"hide-by-default"`
|
|
RunningOnly bool `yaml:"running-only"`
|
|
Category string `yaml:"category"`
|
|
SockPath string `yaml:"sock-path"`
|
|
FormatContainerNames bool `yaml:"format-container-names"`
|
|
Containers dockerContainerList `yaml:"-"`
|
|
LabelOverrides map[string]map[string]string `yaml:"containers"`
|
|
}
|
|
|
|
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.Category,
|
|
widget.RunningOnly,
|
|
widget.FormatContainerNames,
|
|
widget.LabelOverrides,
|
|
)
|
|
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"
|
|
dockerContainerLabelCategory = "glance.category"
|
|
)
|
|
|
|
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 {
|
|
Name 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].Name) < strings.ToLower(containers[b].Name)
|
|
})
|
|
}
|
|
|
|
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,
|
|
category string,
|
|
runningOnly bool,
|
|
formatNames bool,
|
|
labelOverrides map[string]map[string]string,
|
|
) (dockerContainerList, error) {
|
|
containers, err := fetchDockerContainersFromSource(socketPath, category, runningOnly, labelOverrides)
|
|
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{
|
|
Name: deriveDockerContainerName(container, formatNames),
|
|
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{
|
|
Name: deriveDockerContainerName(child, formatNames),
|
|
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 deriveDockerContainerName(container *dockerContainerJsonResponse, formatNames bool) string {
|
|
if v := container.Labels.getOrDefault(dockerContainerLabelName, ""); v != "" {
|
|
return v
|
|
}
|
|
|
|
if len(container.Names) == 0 || container.Names[0] == "" {
|
|
return "n/a"
|
|
}
|
|
|
|
name := strings.TrimLeft(container.Names[0], "/")
|
|
|
|
if formatNames {
|
|
name = strings.ReplaceAll(name, "_", " ")
|
|
name = strings.ReplaceAll(name, "-", " ")
|
|
|
|
words := strings.Split(name, " ")
|
|
for i := range words {
|
|
if len(words[i]) > 0 {
|
|
words[i] = strings.ToUpper(words[i][:1]) + words[i][1:]
|
|
}
|
|
}
|
|
name = strings.Join(words, " ")
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
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 fetchDockerContainersFromSource(
|
|
source string,
|
|
category string,
|
|
runningOnly bool,
|
|
labelOverrides map[string]map[string]string,
|
|
) ([]dockerContainerJsonResponse, error) {
|
|
var hostname string
|
|
|
|
var client *http.Client
|
|
if strings.HasPrefix(source, "tcp://") || strings.HasPrefix(source, "http://") {
|
|
client = &http.Client{}
|
|
parsed, err := url.Parse(source)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing URL: %w", err)
|
|
}
|
|
|
|
port := parsed.Port()
|
|
if port == "" {
|
|
port = "80"
|
|
}
|
|
|
|
hostname = parsed.Hostname() + ":" + port
|
|
} else {
|
|
hostname = "docker"
|
|
client = &http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
|
return net.Dial("unix", source)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
|
|
fetchAll := ternary(runningOnly, "false", "true")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
request, err := http.NewRequestWithContext(ctx, "GET", "http://"+hostname+"/containers/json?all="+fetchAll, 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)
|
|
}
|
|
|
|
for i := range containers {
|
|
container := &containers[i]
|
|
name := strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, ""), "/")
|
|
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
overrides, ok := labelOverrides[name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if container.Labels == nil {
|
|
container.Labels = make(dockerContainerLabels)
|
|
}
|
|
|
|
for label, value := range overrides {
|
|
container.Labels["glance."+label] = value
|
|
}
|
|
}
|
|
|
|
// We have to filter here instead of using the `filters` parameter of Docker's API
|
|
// because the user may define a category override within their config
|
|
if category != "" {
|
|
filtered := make([]dockerContainerJsonResponse, 0, len(containers))
|
|
|
|
for i := range containers {
|
|
container := &containers[i]
|
|
|
|
if container.Labels.getOrDefault(dockerContainerLabelCategory, "") == category {
|
|
filtered = append(filtered, *container)
|
|
}
|
|
}
|
|
|
|
containers = filtered
|
|
}
|
|
|
|
return containers, nil
|
|
}
|