mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-21 02:18:22 +02:00
Merge pull request #453 from mike391/Add-support-configure-docker-containers-yaml
Feat: Added support for configuring Docker containers through glance.yaml instead of labels
This commit is contained in:
commit
1862858c07
@ -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:
|
||||
|
||||
<details>
|
||||
@ -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).
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
{{- 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>
|
||||
<div class="color-highlight">{{ .Name }} <span class="size-h5 color-base">{{ .StateText }}</span></div>
|
||||
</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
@ -24,9 +24,9 @@
|
||||
|
||||
<div class="min-width-0 grow">
|
||||
{{- 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 }}
|
||||
<div class="color-highlight text-truncate size-title-dynamic">{{ .Title }}</div>
|
||||
<div class="color-highlight text-truncate size-title-dynamic">{{ .Name }}</div>
|
||||
{{- end }}
|
||||
{{- if .Description }}
|
||||
<div class="text-truncate">{{ .Description }}</div>
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user