mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-21 18:31:24 +02:00
Update label overrides implementation
This commit is contained in:
parent
51e70347e4
commit
bba2c5b20c
@ -1701,11 +1701,8 @@ Display the status of your Docker containers along with an icon and an optional
|
|||||||
```yaml
|
```yaml
|
||||||
- type: docker-containers
|
- type: docker-containers
|
||||||
hide-by-default: false
|
hide-by-default: false
|
||||||
readable-names: false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `readable-names` will try to auto format your container names by capitalizing the first letter and converting `-` and `_` characters to spaces.
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
>
|
>
|
||||||
> 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:
|
> 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:
|
|||||||
glance.description: Movies & shows
|
glance.description: Movies & shows
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- type: docker-containers
|
- type: docker-containers
|
||||||
hide-by-default: false
|
containers:
|
||||||
readable-names: false
|
container_name_1:
|
||||||
containers: # Alternative to using docker labels
|
title: Container Name
|
||||||
container_name_1: # This is the actual container name
|
description: Description of the container
|
||||||
title: "Test Container Name"
|
url: https://container.domain.com
|
||||||
description: "test-description"
|
icon: si:container-icon
|
||||||
url: "127.0.0.1:3011/test"
|
|
||||||
icon: "si:jellyfin"
|
|
||||||
hide: false
|
hide: false
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1796,11 +1791,15 @@ If any of the child containers are down, their status will propagate up to the p
|
|||||||
| Name | Type | Required | Default |
|
| Name | Type | Required | Default |
|
||||||
| ---- | ---- | -------- | ------- |
|
| ---- | ---- | -------- | ------- |
|
||||||
| hide-by-default | boolean | no | false |
|
| hide-by-default | boolean | no | false |
|
||||||
|
| format-container-names | boolean | no | false |
|
||||||
| sock-path | string | no | /var/run/docker.sock |
|
| sock-path | string | no | /var/run/docker.sock |
|
||||||
|
|
||||||
##### `hide-by-default`
|
##### `hide-by-default`
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
##### `format-container-names`
|
||||||
|
When set to `true`, automatically converts container names such as `container_name_1` into `Container Name 1`.
|
||||||
|
|
||||||
##### `sock-path`
|
##### `sock-path`
|
||||||
The path to the Docker socket.
|
The path to the Docker socket.
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
{{- range .Children }}
|
{{- range .Children }}
|
||||||
<li class="flex gap-7 items-center">
|
<li class="flex gap-7 items-center">
|
||||||
<div class="margin-bottom-3">{{ template "state-icon" .StateIcon }}</div>
|
<div class="margin-bottom-3">{{ template "state-icon" .StateIcon }}</div>
|
||||||
<div class="color-highlight">{{ .Title }} <span class="size-h5 color-base">{{ .StateText }}</span></div>
|
<div class="color-highlight">{{ .Name }} <span class="size-h5 color-base">{{ .StateText }}</span></div>
|
||||||
</li>
|
</li>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</ul>
|
</ul>
|
||||||
@ -24,9 +24,9 @@
|
|||||||
|
|
||||||
<div class="min-width-0">
|
<div class="min-width-0">
|
||||||
{{- if .URL }}
|
{{- if .URL }}
|
||||||
<a href="{{ .URL | safeURL }}" class="color-highlight size-title-dynamic block text-truncate" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
<a href="{{ .URL | safeURL }}" class="color-highlight size-title-dynamic block text-truncate" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Name }}</a>
|
||||||
{{- else }}
|
{{- else }}
|
||||||
<div class="color-highlight text-truncate size-title-dynamic">{{ .Title }}</div>
|
<div class="color-highlight text-truncate size-title-dynamic">{{ .Name }}</div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Description }}
|
{{- if .Description }}
|
||||||
<div class="text-truncate">{{ .Description }}</div>
|
<div class="text-truncate">{{ .Description }}</div>
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -15,12 +16,14 @@ import (
|
|||||||
var dockerContainersWidgetTemplate = mustParseTemplate("docker-containers.html", "widget-base.html")
|
var dockerContainersWidgetTemplate = mustParseTemplate("docker-containers.html", "widget-base.html")
|
||||||
|
|
||||||
type dockerContainersWidget struct {
|
type dockerContainersWidget struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
HideByDefault bool `yaml:"hide-by-default"`
|
HideByDefault bool `yaml:"hide-by-default"`
|
||||||
SockPath string `yaml:"sock-path"`
|
RunningOnly bool `yaml:"running-only"`
|
||||||
ReadableNames bool `yaml:"readable-names"`
|
Category string `yaml:"category"`
|
||||||
Containers dockerContainerList `yaml:"-"`
|
SockPath string `yaml:"sock-path"`
|
||||||
ContainerMap map[string]dockerContainerConfig `yaml:"containers,omitempty"`
|
FormatContainerNames bool `yaml:"format-container-names"`
|
||||||
|
Containers dockerContainerList `yaml:"-"`
|
||||||
|
LabelOverrides map[string]map[string]string `yaml:"containers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *dockerContainersWidget) initialize() error {
|
func (widget *dockerContainersWidget) initialize() error {
|
||||||
@ -34,7 +37,14 @@ func (widget *dockerContainersWidget) initialize() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (widget *dockerContainersWidget) update(ctx context.Context) {
|
func (widget *dockerContainersWidget) update(ctx context.Context) {
|
||||||
containers, err := fetchDockerContainers(widget.SockPath, widget.HideByDefault, widget.ReadableNames, widget.ContainerMap)
|
containers, err := fetchDockerContainers(
|
||||||
|
widget.SockPath,
|
||||||
|
widget.HideByDefault,
|
||||||
|
widget.Category,
|
||||||
|
widget.RunningOnly,
|
||||||
|
widget.FormatContainerNames,
|
||||||
|
widget.LabelOverrides,
|
||||||
|
)
|
||||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -56,6 +66,7 @@ const (
|
|||||||
dockerContainerLabelIcon = "glance.icon"
|
dockerContainerLabelIcon = "glance.icon"
|
||||||
dockerContainerLabelID = "glance.id"
|
dockerContainerLabelID = "glance.id"
|
||||||
dockerContainerLabelParent = "glance.parent"
|
dockerContainerLabelParent = "glance.parent"
|
||||||
|
dockerContainerLabelCategory = "glance.category"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -100,7 +111,7 @@ func (l *dockerContainerLabels) getOrDefault(label, def string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type dockerContainer struct {
|
type dockerContainer struct {
|
||||||
Title string
|
Name string
|
||||||
URL string
|
URL string
|
||||||
SameTab bool
|
SameTab bool
|
||||||
Image string
|
Image string
|
||||||
@ -112,11 +123,6 @@ type dockerContainer struct {
|
|||||||
Children dockerContainerList
|
Children dockerContainerList
|
||||||
}
|
}
|
||||||
|
|
||||||
type dockerContainerConfig struct {
|
|
||||||
dockerContainer `yaml:",inline"`
|
|
||||||
Hide bool `yaml:"hide,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerContainerList []dockerContainer
|
type dockerContainerList []dockerContainer
|
||||||
|
|
||||||
func (containers dockerContainerList) sortByStateIconThenTitle() {
|
func (containers dockerContainerList) sortByStateIconThenTitle() {
|
||||||
@ -127,7 +133,7 @@ func (containers dockerContainerList) sortByStateIconThenTitle() {
|
|||||||
return (*p)[containers[a].StateIcon] < (*p)[containers[b].StateIcon]
|
return (*p)[containers[a].StateIcon] < (*p)[containers[b].StateIcon]
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.ToLower(containers[a].Title) < strings.ToLower(containers[b].Title)
|
return strings.ToLower(containers[a].Name) < strings.ToLower(containers[b].Name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,17 +150,15 @@ func dockerContainerStateToStateIcon(state string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatReadableName(name string) string {
|
func fetchDockerContainers(
|
||||||
name = strings.NewReplacer("-", " ", "_", " ").Replace(name)
|
socketPath string,
|
||||||
words := strings.Fields(name)
|
hideByDefault bool,
|
||||||
for i, word := range words {
|
category string,
|
||||||
words[i] = strings.Title(word)
|
runningOnly bool,
|
||||||
}
|
formatNames bool,
|
||||||
return strings.Join(words, " ")
|
labelOverrides map[string]map[string]string,
|
||||||
}
|
) (dockerContainerList, error) {
|
||||||
|
containers, err := fetchDockerContainersFromSource(socketPath, category, runningOnly, labelOverrides)
|
||||||
func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames bool, containerOverrides map[string]dockerContainerConfig) (dockerContainerList, error) {
|
|
||||||
containers, err := fetchAllDockerContainersFromSock(socketPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetching containers: %w", err)
|
return nil, fmt.Errorf("fetching containers: %w", err)
|
||||||
}
|
}
|
||||||
@ -165,48 +169,15 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames
|
|||||||
for i := range containers {
|
for i := range containers {
|
||||||
container := &containers[i]
|
container := &containers[i]
|
||||||
|
|
||||||
containerName := ""
|
|
||||||
if len(container.Names) > 0 {
|
|
||||||
containerName = strings.TrimLeft(container.Names[0], "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
dc := dockerContainer{
|
dc := dockerContainer{
|
||||||
Image: container.Image,
|
Name: deriveDockerContainerName(container, formatNames),
|
||||||
State: strings.ToLower(container.State),
|
URL: container.Labels.getOrDefault(dockerContainerLabelURL, ""),
|
||||||
StateText: strings.ToLower(container.Status),
|
Description: container.Labels.getOrDefault(dockerContainerLabelDescription, ""),
|
||||||
SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")),
|
SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")),
|
||||||
}
|
Image: container.Image,
|
||||||
|
State: strings.ToLower(container.State),
|
||||||
if override, exists := containerOverrides[containerName]; exists {
|
StateText: strings.ToLower(container.Status),
|
||||||
if override.Hide {
|
Icon: newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker")),
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if override.Title != "" {
|
|
||||||
dc.Title = override.Title
|
|
||||||
} else {
|
|
||||||
title := deriveDockerContainerTitle(container)
|
|
||||||
if readableNames {
|
|
||||||
title = formatReadableName(title)
|
|
||||||
}
|
|
||||||
dc.Title = title
|
|
||||||
}
|
|
||||||
dc.URL = override.URL
|
|
||||||
dc.Description = override.Description
|
|
||||||
if override.Icon != (customIconField{}) {
|
|
||||||
dc.Icon = override.Icon
|
|
||||||
} else {
|
|
||||||
dc.Icon = newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
title := deriveDockerContainerTitle(container)
|
|
||||||
if readableNames {
|
|
||||||
title = formatReadableName(title)
|
|
||||||
}
|
|
||||||
dc.Title = title
|
|
||||||
dc.URL = container.Labels.getOrDefault(dockerContainerLabelURL, "")
|
|
||||||
dc.Description = container.Labels.getOrDefault(dockerContainerLabelDescription, "")
|
|
||||||
dc.Icon = newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if idValue := container.Labels.getOrDefault(dockerContainerLabelID, ""); idValue != "" {
|
if idValue := container.Labels.getOrDefault(dockerContainerLabelID, ""); idValue != "" {
|
||||||
@ -214,7 +185,7 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames
|
|||||||
for i := range children {
|
for i := range children {
|
||||||
child := &children[i]
|
child := &children[i]
|
||||||
dc.Children = append(dc.Children, dockerContainer{
|
dc.Children = append(dc.Children, dockerContainer{
|
||||||
Title: deriveDockerContainerTitle(child),
|
Name: deriveDockerContainerName(child, formatNames),
|
||||||
StateText: child.Status,
|
StateText: child.Status,
|
||||||
StateIcon: dockerContainerStateToStateIcon(strings.ToLower(child.State)),
|
StateIcon: dockerContainerStateToStateIcon(strings.ToLower(child.State)),
|
||||||
})
|
})
|
||||||
@ -242,12 +213,31 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames
|
|||||||
return dockerContainers, nil
|
return dockerContainers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deriveDockerContainerTitle(container *dockerContainerJsonResponse) string {
|
func deriveDockerContainerName(container *dockerContainerJsonResponse, formatNames bool) string {
|
||||||
if v := container.Labels.getOrDefault(dockerContainerLabelName, ""); v != "" {
|
if v := container.Labels.getOrDefault(dockerContainerLabelName, ""); v != "" {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, "n/a"), "/")
|
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(
|
func groupDockerContainerChildren(
|
||||||
@ -288,17 +278,44 @@ func isDockerContainerHidden(container *dockerContainerJsonResponse, hideByDefau
|
|||||||
return hideByDefault
|
return hideByDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonResponse, error) {
|
func fetchDockerContainersFromSource(
|
||||||
client := &http.Client{
|
source string,
|
||||||
Timeout: 5 * time.Second,
|
category string,
|
||||||
Transport: &http.Transport{
|
runningOnly bool,
|
||||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
labelOverrides map[string]map[string]string,
|
||||||
return net.Dial("unix", socketPath)
|
) ([]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)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request, err := http.NewRequest("GET", "http://docker/containers/json?all=true", nil)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating request: %w", err)
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
}
|
}
|
||||||
@ -318,26 +335,43 @@ func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonR
|
|||||||
return nil, fmt.Errorf("decoding response: %w", err)
|
return nil, fmt.Errorf("decoding response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return containers, nil
|
for i := range containers {
|
||||||
}
|
container := &containers[i]
|
||||||
|
name := strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, ""), "/")
|
||||||
|
|
||||||
func (widget *dockerContainersWidget) GetContainerNames() ([]string, error) {
|
if name == "" {
|
||||||
containers, err := fetchAllDockerContainersFromSock(widget.SockPath)
|
continue
|
||||||
if err != nil {
|
}
|
||||||
return nil, fmt.Errorf("fetching containers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
names := make([]string, 0, len(containers))
|
overrides, ok := labelOverrides[name]
|
||||||
for _, container := range containers {
|
if !ok {
|
||||||
if !isDockerContainerHidden(&container, widget.HideByDefault) {
|
continue
|
||||||
// Get the clean container name without the leading '/'
|
}
|
||||||
name := strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, ""), "/")
|
|
||||||
if name != "" {
|
if container.Labels == nil {
|
||||||
names = append(names, name)
|
container.Labels = make(dockerContainerLabels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for label, value := range overrides {
|
||||||
|
container.Labels["glance."+label] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(names)
|
// We have to filter here instead of using the `filters` parameter of Docker's API
|
||||||
return names, nil
|
// 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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user