diff --git a/docs/configuration.md b/docs/configuration.md index 497da94..cb7b835 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1834,6 +1834,19 @@ Configuration of the containers is done via labels applied to each container: glance.description: Movies & shows ``` +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 +- type: docker-containers + containers: + container_name_1: + title: Container Name + description: Description of the container + url: https://container.domain.com + icon: si:container-icon + hide: false +``` + For services with multiple containers you can specify a `glance.id` on the "main" container and `glance.parent` on each "child" container:
@@ -1885,6 +1898,7 @@ If any of the child containers are down, their status will propagate up to the p | Name | Type | Required | Default | | ---- | ---- | -------- | ------- | | hide-by-default | boolean | no | false | +| format-container-names | boolean | no | false | | sock-path | string | no | /var/run/docker.sock | | category | string | no | | | running-only | boolean | no | false | @@ -1892,6 +1906,9 @@ If any of the child containers are down, their status will propagate up to the p ##### `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. +##### `format-container-names` +When set to `true`, automatically converts container names such as `container_name_1` into `Container Name 1`. + ##### `sock-path` The path to the Docker socket. This can also be a [remote socket](https://docs.docker.com/engine/daemon/remote-access/) or proxied socket using something like [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy). diff --git a/internal/glance/templates/docker-containers.html b/internal/glance/templates/docker-containers.html index d84e9a6..afbbb17 100644 --- a/internal/glance/templates/docker-containers.html +++ b/internal/glance/templates/docker-containers.html @@ -14,7 +14,7 @@ {{- range .Children }}
  • {{ template "state-icon" .StateIcon }}
    -
    {{ .Title }} {{ .StateText }}
    +
    {{ .Name }} {{ .StateText }}
  • {{- end }} @@ -24,9 +24,9 @@
    {{- if .URL }} - {{ .Title }} + {{ .Name }} {{- else }} -
    {{ .Title }}
    +
    {{ .Name }}
    {{- end }} {{- if .Description }}
    {{ .Description }}
    diff --git a/internal/glance/widget-docker-containers.go b/internal/glance/widget-docker-containers.go index 7676ca2..702a34b 100644 --- a/internal/glance/widget-docker-containers.go +++ b/internal/glance/widget-docker-containers.go @@ -16,12 +16,14 @@ import ( 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"` - Containers dockerContainerList `yaml:"-"` + 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 { @@ -35,7 +37,14 @@ func (widget *dockerContainersWidget) initialize() error { } func (widget *dockerContainersWidget) update(ctx context.Context) { - containers, err := fetchDockerContainers(widget.SockPath, widget.HideByDefault, widget.Category, widget.RunningOnly) + containers, err := fetchDockerContainers( + widget.SockPath, + widget.HideByDefault, + widget.Category, + widget.RunningOnly, + widget.FormatContainerNames, + widget.LabelOverrides, + ) if !widget.canContinueUpdateAfterHandlingErr(err) { return } @@ -102,7 +111,7 @@ func (l *dockerContainerLabels) getOrDefault(label, def string) string { } type dockerContainer struct { - Title string + Name string URL string SameTab bool Image string @@ -124,7 +133,7 @@ func (containers dockerContainerList) sortByStateIconThenTitle() { 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) }) } @@ -141,8 +150,15 @@ func dockerContainerStateToStateIcon(state string) string { } } -func fetchDockerContainers(socketPath string, hideByDefault bool, category string, runningOnly bool) (dockerContainerList, error) { - containers, err := fetchDockerContainersFromSource(socketPath, category, runningOnly) +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) } @@ -154,7 +170,7 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, category strin container := &containers[i] dc := dockerContainer{ - Title: deriveDockerContainerTitle(container), + Name: deriveDockerContainerName(container, formatNames), URL: container.Labels.getOrDefault(dockerContainerLabelURL, ""), Description: container.Labels.getOrDefault(dockerContainerLabelDescription, ""), SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")), @@ -169,7 +185,7 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, category strin for i := range children { child := &children[i] dc.Children = append(dc.Children, dockerContainer{ - Title: deriveDockerContainerTitle(child), + Name: deriveDockerContainerName(child, formatNames), StateText: child.Status, StateIcon: dockerContainerStateToStateIcon(strings.ToLower(child.State)), }) @@ -197,12 +213,31 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, category strin return dockerContainers, nil } -func deriveDockerContainerTitle(container *dockerContainerJsonResponse) string { +func deriveDockerContainerName(container *dockerContainerJsonResponse, formatNames bool) string { if v := container.Labels.getOrDefault(dockerContainerLabelName, ""); 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( @@ -243,7 +278,13 @@ func isDockerContainerHidden(container *dockerContainerJsonResponse, hideByDefau return hideByDefault } -func fetchDockerContainersFromSource(source string, category string, runningOnly bool) ([]dockerContainerJsonResponse, error) { + +func fetchDockerContainersFromSource( + source string, + category string, + runningOnly bool, + labelOverrides map[string]map[string]string, +) ([]dockerContainerJsonResponse, error) { var hostname string var client *http.Client @@ -271,20 +312,12 @@ func fetchDockerContainersFromSource(source string, category string, runningOnly } } - query := url.Values{} - query.Set("all", ternary(runningOnly, "false", "true")) - - if category != "" { - query.Set( - "filters", - fmt.Sprintf(`{"label": ["%s=%s"]}`, dockerContainerLabelCategory, category), - ) - } + 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?"+query.Encode(), nil) + request, err := http.NewRequestWithContext(ctx, "GET", "http://"+hostname+"/containers/json?all="+fetchAll, nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } @@ -304,5 +337,43 @@ func fetchDockerContainersFromSource(source string, category string, runningOnly 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 }