1
0
mirror of https://github.com/glanceapp/glance.git synced 2025-07-09 02:57:36 +02:00

Update docker containers widget

This commit is contained in:
Svilen Markov
2024-12-03 19:04:43 +00:00
parent 0c36925783
commit fcda017c39
13 changed files with 398 additions and 296 deletions

2
.gitignore vendored

@ -1,5 +1,5 @@
/assets /assets
/build /build
/playground /playground
glance*.yml
/.idea /.idea
glance*.yml

@ -1,16 +0,0 @@
FROM golang:1.23.1-alpine3.20 AS builder
WORKDIR /app
COPY . /app
RUN go install github.com/go-delve/delve/cmd/dlv@latest
RUN CGO_ENABLED=0 go build -gcflags="all=-N -l" .
FROM alpine:3.20
WORKDIR /app
COPY --from=builder /app/glance .
COPY --from=builder /go/bin/dlv .
EXPOSE 2345/tcp 8080/tcp
CMD ["./dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "./glance"]

@ -20,9 +20,9 @@
* Twitch channels & top games * Twitch channels & top games
* GitHub releases * GitHub releases
* Repository overview * Repository overview
* Docker containers
* Site monitor * Site monitor
* Search box * Search box
* Docker
#### Themeable #### Themeable
![multiple color schemes example](docs/images/themes-example.png) ![multiple color schemes example](docs/images/themes-example.png)

@ -1795,7 +1795,8 @@ Example:
Note the use of `|` after `source:`, this allows you to insert a multi-line string. Note the use of `|` after `source:`, this allows you to insert a multi-line string.
### Docker ### Docker Containers
<!-- TODO: update -->
The Docker widget allows you to monitor your Docker containers. The Docker widget allows you to monitor your Docker containers.
To enable this feature, ensure that your setup provides access to the **docker.sock** file (also you may use a TCP connection). To enable this feature, ensure that your setup provides access to the **docker.sock** file (also you may use a TCP connection).

@ -1,81 +0,0 @@
package feed
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
)
type DockerContainer struct {
Id string
Image string
Names []string
Status string
State string
Labels map[string]string
}
func FetchDockerContainers(URL string) ([]DockerContainer, error) {
hostURL, err := parseHostURL(URL)
if err != nil {
return nil, err
}
transport := &http.Transport{
MaxIdleConns: 6,
IdleConnTimeout: 30 * time.Second,
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial(hostURL.Scheme, hostURL.Host)
},
}
cli := &http.Client{
Transport: transport,
CheckRedirect: checkRedirect,
}
resp, err := cli.Get("http://docker/containers/json")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var results []DockerContainer
err = json.NewDecoder(resp.Body).Decode(&results)
return results, err
}
func parseHostURL(host string) (*url.URL, error) {
proto, addr, ok := strings.Cut(host, "://")
if !ok || addr == "" {
return nil, fmt.Errorf("unable to parse docker host: %s", host)
}
var basePath string
if proto == "tcp" {
parsed, err := url.Parse(host)
if err != nil {
return nil, err
}
addr = parsed.Host
basePath = parsed.Path
}
return &url.URL{
Scheme: proto,
Host: addr,
Path: basePath,
}, nil
}
func checkRedirect(_ *http.Request, via []*http.Request) error {
if via[0].Method == http.MethodGet {
return http.ErrUseLastResponse
}
return errors.New("unexpected redirect in response")
}

@ -180,22 +180,19 @@ type customIconField struct {
// invert the color based on the theme being light or dark // invert the color based on the theme being light or dark
} }
func (i *customIconField) UnmarshalYAML(node *yaml.Node) error { func newCustomIconField(value string) customIconField {
var value string field := customIconField{}
if err := node.Decode(&value); err != nil {
return err
}
prefix, icon, found := strings.Cut(value, ":") prefix, icon, found := strings.Cut(value, ":")
if !found { if !found {
i.URL = url field.URL = value
return nil return field
} }
switch prefix { switch prefix {
case "si": case "si":
i.URL = "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/" + icon + ".svg" field.URL = "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/" + icon + ".svg"
i.IsFlatIcon = true field.IsFlatIcon = true
case "di": case "di":
// syntax: di:<icon_name>[.svg|.png] // syntax: di:<icon_name>[.svg|.png]
// if the icon name is specified without extension, it is assumed to be wanting the SVG icon // if the icon name is specified without extension, it is assumed to be wanting the SVG icon
@ -211,18 +208,20 @@ func (i *customIconField) UnmarshalYAML(node *yaml.Node) error {
ext = "svg" ext = "svg"
} }
i.URL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/" + ext + "/" + basename + "." + ext field.URL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/" + ext + "/" + basename + "." + ext
default: default:
i.URL = url field.URL = value
} }
return nil return field
} }
func (i *CustomIcon) UnmarshalYAML(node *yaml.Node) error { func (i *customIconField) UnmarshalYAML(node *yaml.Node) error {
var value string var value string
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return err
} }
return i.FromURL(value)
*i = newCustomIconField(value)
return nil
} }

@ -1390,6 +1390,33 @@ details[open] .summary::after {
flex-shrink: 0; flex-shrink: 0;
} }
.docker-container-icon {
display: block;
filter: grayscale(0.4);
object-fit: contain;
aspect-ratio: 1 / 1;
width: 2.7rem;
opacity: 0.8;
transition: filter 0.3s, opacity 0.3s;
}
.docker-container-icon.flat-icon {
opacity: 0.7;
}
.docker-container:hover .docker-container-icon {
opacity: 1;
}
.docker-container:hover .docker-container-icon:not(.flat-icon) {
filter: grayscale(0);
}
.docker-container-status-icon {
width: 2rem;
height: 2rem;
}
.thumbnail { .thumbnail {
filter: grayscale(0.2) contrast(0.9); filter: grayscale(0.2) contrast(0.9);
opacity: 0.8; opacity: 0.8;
@ -1540,37 +1567,6 @@ details[open] .summary::after {
background: linear-gradient(0deg, var(--color-widget-background) 10%, transparent); background: linear-gradient(0deg, var(--color-widget-background) 10%, transparent);
} }
.docker-container-icon {
display: block;
opacity: 0.8;
filter: grayscale(0.4);
object-fit: contain;
aspect-ratio: 1 / 1;
width: 3.2rem;
position: relative;
top: -0.1rem;
transition: filter 0.3s, opacity 0.3s;
}
.docker-container-icon.simple-icon {
opacity: 0.7;
}
.docker-container:hover .docker-container-icon {
opacity: 1;
}
.docker-container:hover .docker-container-icon:not(.simple-icon) {
filter: grayscale(0);
}
.docker-container-status-icon {
flex-shrink: 0;
margin-left: auto;
width: 2rem;
height: 2rem;
}
@media (max-width: 1190px) { @media (max-width: 1190px) {
.header-container { .header-container {
display: none; display: none;
@ -1837,6 +1833,7 @@ details[open] .summary::after {
.gap-35 { gap: 3.5rem; } .gap-35 { gap: 3.5rem; }
.gap-45 { gap: 4.5rem; } .gap-45 { gap: 4.5rem; }
.gap-55 { gap: 5.5rem; } .gap-55 { gap: 5.5rem; }
.margin-left-auto { margin-left: auto; }
.margin-top-3 { margin-top: 0.3rem; } .margin-top-3 { margin-top: 0.3rem; }
.margin-top-5 { margin-top: 0.5rem; } .margin-top-5 { margin-top: 0.5rem; }
.margin-top-7 { margin-top: 0.7rem; } .margin-top-7 { margin-top: 0.7rem; }

@ -0,0 +1,65 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<div class="dynamic-columns list-gap-24 list-with-separator">
{{ range .Containers }}
<div class="docker-container flex items-center gap-15">
<div class="shrink-0" data-popover-type="html" data-popover-position="above" data-popover-offset="0.25" data-popover-margin="0.1rem">
<img class="docker-container-icon{{ if .Icon.IsFlatIcon }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
<div data-popover-html>
<div class="color-highlight text-truncate block">{{ .Image }}</div>
<div>{{ .StateText }}</div>
{{ if .Children }}
<ul class="list list-gap-4 margin-top-10">
{{ range .Children }}
<li class="flex gap-7 items-center">
<div class="margin-bottom-3">{{ template "state-icon" .StateIcon }}</div>
<div class="color-highlight">{{ .Title }} <span class="size-h5 color-base">{{ .StateText }}</span></div>
</li>
{{ end }}
</ul>
{{ end }}
<div class="margin-top-10">created <span {{ .Created | dynamicRelativeTimeAttrs }}></span> ago</div>
</div>
</div>
<div class="min-width-0">
{{ if .URL }}
<a href="{{ .URL }}" class="color-highlight size-title-dynamic block text-truncate" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
{{ else }}
<div class="color-highlight text-truncate size-title-dynamic">{{ .Title }}</div>
{{ end }}
{{ if .Description }}
<div class="text-truncate">{{ .Description }}</div>
{{ end }}
</div>
<div class="margin-left-auto shrink-0" data-popover-type="text" data-popover-position="above" data-popover-text="{{ .State }}">
{{ template "state-icon" .StateIcon }}
</div>
</div>
{{ else }}
<div class="text-center">No containers available to show.</div>
{{ end }}
</div>
{{ end }}
{{ define "state-icon" }}
{{ if eq . "ok" }}
<svg class="docker-container-status-icon" fill="var(--color-positive)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
</svg>
{{ else if eq . "warn" }}
<svg class="docker-container-status-icon" fill="var(--color-negative)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
</svg>
{{ else if eq . "paused" }}
<svg class="docker-container-status-icon" fill="var(--color-text-base)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm5-2.25A.75.75 0 0 1 7.75 7h.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1-.75-.75v-4.5Zm4 0a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1-.75-.75v-4.5Z" clip-rule="evenodd" />
</svg>
{{ else }}
<svg class="docker-container-status-icon" fill="var(--color-text-base)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0ZM8.94 6.94a.75.75 0 1 1-1.061-1.061 3 3 0 1 1 2.871 5.026v.345a.75.75 0 0 1-1.5 0v-.5c0-.72.57-1.172 1.081-1.287A1.5 1.5 0 1 0 8.94 6.94ZM10 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
</svg>
{{ end }}
{{ end }}

@ -1,44 +0,0 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<div class="dynamic-columns list-gap-20 list-with-separator">
{{ range .Containers }}
<div class="docker-container flex items-center gap-15">
{{ template "container" . }}
</div>
{{ end }}
</div>
{{ end }}
{{ define "container" }}
{{ if .Icon.URL }}
<img class="monitor-site-icon{{ if .Icon.IsFlatIcon }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
{{ end }}
<div class="min-width-0">
<a class="size-h3 color-highlight text-truncate block" href="{{ .URL }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
<div class="text-truncate" title="{{ .Image }}">{{ .Image }}</div>
<ul class="size-h6 color-subdue list-horizontal-text">
<li>{{ .StatusShort }}</li>
<li>{{ .StatusFull }}</li>
</ul>
</div>
{{ if eq .StatusStyle "success" }}
<div class="docker-container-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
</svg>
</div>
{{ else if eq .StatusStyle "warning" }}
<div class="docker-container-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</div>
{{ else }}
<div class="docker-container-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</div>
{{ end }}
{{ end }}

@ -166,3 +166,15 @@ func executeTemplateToHTML(t *template.Template, data interface{}) (template.HTM
return template.HTML(b.String()), nil return template.HTML(b.String()), nil
} }
func stringToBool(s string) bool {
return s == "true" || s == "yes"
}
func itemAtIndexOrDefault[T any](items []T, index int, def T) T {
if index >= len(items) {
return def
}
return items[index]
}

@ -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
}

@ -69,7 +69,7 @@ func newWidget(widgetType string) (widget, error) {
w = &splitColumnWidget{} w = &splitColumnWidget{}
case "custom-api": case "custom-api":
w = &customAPIWidget{} w = &customAPIWidget{}
case "docker": case "docker-containers":
w = &dockerContainersWidget{} w = &dockerContainersWidget{}
default: default:
return nil, fmt.Errorf("unknown widget type: %s", widgetType) return nil, fmt.Errorf("unknown widget type: %s", widgetType)

@ -1,106 +0,0 @@
package widget
import (
"context"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"html/template"
"strings"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
const (
defaultDockerHost = "unix:///var/run/docker.sock"
dockerGlanceEnable = "glance.enable"
dockerGlanceTitle = "glance.title"
dockerGlanceUrl = "glance.url"
dockerGlanceIconUrl = "glance.iconUrl"
)
type containerData struct {
Id string
Image string
URL string
Title string
Icon CustomIcon
StatusShort string
StatusFull string
StatusStyle string
}
type Docker struct {
widgetBase `yaml:",inline"`
HostURL string `yaml:"host-url"`
Containers []containerData `yaml:"-"`
}
func (widget *Docker) Initialize() error {
widget.withTitle("Docker").withCacheDuration(1 * time.Minute)
return nil
}
func (widget *Docker) Update(_ context.Context) {
if widget.HostURL == "" {
widget.HostURL = defaultDockerHost
}
containers, err := feed.FetchDockerContainers(widget.HostURL)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
var items []containerData
for _, c := range containers {
isGlanceEnabled := getLabelValue(c.Labels, dockerGlanceEnable, "true")
if isGlanceEnabled != "true" {
continue
}
var item containerData
item.Id = c.Id
item.Image = c.Image
item.Title = getLabelValue(c.Labels, dockerGlanceTitle, strings.Join(c.Names, ""))
item.URL = getLabelValue(c.Labels, dockerGlanceUrl, "")
_ = item.Icon.FromURL(getLabelValue(c.Labels, dockerGlanceIconUrl, "si:docker"))
switch c.State {
case "paused":
case "starting":
case "unhealthy":
item.StatusStyle = "warning"
break
case "stopped":
case "dead":
case "exited":
item.StatusStyle = "error"
break
default:
item.StatusStyle = "success"
}
item.StatusFull = c.Status
item.StatusShort = cases.Title(language.English, cases.Compact).String(c.State)
items = append(items, item)
}
widget.Containers = items
}
func (widget *Docker) Render() template.HTML {
return widget.render(widget, assets.DockerTemplate)
}
// getLabelValue get string value associated to a label.
func getLabelValue(labels map[string]string, labelName, defaultValue string) string {
if value, ok := labels[labelName]; ok && len(value) > 0 {
return value
}
return defaultValue
}