Skip to content

Commit bba2c5b

Browse files
committed
Update label overrides implementation
1 parent 51e7034 commit bba2c5b

File tree

3 files changed

+140
-107
lines changed

3 files changed

+140
-107
lines changed

docs/configuration.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,11 +1701,8 @@ Display the status of your Docker containers along with an icon and an optional
17011701
```yaml
17021702
- type: docker-containers
17031703
hide-by-default: false
1704-
readable-names: false
17051704
```
17061705

1707-
The `readable-names` will try to auto format your container names by capitalizing the first letter and converting `-` and `_` characters to spaces.
1708-
17091706
> [!NOTE]
17101707
>
17111708
> The widget requires access to `docker.sock`. If you're running Glance inside a container, this can be done by mounting the socket as a volume:
@@ -1730,18 +1727,16 @@ Configuration of the containers is done via labels applied to each container:
17301727
glance.description: Movies & shows
17311728
```
17321729

1733-
Configuration of the containers can also be overridden using `glance.yml`. Containers are specified by their container names, these will take preference over any docker labels that are set:
1730+
Alternatively, you can also define the values within your `glance.yml` via the `containers` property, where the key is the container name and each value is the same as the labels but without the "glance." prefix:
17341731

17351732
```yaml
17361733
- type: docker-containers
1737-
hide-by-default: false
1738-
readable-names: false
1739-
containers: # Alternative to using docker labels
1740-
container_name_1: # This is the actual container name
1741-
title: "Test Container Name"
1742-
description: "test-description"
1743-
url: "127.0.0.1:3011/test"
1744-
icon: "si:jellyfin"
1734+
containers:
1735+
container_name_1:
1736+
title: Container Name
1737+
description: Description of the container
1738+
url: https://container.domain.com
1739+
icon: si:container-icon
17451740
hide: false
17461741
```
17471742

@@ -1796,11 +1791,15 @@ If any of the child containers are down, their status will propagate up to the p
17961791
| Name | Type | Required | Default |
17971792
| ---- | ---- | -------- | ------- |
17981793
| hide-by-default | boolean | no | false |
1794+
| format-container-names | boolean | no | false |
17991795
| sock-path | string | no | /var/run/docker.sock |
18001796

18011797
##### `hide-by-default`
18021798
Whether to hide the containers by default. If set to `true` you'll have to manually add a `glance.hide: false` label to each container you want to display. By default all containers will be shown and if you want to hide a specific container you can add a `glance.hide: true` label.
18031799

1800+
##### `format-container-names`
1801+
When set to `true`, automatically converts container names such as `container_name_1` into `Container Name 1`.
1802+
18041803
##### `sock-path`
18051804
The path to the Docker socket.
18061805

internal/glance/templates/docker-containers.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
{{- range .Children }}
1515
<li class="flex gap-7 items-center">
1616
<div class="margin-bottom-3">{{ template "state-icon" .StateIcon }}</div>
17-
<div class="color-highlight">{{ .Title }} <span class="size-h5 color-base">{{ .StateText }}</span></div>
17+
<div class="color-highlight">{{ .Name }} <span class="size-h5 color-base">{{ .StateText }}</span></div>
1818
</li>
1919
{{- end }}
2020
</ul>
@@ -24,9 +24,9 @@
2424

2525
<div class="min-width-0">
2626
{{- if .URL }}
27-
<a href="{{ .URL | safeURL }}" class="color-highlight size-title-dynamic block text-truncate" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
27+
<a href="{{ .URL | safeURL }}" class="color-highlight size-title-dynamic block text-truncate" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Name }}</a>
2828
{{- else }}
29-
<div class="color-highlight text-truncate size-title-dynamic">{{ .Title }}</div>
29+
<div class="color-highlight text-truncate size-title-dynamic">{{ .Name }}</div>
3030
{{- end }}
3131
{{- if .Description }}
3232
<div class="text-truncate">{{ .Description }}</div>

internal/glance/widget-docker-containers.go

Lines changed: 126 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"html/template"
88
"net"
99
"net/http"
10+
"net/url"
1011
"sort"
1112
"strings"
1213
"time"
@@ -15,12 +16,14 @@ import (
1516
var dockerContainersWidgetTemplate = mustParseTemplate("docker-containers.html", "widget-base.html")
1617

1718
type dockerContainersWidget struct {
18-
widgetBase `yaml:",inline"`
19-
HideByDefault bool `yaml:"hide-by-default"`
20-
SockPath string `yaml:"sock-path"`
21-
ReadableNames bool `yaml:"readable-names"`
22-
Containers dockerContainerList `yaml:"-"`
23-
ContainerMap map[string]dockerContainerConfig `yaml:"containers,omitempty"`
19+
widgetBase `yaml:",inline"`
20+
HideByDefault bool `yaml:"hide-by-default"`
21+
RunningOnly bool `yaml:"running-only"`
22+
Category string `yaml:"category"`
23+
SockPath string `yaml:"sock-path"`
24+
FormatContainerNames bool `yaml:"format-container-names"`
25+
Containers dockerContainerList `yaml:"-"`
26+
LabelOverrides map[string]map[string]string `yaml:"containers"`
2427
}
2528

2629
func (widget *dockerContainersWidget) initialize() error {
@@ -34,7 +37,14 @@ func (widget *dockerContainersWidget) initialize() error {
3437
}
3538

3639
func (widget *dockerContainersWidget) update(ctx context.Context) {
37-
containers, err := fetchDockerContainers(widget.SockPath, widget.HideByDefault, widget.ReadableNames, widget.ContainerMap)
40+
containers, err := fetchDockerContainers(
41+
widget.SockPath,
42+
widget.HideByDefault,
43+
widget.Category,
44+
widget.RunningOnly,
45+
widget.FormatContainerNames,
46+
widget.LabelOverrides,
47+
)
3848
if !widget.canContinueUpdateAfterHandlingErr(err) {
3949
return
4050
}
@@ -56,6 +66,7 @@ const (
5666
dockerContainerLabelIcon = "glance.icon"
5767
dockerContainerLabelID = "glance.id"
5868
dockerContainerLabelParent = "glance.parent"
69+
dockerContainerLabelCategory = "glance.category"
5970
)
6071

6172
const (
@@ -100,7 +111,7 @@ func (l *dockerContainerLabels) getOrDefault(label, def string) string {
100111
}
101112

102113
type dockerContainer struct {
103-
Title string
114+
Name string
104115
URL string
105116
SameTab bool
106117
Image string
@@ -112,11 +123,6 @@ type dockerContainer struct {
112123
Children dockerContainerList
113124
}
114125

115-
type dockerContainerConfig struct {
116-
dockerContainer `yaml:",inline"`
117-
Hide bool `yaml:"hide,omitempty"`
118-
}
119-
120126
type dockerContainerList []dockerContainer
121127

122128
func (containers dockerContainerList) sortByStateIconThenTitle() {
@@ -127,7 +133,7 @@ func (containers dockerContainerList) sortByStateIconThenTitle() {
127133
return (*p)[containers[a].StateIcon] < (*p)[containers[b].StateIcon]
128134
}
129135

130-
return strings.ToLower(containers[a].Title) < strings.ToLower(containers[b].Title)
136+
return strings.ToLower(containers[a].Name) < strings.ToLower(containers[b].Name)
131137
})
132138
}
133139

@@ -144,17 +150,15 @@ func dockerContainerStateToStateIcon(state string) string {
144150
}
145151
}
146152

147-
func formatReadableName(name string) string {
148-
name = strings.NewReplacer("-", " ", "_", " ").Replace(name)
149-
words := strings.Fields(name)
150-
for i, word := range words {
151-
words[i] = strings.Title(word)
152-
}
153-
return strings.Join(words, " ")
154-
}
155-
156-
func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames bool, containerOverrides map[string]dockerContainerConfig) (dockerContainerList, error) {
157-
containers, err := fetchAllDockerContainersFromSock(socketPath)
153+
func fetchDockerContainers(
154+
socketPath string,
155+
hideByDefault bool,
156+
category string,
157+
runningOnly bool,
158+
formatNames bool,
159+
labelOverrides map[string]map[string]string,
160+
) (dockerContainerList, error) {
161+
containers, err := fetchDockerContainersFromSource(socketPath, category, runningOnly, labelOverrides)
158162
if err != nil {
159163
return nil, fmt.Errorf("fetching containers: %w", err)
160164
}
@@ -165,56 +169,23 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames
165169
for i := range containers {
166170
container := &containers[i]
167171

168-
containerName := ""
169-
if len(container.Names) > 0 {
170-
containerName = strings.TrimLeft(container.Names[0], "/")
171-
}
172-
173172
dc := dockerContainer{
174-
Image: container.Image,
175-
State: strings.ToLower(container.State),
176-
StateText: strings.ToLower(container.Status),
177-
SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")),
178-
}
179-
180-
if override, exists := containerOverrides[containerName]; exists {
181-
if override.Hide {
182-
continue
183-
}
184-
185-
if override.Title != "" {
186-
dc.Title = override.Title
187-
} else {
188-
title := deriveDockerContainerTitle(container)
189-
if readableNames {
190-
title = formatReadableName(title)
191-
}
192-
dc.Title = title
193-
}
194-
dc.URL = override.URL
195-
dc.Description = override.Description
196-
if override.Icon != (customIconField{}) {
197-
dc.Icon = override.Icon
198-
} else {
199-
dc.Icon = newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker"))
200-
}
201-
} else {
202-
title := deriveDockerContainerTitle(container)
203-
if readableNames {
204-
title = formatReadableName(title)
205-
}
206-
dc.Title = title
207-
dc.URL = container.Labels.getOrDefault(dockerContainerLabelURL, "")
208-
dc.Description = container.Labels.getOrDefault(dockerContainerLabelDescription, "")
209-
dc.Icon = newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker"))
173+
Name: deriveDockerContainerName(container, formatNames),
174+
URL: container.Labels.getOrDefault(dockerContainerLabelURL, ""),
175+
Description: container.Labels.getOrDefault(dockerContainerLabelDescription, ""),
176+
SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")),
177+
Image: container.Image,
178+
State: strings.ToLower(container.State),
179+
StateText: strings.ToLower(container.Status),
180+
Icon: newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker")),
210181
}
211182

212183
if idValue := container.Labels.getOrDefault(dockerContainerLabelID, ""); idValue != "" {
213184
if children, ok := children[idValue]; ok {
214185
for i := range children {
215186
child := &children[i]
216187
dc.Children = append(dc.Children, dockerContainer{
217-
Title: deriveDockerContainerTitle(child),
188+
Name: deriveDockerContainerName(child, formatNames),
218189
StateText: child.Status,
219190
StateIcon: dockerContainerStateToStateIcon(strings.ToLower(child.State)),
220191
})
@@ -242,12 +213,31 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames
242213
return dockerContainers, nil
243214
}
244215

245-
func deriveDockerContainerTitle(container *dockerContainerJsonResponse) string {
216+
func deriveDockerContainerName(container *dockerContainerJsonResponse, formatNames bool) string {
246217
if v := container.Labels.getOrDefault(dockerContainerLabelName, ""); v != "" {
247218
return v
248219
}
249220

250-
return strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, "n/a"), "/")
221+
if len(container.Names) == 0 || container.Names[0] == "" {
222+
return "n/a"
223+
}
224+
225+
name := strings.TrimLeft(container.Names[0], "/")
226+
227+
if formatNames {
228+
name = strings.ReplaceAll(name, "_", " ")
229+
name = strings.ReplaceAll(name, "-", " ")
230+
231+
words := strings.Split(name, " ")
232+
for i := range words {
233+
if len(words[i]) > 0 {
234+
words[i] = strings.ToUpper(words[i][:1]) + words[i][1:]
235+
}
236+
}
237+
name = strings.Join(words, " ")
238+
}
239+
240+
return name
251241
}
252242

253243
func groupDockerContainerChildren(
@@ -288,17 +278,44 @@ func isDockerContainerHidden(container *dockerContainerJsonResponse, hideByDefau
288278
return hideByDefault
289279
}
290280

291-
func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonResponse, error) {
292-
client := &http.Client{
293-
Timeout: 5 * time.Second,
294-
Transport: &http.Transport{
295-
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
296-
return net.Dial("unix", socketPath)
281+
func fetchDockerContainersFromSource(
282+
source string,
283+
category string,
284+
runningOnly bool,
285+
labelOverrides map[string]map[string]string,
286+
) ([]dockerContainerJsonResponse, error) {
287+
var hostname string
288+
289+
var client *http.Client
290+
if strings.HasPrefix(source, "tcp://") || strings.HasPrefix(source, "http://") {
291+
client = &http.Client{}
292+
parsed, err := url.Parse(source)
293+
if err != nil {
294+
return nil, fmt.Errorf("parsing URL: %w", err)
295+
}
296+
297+
port := parsed.Port()
298+
if port == "" {
299+
port = "80"
300+
}
301+
302+
hostname = parsed.Hostname() + ":" + port
303+
} else {
304+
hostname = "docker"
305+
client = &http.Client{
306+
Transport: &http.Transport{
307+
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
308+
return net.Dial("unix", source)
309+
},
297310
},
298-
},
311+
}
299312
}
300313

301-
request, err := http.NewRequest("GET", "http://docker/containers/json?all=true", nil)
314+
fetchAll := ternary(runningOnly, "false", "true")
315+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
316+
defer cancel()
317+
318+
request, err := http.NewRequestWithContext(ctx, "GET", "http://"+hostname+"/containers/json?all="+fetchAll, nil)
302319
if err != nil {
303320
return nil, fmt.Errorf("creating request: %w", err)
304321
}
@@ -318,26 +335,43 @@ func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonR
318335
return nil, fmt.Errorf("decoding response: %w", err)
319336
}
320337

321-
return containers, nil
322-
}
338+
for i := range containers {
339+
container := &containers[i]
340+
name := strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, ""), "/")
323341

324-
func (widget *dockerContainersWidget) GetContainerNames() ([]string, error) {
325-
containers, err := fetchAllDockerContainersFromSock(widget.SockPath)
326-
if err != nil {
327-
return nil, fmt.Errorf("fetching containers: %w", err)
342+
if name == "" {
343+
continue
344+
}
345+
346+
overrides, ok := labelOverrides[name]
347+
if !ok {
348+
continue
349+
}
350+
351+
if container.Labels == nil {
352+
container.Labels = make(dockerContainerLabels)
353+
}
354+
355+
for label, value := range overrides {
356+
container.Labels["glance."+label] = value
357+
}
328358
}
329359

330-
names := make([]string, 0, len(containers))
331-
for _, container := range containers {
332-
if !isDockerContainerHidden(&container, widget.HideByDefault) {
333-
// Get the clean container name without the leading '/'
334-
name := strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, ""), "/")
335-
if name != "" {
336-
names = append(names, name)
360+
// We have to filter here instead of using the `filters` parameter of Docker's API
361+
// because the user may define a category override within their config
362+
if category != "" {
363+
filtered := make([]dockerContainerJsonResponse, 0, len(containers))
364+
365+
for i := range containers {
366+
container := &containers[i]
367+
368+
if container.Labels.getOrDefault(dockerContainerLabelCategory, "") == category {
369+
filtered = append(filtered, *container)
337370
}
338371
}
372+
373+
containers = filtered
339374
}
340375

341-
sort.Strings(names)
342-
return names, nil
376+
return containers, nil
343377
}

0 commit comments

Comments
 (0)