Docker containers: remote socket, category and running-only

This commit is contained in:
Svilen Markov 2025-04-26 16:19:17 +01:00
parent 77fb199cb3
commit 18436e91e0
2 changed files with 102 additions and 12 deletions

View File

@ -1886,12 +1886,66 @@ If any of the child containers are down, their status will propagate up to the p
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| hide-by-default | boolean | no | false | | hide-by-default | boolean | no | false |
| sock-path | string | no | /var/run/docker.sock | | sock-path | string | no | /var/run/docker.sock |
| category | string | no | |
| running-only | boolean | no | false |
##### `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.
##### `sock-path` ##### `sock-path`
The path to the Docker socket. 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).
###### `category`
Filter to only the containers which have this category specified via the `glance.category` label. Useful if you want to have multiple containers widgets, each showing a different set of containers.
<details>
<summary>View example</summary>
<br>
```yaml
services:
jellyfin:
image: jellyfin/jellyfin:latest
labels:
glance.name: Jellyfin
glance.icon: si:jellyfin
glance.url: https://jellyfin.domain.com
glance.category: media
gitea:
image: gitea/gitea:latest
labels:
glance.name: Gitea
glance.icon: si:gitea
glance.url: https://gitea.domain.com
glance.category: dev-tools
vaultwarden:
image: vaultwarden/server:latest
labels:
glance.name: Vaultwarden
glance.icon: si:vaultwarden
glance.url: https://vaultwarden.domain.com
glance.category: dev-tools
```
Then you can use the `category` property to filter the containers:
```yaml
- type: docker-containers
title: Dev tool containers
category: dev-tools
- type: docker-containers
title: Media containers
category: media
```
</details>
##### `running-only`
Whether to only show running containers. If set to `true` only containers that are currently running will be displayed. If set to `false` all containers will be displayed regardless of their state.
#### Labels #### Labels
| Name | Description | | Name | Description |
@ -1904,6 +1958,7 @@ The path to the Docker socket.
| glance.hide | Whether to hide the container. If set to `true` the container will not be displayed. Defaults to `false`. | | glance.hide | Whether to hide the container. If set to `true` the container will not be displayed. Defaults to `false`. |
| glance.id | The custom ID of the container. Used to group containers under a single parent. | | glance.id | The custom ID of the container. Used to group containers under a single parent. |
| glance.parent | The ID of the parent container. Used to group containers under a single parent. | | glance.parent | The ID of the parent container. Used to group containers under a single parent. |
| glance.category | The category of the container. Used to filter containers by category. |
### DNS Stats ### DNS Stats
Display statistics from a self-hosted ad-blocking DNS resolver such as AdGuard Home, Pi-hole, or Technitium. Display statistics from a self-hosted ad-blocking DNS resolver such as AdGuard Home, Pi-hole, or Technitium.

View File

@ -7,6 +7,7 @@ import (
"html/template" "html/template"
"net" "net"
"net/http" "net/http"
"net/url"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -17,6 +18,8 @@ var dockerContainersWidgetTemplate = mustParseTemplate("docker-containers.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"`
RunningOnly bool `yaml:"running-only"`
Category string `yaml:"category"`
SockPath string `yaml:"sock-path"` SockPath string `yaml:"sock-path"`
Containers dockerContainerList `yaml:"-"` Containers dockerContainerList `yaml:"-"`
} }
@ -32,7 +35,7 @@ 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) containers, err := fetchDockerContainers(widget.SockPath, widget.HideByDefault, widget.Category, widget.RunningOnly)
if !widget.canContinueUpdateAfterHandlingErr(err) { if !widget.canContinueUpdateAfterHandlingErr(err) {
return return
} }
@ -54,6 +57,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 (
@ -137,8 +141,8 @@ func dockerContainerStateToStateIcon(state string) string {
} }
} }
func fetchDockerContainers(socketPath string, hideByDefault bool) (dockerContainerList, error) { func fetchDockerContainers(socketPath string, hideByDefault bool, category string, runningOnly bool) (dockerContainerList, error) {
containers, err := fetchAllDockerContainersFromSock(socketPath) containers, err := fetchDockerContainersFromSource(socketPath, category, runningOnly)
if err != nil { if err != nil {
return nil, fmt.Errorf("fetching containers: %w", err) return nil, fmt.Errorf("fetching containers: %w", err)
} }
@ -239,17 +243,48 @@ func isDockerContainerHidden(container *dockerContainerJsonResponse, hideByDefau
return hideByDefault return hideByDefault
} }
func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonResponse, error) { func fetchDockerContainersFromSource(source string, category string, runningOnly bool) ([]dockerContainerJsonResponse, error) {
client := &http.Client{ var hostname string
Timeout: 5 * time.Second,
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{ Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath) return net.Dial("unix", source)
}, },
}, },
} }
}
request, err := http.NewRequest("GET", "http://docker/containers/json?all=true", nil) query := url.Values{}
query.Set("all", ternary(runningOnly, "false", "true"))
if category != "" {
query.Set(
"filters",
fmt.Sprintf(`{"label": ["%s=%s"]}`, dockerContainerLabelCategory, category),
)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
request, err := http.NewRequestWithContext(ctx, "GET", "http://"+hostname+"/containers/json?"+query.Encode(), nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("creating request: %w", err) return nil, fmt.Errorf("creating request: %w", err)
} }