├── .dockerignore ├── .github └── workflows │ ├── docker.yaml │ └── pr.yaml ├── .gitignore ├── Dockerfile ├── README.md ├── assets └── screen.png ├── compose.sample.yaml ├── docker.go ├── go.mod ├── go.sum ├── html.go ├── main.go └── widget.gohtml /.dockerignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | go.work 9 | 10 | .idea 11 | *.iml 12 | glance-docker-container-ext* 13 | 14 | .git 15 | .gitignore 16 | assets 17 | README.md 18 | compose.sample.yaml 19 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | attestations: write 18 | id-token: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: '1.22' 28 | 29 | - name: Log in to the Container registry 30 | uses: docker/login-action@v3 31 | with: 32 | registry: ${{ env.REGISTRY }} 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Log in to the Container registry 37 | uses: docker/login-action@v3 38 | with: 39 | username: dvdandroid 40 | password: ${{ secrets.DOCKERHUB_TOKEN }} 41 | 42 | - name: Build Docker image 43 | run: | 44 | GITHUB_SHA=$(echo ${{ github.sha }} | cut -c1-7) 45 | VERSION=$(echo ${{ github.ref }} | sed 's/refs\/tags\///') 46 | IMAGE_TAG=${VERSION}-${GITHUB_SHA} 47 | docker build -t img -f Dockerfile . 48 | # make IMAGE_NAME lowercase 49 | IMAGE_NAME=$(echo ${{ env.IMAGE_NAME }} | tr '[:upper:]' '[:lower:]') 50 | 51 | if [[ ${{ github.event.release.prerelease }} == "false" ]]; then 52 | major=$(echo $VERSION | cut -d. -f1) 53 | minor=$(echo $VERSION | cut -d. -f2) 54 | 55 | tag_major="$major" 56 | tag_minor="$major.$minor" 57 | 58 | docker tag img ${{ env.REGISTRY }}/${IMAGE_NAME}:${tag_major} 59 | docker tag img ${{ env.REGISTRY }}/${IMAGE_NAME}:${tag_minor} 60 | docker tag img ${{ env.REGISTRY }}/${IMAGE_NAME}:latest 61 | 62 | docker tag img ${IMAGE_NAME}:${tag_major} 63 | docker tag img ${IMAGE_NAME}:${tag_minor} 64 | docker tag img ${IMAGE_NAME}:latest 65 | fi 66 | 67 | docker tag img ${{ env.REGISTRY }}/${IMAGE_NAME}:${VERSION} 68 | docker tag img ${{ env.REGISTRY }}/${IMAGE_NAME}:${IMAGE_TAG} 69 | docker tag img ${{ env.REGISTRY }}/${IMAGE_NAME}:${GITHUB_SHA} 70 | 71 | docker tag img ${IMAGE_NAME}:${VERSION} 72 | docker tag img ${IMAGE_NAME}:${IMAGE_TAG} 73 | docker tag img ${IMAGE_NAME}:${GITHUB_SHA} 74 | 75 | - name: Push Docker image 76 | run: | 77 | # make IMAGE_NAME lowercase 78 | IMAGE_NAME=$(echo ${{ env.IMAGE_NAME }} | tr '[:upper:]' '[:lower:]') 79 | docker push -a ${{ env.REGISTRY }}/${IMAGE_NAME} 80 | docker push -a ${IMAGE_NAME} 81 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | paths: 6 | - '*go*' 7 | - 'Dockerfile' 8 | pull_request: 9 | paths: 10 | - '*go*' 11 | - 'Dockerfile' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: '1.22' 25 | 26 | - name: Build Docker image 27 | run: | 28 | docker build -f Dockerfile . 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | go.work 9 | 10 | .idea 11 | *.iml 12 | glance-docker-container-ext* 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine AS builder 2 | 3 | WORKDIR /build 4 | COPY . . 5 | 6 | RUN go mod download 7 | RUN CGO_ENABLED=0 go build --trimpath -o glance-docker-container-ext . 8 | 9 | FROM alpine:latest 10 | 11 | WORKDIR /app 12 | 13 | COPY --from=builder /build/glance-docker-container-ext . 14 | COPY widget.gohtml /app/widget.gohtml 15 | 16 | CMD ["./glance-docker-container-ext"] 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | glance-docker-container-ext 2 | === 3 | 4 | ![Docker Image Size](https://img.shields.io/docker/image-size/dvdandroid/glance-docker-container-ext) 5 | ![Docker Image Version](https://img.shields.io/docker/v/dvdandroid/glance-docker-container-ext) 6 | 7 | [Glance](https://github.com/glanceapp/glance) extension that creates a widget that displays the running Docker containers. 8 | 9 | ![Sample Screenshot](./assets/screen.png) 10 | 11 | ## Installation 12 | 13 | Assuming you are using Docker compose, add the following to your `docker-compose.yml` file containing Glance container: 14 | 15 | ```yaml 16 | glance-docker-container-ext: 17 | image: dvdandroid/glance-docker-container-ext 18 | container_name: glance-docker-container-ext 19 | restart: unless-stopped 20 | environment: 21 | - DOCKER_HOST=unix:///var/run/docker.sock 22 | - PORT=8081 # Optional, default is 8081 23 | volumes: 24 | - /var/run/docker.sock:/var/run/docker.sock 25 | ``` 26 | 27 | then in your `glance.yml` config file, add the following: 28 | 29 | ```yaml 30 | - type: extension 31 | allow-potentially-dangerous-html: true 32 | url: http://glance-docker-container-ext:8081 33 | cache: 5m 34 | parameters: 35 | title: Docker Containers 36 | all: true 37 | order: name,status 38 | ``` 39 | 40 | ### Parameters 41 | 42 | | Parameter | Description | Default | 43 | |-----------------|--------------------------------------------------------------------------------------------------------------------------|---------------------| 44 | | `title` | Title of the widget | "Docker Containers" | 45 | | `all` | Show all containers or only running ones | `true` | 46 | | `order` | Order of the containers, comma separated **string** of `name`, `status`
(`name`,`status`,`name,status`,`status,name`) | `name` | 47 | | `group` | Identifier for the group of containers. If set, only containers with the same group will be displayed. | | 48 | | `same-tab` | Open the URL in the same tab. Value customizable per container | `false` | 49 | | `ignore-status` | Status of the containers will not be displayed | `false` | 50 | 51 | ## Configuration 52 | 53 | Then, for every container you want to monitor, add the following labels to its compose file: 54 | 55 | ```yaml 56 | labels: 57 | glance.0.enable: true 58 | glance.0.name: Sonarr 59 | glance.0.description: TV show search 60 | glance.0.group: media 61 | glance.0.url: http://sonarr.lan 62 | glance.0.icon: ./assets/imgs/television-classic.svg 63 | ``` 64 | 65 | :warning: Multiple labels can be added to the same container. Read below 66 | 67 | | Label | Description | Default | 68 | |------------------------|------------------------------------------------------------------------------------------------------------|----------------| 69 | | `glance.X.enable` | Enable monitoring for this container | | 70 | | `glance.X.name` | Name of the container | container name | 71 | | `glance.X.description` | Description of the container | | 72 | | `glance.X.group` | Identifier for the group of containers, used in combination with parameter `group` in glance configuration | | 73 | | `glance.X.url` | URL to open when clicking on the container | | 74 | | `glance.X.icon` | Icon to display, pointing to assets or Simple Icon (`si:` prefix) | | 75 | | `glance.X.same-tab` | Open the URL in the same tab | `false` | 76 | 77 | Value of `X` must be replaced with a number starting from 0: this allows to add multiple widget referring to the same container. 78 | 79 | For example, if you want to define two widgets for the same container, but with different labels, you can do it like this (in the compose file): 80 | 81 | ```yaml 82 | labels: 83 | glance.0.enable: true 84 | glance.0.name: Container (ADMIN) 85 | glance.0.url: http://website.lan/admin 86 | glance.1.enable: true 87 | glance.1.group: User 88 | glance.1.name: Container (USER) 89 | glance.1.description: User access 90 | glance.1.url: http://website.lan/user 91 | ``` 92 | -------------------------------------------------------------------------------- /assets/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DVDAndroid/glance-docker-container-ext/f618dcb633504f8c36b1a2314470afe0baaca938/assets/screen.png -------------------------------------------------------------------------------- /compose.sample.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | glance: 3 | image: ghcr.io/dvdandroid/glance:2024-08-05-5da1768 4 | container_name: glance 5 | restart: unless-stopped 6 | volumes: 7 | - ./glance/glance.yml:/app/glance.yml 8 | - /etc/timezone:/etc/timezone:ro 9 | - /etc/localtime:/etc/localtime:ro 10 | networks: 11 | - .... 12 | - glance-network 13 | 14 | glance-docker-container-ext: 15 | image: ghcr.io/dvdandroid/glance-docker-container-ext 16 | container_name: glance-docker-container-ext 17 | restart: unless-stopped 18 | environment: 19 | - DOCKER_HOST=unix:///var/run/docker.sock 20 | volumes: 21 | - /var/run/docker.sock:/var/run/docker.sock 22 | networks: 23 | - glance-network 24 | 25 | networks: 26 | glance-network: 27 | name: glance-network 28 | driver_opts: 29 | com.docker.network.bridge.name: br-glance 30 | -------------------------------------------------------------------------------- /docker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | docker "github.com/fsouza/go-dockerclient" 5 | "log/slog" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type DockerContainer struct { 12 | Name string 13 | Description string 14 | State string // created|restarting|running|removing|paused|exited|dead 15 | Status string 16 | Icon string 17 | IsSvgIcon bool 18 | URL string 19 | SameTab bool 20 | } 21 | 22 | type GlanceLabel struct { 23 | Enable bool 24 | Name string 25 | Description string 26 | Url string 27 | Icon string 28 | Group string 29 | SameTab bool 30 | } 31 | 32 | func LoadContainers(dockerClient *docker.Client, p params) ([]DockerContainer, error) { 33 | containerList, err := dockerClient.ListContainers(docker.ListContainersOptions{ 34 | All: p.AllContainers, 35 | }) 36 | 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | var containers []DockerContainer 42 | for _, container := range containerList { 43 | glanceLabels := make(map[int]GlanceLabel) 44 | 45 | for label, value := range container.Labels { 46 | if !strings.HasPrefix(label, "glance.") { 47 | continue 48 | } 49 | 50 | parts := strings.Split(label, ".") 51 | if len(parts) != 3 { 52 | if len(parts) == 2 { 53 | slog.Warn("found deprecated label. use new format instead glance.."+parts[1], "label", label, "container", container.Names[0][1:]) 54 | } 55 | continue 56 | } 57 | 58 | index, err := strconv.Atoi(parts[1]) 59 | if err != nil { 60 | continue 61 | } 62 | 63 | gl, exists := glanceLabels[index] 64 | if !exists { 65 | glanceLabels[index] = GlanceLabel{} 66 | gl = glanceLabels[index] 67 | } 68 | switch parts[2] { 69 | case "enable": 70 | gl.Enable = value == "true" 71 | case "name": 72 | gl.Name = value 73 | case "description": 74 | gl.Description = value 75 | case "group": 76 | gl.Group = value 77 | case "icon": 78 | gl.Icon = value 79 | if strings.HasPrefix(value, "si:") { 80 | gl.Icon = strings.TrimPrefix(value, "si:") 81 | gl.Icon = "https://cdnjs.cloudflare.com/ajax/libs/simple-icons/11.14.0/" + gl.Icon + ".svg" 82 | } 83 | case "url": 84 | gl.Url = value 85 | case "same-tab": 86 | gl.SameTab = value == "true" 87 | } 88 | glanceLabels[index] = gl 89 | } 90 | 91 | for _, gl := range glanceLabels { 92 | if !gl.Enable { 93 | continue 94 | } 95 | if gl.Group != p.Group { 96 | continue 97 | } 98 | 99 | state := container.State 100 | if p.IgnoreStatus { 101 | state = "" 102 | } 103 | 104 | if gl.Name == "" { 105 | gl.Name = container.Names[0][1:] 106 | } 107 | 108 | containers = append(containers, DockerContainer{ 109 | Name: gl.Name, 110 | Status: container.Status, 111 | State: state, 112 | Description: gl.Description, 113 | Icon: gl.Icon, 114 | IsSvgIcon: strings.Contains(gl.Icon, "/simple-icons/") || strings.HasSuffix(gl.Icon, ".svg"), 115 | URL: gl.Url, 116 | SameTab: p.SameTab || gl.SameTab, 117 | }) 118 | } 119 | } 120 | 121 | sortContainers(containers, strings.Split(p.Order, ",")) 122 | 123 | return containers, nil 124 | } 125 | 126 | func sortContainers(containers []DockerContainer, order []string) { 127 | sort.Slice(containers, func(i, j int) bool { 128 | for _, field := range order { 129 | switch field { 130 | case "name": 131 | name1 := strings.ToLower(containers[i].Name) 132 | name2 := strings.ToLower(containers[j].Name) 133 | if name1 != name2 { 134 | return name1 < name2 135 | } 136 | description1 := strings.ToLower(containers[i].Description) 137 | description2 := strings.ToLower(containers[j].Description) 138 | if description1 != description2 { 139 | return description1 < description2 140 | } 141 | case "status": 142 | if containers[i].State != containers[j].State { 143 | return containers[i].State < containers[j].State 144 | } 145 | } 146 | } 147 | return false 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module glance-docker-container-ext 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/fsouza/go-dockerclient v1.11.1 7 | github.com/gorilla/schema v1.4.1 8 | ) 9 | 10 | require ( 11 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 12 | github.com/Microsoft/go-winio v0.6.2 // indirect 13 | github.com/containerd/containerd v1.6.26 // indirect 14 | github.com/containerd/log v0.1.0 // indirect 15 | github.com/docker/docker v27.1.1+incompatible // indirect 16 | github.com/docker/go-connections v0.4.0 // indirect 17 | github.com/docker/go-units v0.5.0 // indirect 18 | github.com/gogo/protobuf v1.3.2 // indirect 19 | github.com/klauspost/compress v1.15.9 // indirect 20 | github.com/moby/docker-image-spec v1.3.1 // indirect 21 | github.com/moby/patternmatcher v0.6.0 // indirect 22 | github.com/moby/sys/sequential v0.5.0 // indirect 23 | github.com/moby/sys/user v0.1.0 // indirect 24 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 25 | github.com/morikuni/aec v1.0.0 // indirect 26 | github.com/opencontainers/go-digest v1.0.0 // indirect 27 | github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect 28 | github.com/pkg/errors v0.9.1 // indirect 29 | github.com/sirupsen/logrus v1.9.3 // indirect 30 | golang.org/x/sys v0.22.0 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krnnfGj4pV65YLUm3C0/8bl7V5Nry2Pwvy3ru/wLc= 2 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/containerd/containerd v1.6.26 h1:VVfrE6ZpyisvB1fzoY8Vkiq4sy+i5oF4uk7zu03RaHs= 8 | github.com/containerd/containerd v1.6.26/go.mod h1:I4TRdsdoo5MlKob5khDJS2EPT1l1oMNaE2MBm6FrwxM= 9 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 10 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 11 | github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= 12 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 17 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 18 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 19 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 20 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 21 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 22 | github.com/fsouza/go-dockerclient v1.11.1 h1:i5Vk9riDxW2uP9pVS5FYkpquMTFT5lsx2pt7oErRTjI= 23 | github.com/fsouza/go-dockerclient v1.11.1/go.mod h1:UfjOOaspAq+RGh7GX1aZ0HeWWGHQWWsh+H5BgEWB3Pk= 24 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 25 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 26 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 27 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 29 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 | github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= 31 | github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 32 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 33 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 34 | github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= 35 | github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 36 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 37 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 38 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 39 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 40 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 41 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 42 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 43 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 44 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= 45 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= 46 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 47 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 48 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 49 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 50 | github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= 51 | github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= 52 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 53 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 54 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 58 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 59 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 60 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 61 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 63 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 64 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 65 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 66 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 67 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 68 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 69 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 70 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 71 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 72 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 73 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 74 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 75 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 76 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 77 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 78 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 79 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 80 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 85 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 86 | golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= 87 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 88 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 89 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 90 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 91 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 92 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 93 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 94 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 95 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 96 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 97 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 98 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 102 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 104 | gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= 105 | gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 106 | -------------------------------------------------------------------------------- /html.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "log/slog" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | func BuildHtml(w http.ResponseWriter, containers []DockerContainer) { 11 | content, err := os.ReadFile("widget.gohtml") 12 | if err != nil { 13 | slog.Error("error reading widget template", "err", err) 14 | return 15 | } 16 | 17 | tmpl := template.Must(template.New("webpage").Parse(string(content))) 18 | tmpl.Execute(w, map[string]interface{}{ 19 | "Containers": containers, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | docker "github.com/fsouza/go-dockerclient" 6 | "github.com/gorilla/schema" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | type params struct { 13 | WidgetTitle string `schema:"title,default:Docker Containers"` 14 | Group string `schema:"group"` 15 | AllContainers bool `schema:"all,default:true"` 16 | Order string `schema:"order,default:name"` 17 | SameTab bool `schema:"same-tab,default:false"` 18 | IgnoreStatus bool `schema:"ignore-status,default:false"` 19 | } 20 | 21 | func main() { 22 | host := os.Getenv("HOST") 23 | port := os.Getenv("PORT") 24 | if port == "" { 25 | port = "8081" 26 | } 27 | 28 | dockerClient, err := docker.NewClientFromEnv() 29 | if err != nil { 30 | slog.Error("error creating docker client", "err", err) 31 | panic(err) 32 | } 33 | 34 | var decoder = schema.NewDecoder() 35 | decoder.IgnoreUnknownKeys(true) 36 | 37 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 38 | var p params 39 | err := decoder.Decode(&p, r.URL.Query()) 40 | if err != nil { 41 | slog.Error("error decoding params", "err", err) 42 | w.WriteHeader(http.StatusInternalServerError) 43 | return 44 | } 45 | 46 | w.Header().Set("Widget-Title", p.WidgetTitle) 47 | w.Header().Set("Widget-Content-Type", "html") 48 | w.Header().Set("Content-Type", "text/html") 49 | 50 | containers, err := LoadContainers(dockerClient, p) 51 | if err != nil { 52 | slog.Error("cannot connect to docker engine", "err", err) 53 | w.WriteHeader(http.StatusInternalServerError) 54 | w.Write([]byte(`
55 |
ERROR
56 |
57 |
58 |

Cannot connect to Docker Engine

`)) 59 | return 60 | } 61 | 62 | if len(containers) == 0 { 63 | w.Write([]byte(`

No containers found

`)) 64 | return 65 | } 66 | 67 | BuildHtml(w, containers) 68 | }) 69 | 70 | slog.Info("starting webserver", "host", host, "port", port) 71 | err = http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), nil) 72 | if err != nil { 73 | slog.Error("error starting webserver", "err", err) 74 | return 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /widget.gohtml: -------------------------------------------------------------------------------- 1 | 8 | 9 | {{ define "container" }} 10 | {{- /*gotype: glance-docker-container-ext.DockerContainer*/ -}} 11 | {{ if .Icon }} 12 | {{ if .URL }} 13 | 14 | {{ end }} 15 | 16 | {{ if .URL }} 17 | 18 | {{ end }} 19 | {{ end }} 20 |
21 | {{ if .URL }} 22 | {{ .Name }} 24 | {{ else }} 25 |

{{ .Name }}

26 | {{ end }} 27 | {{ if .Description}} 28 |

{{ .Description }}

29 | {{ end }} 30 |
31 | {{ if ne .State "" }} 32 |
33 | {{ if eq .State "created" }} 34 | 36 | 37 | 38 | {{ end }} 39 | {{ if eq .State "running" }} 40 | 41 | 44 | 45 | {{ end }} 46 | {{ if eq .State "restarting" }} 47 | 49 | 51 | 52 | {{ end }} 53 | {{ if eq .State "paused" }} 54 | 56 | 58 | 59 | {{ end }} 60 | {{ if eq .State "removing" }} 61 | 63 | 65 | 66 | {{ end }} 67 | {{ if eq .State "exited" }} 68 | 70 | 71 | 73 | 74 | {{ end }} 75 | {{ if eq .State "dead" }} 76 | 78 | 80 | 81 | {{ end }} 82 |
83 | {{ end }} 84 | {{ end }} 85 | --------------------------------------------------------------------------------