Compare commits

...

77 Commits

Author SHA1 Message Date
Amir Raminfar f8fbd796b7 chore: updates mods 2025-03-07 08:41:50 -08:00
renovate[bot] e4a42125c5 fix(deps): update all non-major dependencies (#3681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 16:04:17 +00:00
renovate[bot] db2b7b12a3 fix(deps): update all non-major dependencies (#3680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 16:41:58 +00:00
renovate[bot] d6b262aead chore(deps): update dependency prettier to ^3.5.3 (#3679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 02:49:02 +00:00
renovate[bot] 917489838d fix(deps): update all non-major dependencies (#3676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 13:17:57 +00:00
Amir Raminfar cefee41857 chore: updates mods (#3677) 2025-03-01 08:23:26 -08:00
Amir Raminfar 8455e2743d chore: adds readme to examples 2025-03-01 08:22:33 -08:00
Amir Raminfar a5cc1f7ca0 chore: adds readme to examples 2025-03-01 08:22:26 -08:00
Amir Raminfar 1ffc338720 chore: adds readme to examples 2025-03-01 08:21:46 -08:00
renovate[bot] 19bb865913 fix(deps): update all non-major dependencies (#3672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 16:45:35 +00:00
renovate[bot] bb4dca2386 fix(deps): update all non-major dependencies (#3671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-26 19:09:12 +00:00
Amir Raminfar 808439365f chore: release v8.11.7 2025-02-24 15:28:24 -08:00
Amir Raminfar c1038c0a9e chore: adds warning log if containers are empty using user filter (#3669) 2025-02-24 20:42:30 +00:00
Amir Raminfar 15e410a874 chore: updates readme 2025-02-24 08:29:32 -08:00
renovate[bot] 5049a53a95 chore(deps): update all non-major dependencies to ^2.2.4 (#3668)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 01:42:45 +00:00
Amir Raminfar c86d108997 chore: adds app version to logger (#3666)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-23 08:51:30 -08:00
Amir Raminfar 604c4b9b16 chore: updates docs
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-22 16:59:54 -08:00
Amir Raminfar 64b03d7b92 chore: updates docs 2025-02-22 16:58:28 -08:00
Amir Raminfar a533533d4c chore: updates docs 2025-02-22 11:37:27 -08:00
Amir Raminfar 2d50820932 fix: uses request context instead of timeout for actions (#3663) 2025-02-22 19:23:59 +00:00
Amir Raminfar 3190868268 chore: release v8.11.6
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-22 08:45:12 -08:00
renovate[bot] 2c031da510 fix(deps): update all non-major dependencies (#3661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 16:36:30 +00:00
Amir Raminfar 58ececc42a chore: fixes static
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Go Staticcheck (push) Waiting to run
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-21 15:05:29 -08:00
Amir Raminfar 7be55cbc08 chore: refactors sub commands 2025-02-21 15:03:14 -08:00
garikkh 90b90ceea4 feat: Supports log levels with common separators (#3656) 2025-02-21 11:56:02 -08:00
renovate[bot] bc842389f9 fix(deps): update all non-major dependencies to v4.0.8 (#3658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 16:25:29 +00:00
Amir Raminfar cf9bf4dcd5 chore: release v8.11.5
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-20 10:57:33 -08:00
Amir Raminfar 3eec803e45 chore: updates docker to v28 (#3655) 2025-02-20 18:54:25 +00:00
renovate[bot] e332b4225b fix(deps): update all non-major dependencies (#3653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 18:41:29 +00:00
Amir Raminfar 538d16a5b7 chore: updates docs 2025-02-20 10:28:35 -08:00
Amir Raminfar 561081ebf0 chore: removes crowdin 2025-02-20 08:52:11 -08:00
Amir Raminfar c3d2e7ac09 chore: fix pt lang 2025-02-20 08:47:18 -08:00
Amir Raminfar 90808aff68 chore: Lang updates from crowdin (#3651)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-20 01:52:50 +00:00
Amir Raminfar 77bfb43b6d fix: Removes streams from k8s. See #3646 (#3647)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Go Staticcheck (push) Waiting to run
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-19 09:28:34 -08:00
Amir Raminfar 49881c2dfe chore: updates git issues template 2025-02-19 08:40:27 -08:00
Amir Raminfar 3f901769cb chore: updates docs 2025-02-19 08:35:33 -08:00
Amir Raminfar 26f7704edb chore: updates components 2025-02-19 07:49:13 -08:00
renovate[bot] 3686f5c54a fix(deps): update all non-major dependencies (#3645)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 15:01:58 +00:00
Amir Raminfar aabd9dd994 chore: removes replica
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-18 12:22:45 -08:00
Amir Raminfar 52e0be0ac6 chore: release v8.11.4 2025-02-18 12:20:47 -08:00
Amir Raminfar bb0ffbfa04 chore: updates docs 2025-02-18 12:20:38 -08:00
Amir Raminfar 4022b510e2 feat: adds labels support to k8s (#3642) 2025-02-18 12:15:19 -08:00
renovate[bot] 21fb03c9d0 fix(deps): update all non-major dependencies (#3641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 18:23:05 +00:00
Amir Raminfar 392f480c76 feat: adds streams to k8s
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-02-17 15:47:50 -08:00
renovate[bot] c9418d4b32 chore(deps): update pnpm to v10.4.1 (#3639)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 03:40:39 +00:00
Amir Raminfar 14b14e6210 chore: adds funding
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-16 08:53:46 -08:00
Amir Raminfar 3d5b279a1b Update Crowdin configuration file 2025-02-16 08:18:48 -08:00
Jérémy Touati aca3e3a219 fix: fixes ansi-to-html fallback colors (#3635) 2025-02-16 06:11:31 -08:00
Amir Raminfar 241d349d39 chore: release v8.11.3
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Go Staticcheck (push) Waiting to run
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-15 10:08:27 -08:00
Amir Raminfar b098dcaef0 fix: fixes missing client 2025-02-15 10:07:20 -08:00
Amir Raminfar 7cf67eb99a chore: release v8.11.2 2025-02-15 09:49:01 -08:00
renovate[bot] 869d5e5002 fix(deps): update all non-major dependencies (#3633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 17:48:48 +00:00
Amir Raminfar 392dfafc3d chore: updates docs 2025-02-15 09:37:57 -08:00
Amir Raminfar 21fe5b84f2 feat: supports namespaces properly in k8s mode (#3631) 2025-02-15 09:35:32 -08:00
Amir Raminfar 65fa04c877 chore: adds metrics to docs
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-14 07:46:58 -08:00
renovate[bot] 3a352a7320 fix(deps): update all non-major dependencies (#3629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 15:01:24 +00:00
Amir Raminfar 3bdc9c04f8 chore: release v8.11.1
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-13 16:41:06 -08:00
Amir Raminfar 6e3c811974 fix: fixes broken nil pointer. fixes #3626 (#3627)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-13 13:31:25 -08:00
Amir Raminfar ddff9e96a2 chore: updates static go version 2025-02-13 08:35:11 -08:00
Amir Raminfar 1daf813e1b chore: release v8.11.0 2025-02-13 08:21:04 -08:00
renovate[bot] 09a169851b fix(deps): update all non-major dependencies (#3624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 16:19:57 +00:00
Amir Raminfar 1d5ddee1ae docs: adds k8s in docs (#3623) 2025-02-13 08:19:37 -08:00
Amir Raminfar b7e9871382 chore: adds tests cases back
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-12 17:02:24 -08:00
Amir Raminfar 9e028b9916 chore: improves tests 2025-02-12 15:55:49 -08:00
renovate[bot] 78c0e5fd7f fix(deps): update dependency pinia to ^3.0.1 (#3619)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-12 19:59:28 +00:00
Amir Raminfar 4861503488 chore: updates to go v1.24 (#3622) 2025-02-12 11:53:52 -08:00
Amir Raminfar 115b0e475d docs: adds k8s in docs (#3620) 2025-02-12 15:53:56 +00:00
Amir Raminfar 6d928815df chore: updates pinia (#3618)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-11 23:00:43 +00:00
Amir Raminfar 2a70d0b458 chore: add metrics to config 2025-02-11 14:54:37 -08:00
Amir Raminfar 8c9c9a83af fix: fixes deadlocks in store (#3617)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-11 12:32:56 -08:00
Amir Raminfar 571de4084e chore: removes survey 2025-02-11 09:57:36 -08:00
Amir Raminfar 5abf7e4a9d docs: remove banner for survey 2025-02-11 09:22:27 -08:00
renovate[bot] d580e9447b chore(deps): update pnpm to v10.3.0 (#3616)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 03:29:31 +00:00
Amir Raminfar 877faaa740 chore: fixes broken interface 2025-02-10 14:30:37 -08:00
Amir Raminfar 1d4698d77a chore: adds error for agent 2025-02-10 14:19:05 -08:00
Amir Raminfar 13da2a4222 feat: supports k8s cluster (#3599)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-10 09:29:39 -08:00
renovate[bot] 209ce312d4 fix(deps): update all non-major dependencies to v4.0.6 (#3615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 17:21:40 +00:00
99 changed files with 3836 additions and 2192 deletions
+1
View File
@@ -23,6 +23,7 @@ body:
- Agents
- Remote Hosts
- Swarm Mode
- Kubernetes
validations:
required: true
- type: input
+1
View File
@@ -0,0 +1 @@
buy_me_a_coffee: amirraminfar
+2 -2
View File
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to DockerHub
uses: docker/login-action@v3.3.0
with:
@@ -121,7 +121,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.13.0
uses: docker/build-push-action@v6.15.0
with:
push: true
context: .
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to DockerHub
uses: docker/login-action@v3.3.0
with:
@@ -39,7 +39,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.13.0
uses: docker/build-push-action@v6.15.0
with:
context: .
push: true
+1 -1
View File
@@ -77,7 +77,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.24"
check-latest: true
- name: Generate dependencies
run: make fake_assets shared_key.pem shared_cert.pem
-16
View File
@@ -1,16 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug test",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"args": ["test"]
}
]
}
-7
View File
@@ -1,7 +0,0 @@
{
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.keystyle": "nested",
"cSpell.words": ["healthcheck", "orderedmap", "Retriable", "stdcopy", "Warnf"],
"editor.formatOnSave": true,
"i18n-ally.extract.autoDetect": true
}
+2 -2
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:23.7.0-alpine AS node
FROM --platform=$BUILDPLATFORM node:23.9.0-alpine AS node
RUN corepack enable
@@ -22,7 +22,7 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.23.6-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
# install gRPC dependencies
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
+6 -4
View File
@@ -97,9 +97,9 @@ Once you have the podman remote socket you can run Dozzle on podman.
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 docker.io/amir20/dozzle:latest
```
Additionally you have to create a fake engine-id to prevent ```host not found``` errors. Podman doesn't generate an engine-id like Docker by itself due to its daemonless architecture.
Additionally you have to create a fake engine-id to prevent `host not found` errors. Podman doesn't generate an engine-id like Docker by itself due to its daemonless architecture.
Under ```/var/lib/docker``` create a file named ```engine-id```. On a system with Podman you will have to create the folder path as well. Inside the file place the UUID, for instance using ```uuidgen > engine-id```. After that the file should have an identifier that looks like this: ```b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9```.
Under `/var/lib/docker` create a file named `engine-id`. On a system with Podman you will have to create the folder path as well. Inside the file place the UUID, for instance using `uuidgen > engine-id`. After that the file should have an identifier that looks like this: `b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9`.
For more details check [Podman Infos](docs/guide/podman.md) or the [FAQ](docs/guide/faq.md#i-am-seeing-host-not-found-error-in-the-logs-how-do-i-fix-it)
@@ -127,12 +127,14 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | false |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | `false` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | `false` |
| `--mode` | `DOZZLE_MODE` | `server` |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
| `--timeout` | `DOZZLE_TIMEOUT` | `10s` |
| `--namespace` | `DOZZLE_NAMESPACE` | `""` |
## Support
+2
View File
@@ -2,6 +2,7 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
@@ -12,6 +13,7 @@ declare module 'vue' {
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
'Carbon:copyFile': typeof import('~icons/carbon/copy-file')['default']
'Carbon:information': typeof import('~icons/carbon/information')['default']
'Carbon:logoKubernetes': typeof import('~icons/carbon/logo-kubernetes')['default']
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
'Carbon:play': typeof import('~icons/carbon/play')['default']
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
+6 -2
View File
@@ -31,7 +31,8 @@
</ul>
<ul class="flex flex-row flex-wrap gap-x-2 text-sm md:gap-3">
<li class="flex items-center gap-1">
<octicon:container-24 class="inline-block" /> {{ $t("label.container", hostContainers[host.id]?.length) }}
<octicon:container-24 class="inline-block" />
{{ $t("label.container", hostContainers[host.id]?.length ?? 0) }}
</li>
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
</ul>
@@ -107,7 +108,10 @@ useIntervalFn(
stat.totalCPU += container.stat.cpu;
stat.totalMem += container.stat.memoryUsage;
}
weightedStats[host].mostRecent = stat;
if (weightedStats[host]) {
// TODO fix this init
weightedStats[host].mostRecent = stat;
}
}
},
1000,
@@ -19,8 +19,8 @@ import stripAnsi from "strip-ansi";
const ansiConvertor = new AnsiConvertor({
escapeXML: false,
fg: "oklch(var(--base-content-color))",
bg: "oklch(var(--base-color))",
fg: "var(--color-base-content)",
bg: "var(--color-base-100)",
});
defineProps<{
+1
View File
@@ -2,6 +2,7 @@
<mdi:satellite-variant v-if="type == 'agent'" />
<ph:globe-simple v-else-if="type == 'remote'" />
<mdi:hexagon-multiple v-else-if="type == 'swarm'" />
<carbon:logo-kubernetes v-else-if="type == 'k8s'" />
<ph:computer-tower v-else />
</template>
<script setup lang="ts">
+1 -15
View File
@@ -14,21 +14,7 @@ type Announcement = {
const { data: releases } = useFetch(withBase("/api/releases")).get().json<Announcement[]>();
const otherAnnouncements = [
{
body: "I'd love to hear about your experience in this short survey to shape the future of Dozzle!",
createdAt: new Date("2025-01-22T18:00:00Z"),
htmlUrl: "https://tally.so/r/wLv4g2?ref=notification",
name: "Take survey!",
announcement: true,
tag: "survey-2025-01",
latest: true,
mentionsCount: 0,
features: 0,
bugFixes: 0,
breaking: 0,
},
] as Announcement[];
const otherAnnouncements = [] as Announcement[];
const announcements = computed(() => {
const newReleases =
+1 -1
View File
@@ -3,7 +3,7 @@ export type Host = {
name: string;
nCPU: number;
memTotal: number;
type: "agent" | "local" | "remote" | "swarm";
type: "agent" | "local" | "remote" | "swarm" | "k8s";
endpoint: string;
available: boolean;
dockerVersion: string;
+1 -1
View File
@@ -95,7 +95,7 @@ services:
playwright:
container_name: playwright
image: mcr.microsoft.com/playwright:v1.50.1-jammy
image: mcr.microsoft.com/playwright:v1.51.0-jammy
working_dir: /app
volumes:
- .:/app
+9 -2
View File
@@ -59,6 +59,14 @@ export default defineConfig({
{ text: "Getting Started", link: "/guide/getting-started" },
],
},
{
text: "Platforms",
items: [
{ text: "Swarm", link: "/guide/swarm-mode" },
{ text: "K8s", link: "/guide/k8s" },
{ text: "Podman", link: "/guide/podman" },
],
},
{
text: "Advanced Configuration",
items: [
@@ -73,8 +81,6 @@ export default defineConfig({
{ text: "Filters", link: "/guide/filters" },
{ text: "Healthcheck", link: "/guide/healthcheck" },
{ text: "Remote Hosts", link: "/guide/remote-hosts" },
{ text: "Swarm Mode", link: "/guide/swarm-mode" },
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
{ text: "Logging Files on Disk", link: "/guide/log-files-on-disk" },
{ text: "SQL Engine", link: "/guide/sql-engine" },
],
@@ -84,6 +90,7 @@ export default defineConfig({
items: [
{ text: "FAQ", link: "/guide/faq" },
{ text: "Debugging", link: "/guide/debugging" },
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
],
},
{
+8 -4
View File
@@ -3,10 +3,14 @@
class="banner fixed top-0 right-0 left-0 z-(--vp-z-index-layout-top) flex items-center overflow-hidden bg-[oklch(74%_0.16_232.661)] p-4 font-bold text-gray-800 dark:bg-[oklch(60%_0.126_221.723)] dark:text-white"
>
<div class="mx-auto flex items-center gap-2 lg:gap-4">
<span class="animate-bounce">👋🏼</span> Using Dozzle? I'd love to hear about your experience in this short survey
to shape the future of Dozzle.
<a href="https://tally.so/r/wLv4g2?ref=docs" target="_blank" class="btn btn-sm btn-primary text-white!">
Take a quick survey 🙏🏼
<span class="animate-bounce">🚀</span>K8s users, I need your help! I have added K8s support to Dozzle and I need
your feedback! 🙏
<a
href="https://github.com/amir20/dozzle/discussions/3614"
target="_blank"
class="btn btn-sm btn-primary text-white!"
>
See the discussion
</a>
</div>
<button class="btn btn-circle btn-ghost btn-xs ml-auto" @click="dismiss">
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -8,6 +8,7 @@ import HeroVideo from "./components/HeroVideo.vue";
import BuyMeCoffee from "./components/BuyMeCoffee.vue";
import Stats from "./components/Stats.vue";
import Banner from "./components/Banner.vue";
import Supported from "./components/Supported.vue";
export default {
...DefaultTheme,
@@ -16,7 +17,8 @@ export default {
"home-hero-image": () => h(HeroVideo),
"sidebar-nav-after": () => h(BuyMeCoffee),
"home-hero-actions-after": () => h(Stats),
"layout-top": () => h(Banner),
// "layout-top": () => h(Banner),
"home-hero-after": () => h(Supported),
});
},
enhanceApp(ctx) {
+2
View File
@@ -2,6 +2,7 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
@@ -14,5 +15,6 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Stats: typeof import('./.vitepress/theme/components/Stats.vue')['default']
Supported: typeof import('./.vitepress/theme/components/Supported.vue')['default']
}
}
+1 -3
View File
@@ -4,9 +4,7 @@ title: Container Actions
# Using Container Actions
Dozzle now supports **Container Actions**, which allows you to `start`, `stop` and `restart` containers from within the UI in the dropdown menu.
This feature is **disabled** by default and can be enabled by setting the environment variable `DOZZLE_ENABLE_ACTIONS` to `true`.
Dozzle supports Container Actions, which allows you to `start`, `stop` and `restart` containers from within the UI in the dropdown menu. This feature is **disabled** by default and can be enabled by setting the environment variable `DOZZLE_ENABLE_ACTIONS` to `true`.
::: code-group
+1 -1
View File
@@ -2,7 +2,7 @@
title: Agent Mode
---
# Agent Mode
# Agent Mode <Badge type="warning" text="Docker Only" />
Dozzle can run in agent mode which can expose Docker hosts to other Dozzle instances. All communication is done over a secured connection using TLS. This means that you can deploy Dozzle on a remote host and connect to it from your local machine.
+90 -6
View File
@@ -4,9 +4,12 @@ title: Getting Started
# Getting Started
Dozzle supports multiple ways to run the application. You can run it using Docker CLI, Docker Compose, or in Swarm. The following sections will guide you through the process of setting up Dozzle.
Dozzle supports multiple ways to run the application. You can run it using Docker CLI, Docker Compose, Swarm, or Kubernetes. The following sections will guide you through the process of setting up Dozzle.
## Running with Docker <Badge type="tip" text="Updated" />
> [!TIP]
> If Docker Hub is blocked in your network, you can use the [GitHub Container Registry](https://ghcr.io/amir20/dozzle:latest) to pull the image. Use `ghcr.io/amir20/dozzle:latest` instead of `amir20/dozzle:latest`.
## Standalone Docker
The easiest way to set up Dozzle is to use the CLI and mount `docker.sock` file. This file is usually located at `/var/run/docker.sock` and can be mounted with the `--volume` flag. You also need to expose the port to view Dozzle. By default, Dozzle listens on port 8080, but you can change the external port using `-p`. You can also run using compose or as a service in Swarm.
@@ -27,6 +30,14 @@ services:
- 8080:8080
```
:::
Dozzle also supports connecting to remote agents to monitor multiple Docker hosts. See [agent](/guide/agent) to learn more.
## Docker Swarm
Dozzle supports running in Swarm mode by deploying it on every node. To run Dozzle in Swarm mode, you can use the following configuration:
```yaml [dozzle-stack.yml]
# Run with docker stack deploy -c dozzle-stack.yml <name>
services:
@@ -47,9 +58,82 @@ networks:
driver: overlay
```
:::
Then you can deploy the stack using the following command:
See [swarm mode](/guide/swarm-mode) for more information on running Dozzle in Swarm.
```bash
docker stack deploy -c dozzle-stack.yml <name>
```
> [!TIP]
> If Docker Hub is blocked in your network, you can use the [GitHub Container Registry](https://ghcr.io/amir20/dozzle:latest) to pull the image. Use `ghcr.io/amir20/dozzle:latest` instead of `amir20/dozzle:latest`.
See [swarm mode](/guide/swarm-mode) for more information.
## K8s <Badge type="tip" text="New" />
Dozzle supports running in Kubernetes. It only needs to be deployed on one node within the cluster.
```yaml [k8s-dozzle.yml]
# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-viewer
---
# clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-viewer-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
---
# clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pod-viewer-binding
subjects:
- kind: ServiceAccount
name: pod-viewer
namespace: default
roleRef:
kind: ClusterRole
name: pod-viewer-role
apiGroup: rbac.authorization.k8s.io
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
replicas: 1
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
```
Then apply the configuration using the following command:
```bash
kubectl apply -f k8s-dozzle.yml
```
See [Kubernetes mode](/guide/k8s) for more information.
+171
View File
@@ -0,0 +1,171 @@
---
title: Kubernetes Support
---
# Kubernetes Support <Badge type="warning" text="beta" /> <Badge type="tip" text="v8.11.x" />
Dozzle now supports Kubernetes, allowing you to view logs from your Kubernetes pods. This feature is available in `v8.11` version of Dozzle.
## Kubernetes Setup
To set up Dozzle in Kubernetes, you can use the following YAML configuration using `DOZZLE_MODE=k8s`. This configuration includes a deployment and a service to expose Dozzle.
```yaml
# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-viewer
---
# clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-viewer-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
---
# clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pod-viewer-binding
subjects:
- kind: ServiceAccount
name: pod-viewer
namespace: default
roleRef:
kind: ClusterRole
name: pod-viewer-role
apiGroup: rbac.authorization.k8s.io
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: dozzle-service
spec:
type: ClusterIP
selector:
app: dozzle
ports:
- port: 8080
targetPort: 8080
protocol: TCP
```
This configuration creates a service account, a cluster role, and a cluster role binding to allow Dozzle to access the necessary Kubernetes resources. It also creates a deployment for Dozzle and exposes it via a service.
All other features are supported as well, including authentication, filtering, and more. You can use the same environment variables as you would in Docker to configure Dozzle in Kubernetes.
> [!NOTE]
> Dozzle in Kubernetes is a new feature and may have some limitations compared to the Docker version. Please use this [discussion](https://github.com/amir20/dozzle/discussions/3614) to report any issues or suggestions for improvement.
## Metrics API
Dozzle relies on the [Kubernetes Metrics API](https://github.com/kubernetes-sigs/metrics-server) to retrieve resource usage information. The API can be installed using the following command:
```bash
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
```
To verify that the API is running, you can run the following command:
```bash
kubectl top pod
```
For now this is required to use Dozzle in Kubernetes.
## Namespaces and Filters
### Namespaces
By default, Dozzle will monitor all namespaces in the cluster. If you want to restrict Dozzle to a specific namespace, you can set the `DOZZLE_NAMESPACE` environment variable to the name of the namespace.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
- name: DOZZLE_NAMESPACE
value: "default"
```
### Labels and Filters
`DOZZLE_FILTER` behave similarlty to Docker filters. You can limit the scope of Dozzle using the `DOZZLE_FILTER` environment variable. For example, to scope only to `env=prod`:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
- name: DOZZLE_FILTER
value: "env=prod"
```
+4
View File
@@ -2,6 +2,10 @@
title: Podman
---
# Podman
Dozzle supports Podman. However, there are some issues with Podman that might prevent Dozzle from working properly. One of the main issues is that Podman doesn't create an engine-id like Docker.
## I am seeing host not found error in the logs. How do I fix it?
This should be mainly a Podman only error: Using Podman doesn't create an engine-id like Docker.
+21 -5
View File
@@ -9,7 +9,7 @@ Dozzle supports connecting to remote Docker hosts. This is useful when running D
However, with Dozzle agents, you can connect to remote hosts without exposing the Docker socket. See the [agent](/guide/agent) page for more information.
> [!WARNING]
> Remote hosts have been replaced with agents. Agents provide a more secure way to connect to remote hosts. Although remote hosts are still supported, it is recommended to use agents. See the [agent](/guide/agent) page for more information and examples. For comparison, see the [comparing agents with remote connections](/guide/agent#comparing-agents-with-remote-connection) section.
> Remote hosts have been replaced with agents. Agents provide a more secure way to connect to remote hosts. Although remote hosts are still supported, it is recommended to use agents. See the [agent](/guide/agent) page for more information and examples. For comparison, see the [comparing agents with remote connections](/guide/agent#comparing-agents-with-remote-connection) section. I won't be able to investigate user's issues with remote hosts as it is very time consuming.
## Connecting to Remote Hosts with TLS
@@ -45,7 +45,7 @@ services:
If you are in a private network, then you can use [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy) which exposes `docker.sock` file without the need for TLS. Dozzle will never try to write to Docker but it will need access to list APIs. The following command will start a proxy with minimal access:
```sh
docker container run --privileged -e CONTAINERS=1 -e INFO=1 -v /var/run/docker.sock:/var/run/docker.sock -p 2375:2375 tecnativa/docker-socket-proxy
$ docker container run --privileged -e CONTAINERS=1 -e INFO=1 -v /var/run/docker.sock:/var/run/docker.sock -p 2375:2375 tecnativa/docker-socket-proxy
```
> [!TIP]
@@ -53,12 +53,28 @@ docker container run --privileged -e CONTAINERS=1 -e INFO=1 -v /var/run/docker.s
Running Dozzle without any certificates should work. Here is an example:
```sh
docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --remote-host tcp://123.1.1.1:2375
::: code-group
```sh [cli]
$ docker run -p 8080:8080 amir20/dozzle --remote-host tcp://123.1.1.1:2375
```
```yaml [docker-compose.yml]
services:
dozzle:
image: amir20/dozzle:latest
ports:
- 8080:8080
environment:
DOZZLE_REMOTE_HOST: tcp://123.1.1.1:2375
```
:::
When using remote host, mounting `/var/run/docker.sock` is optional. You need to have at least one remote host to connect to.
> [!WARNING]
> Docker Socket Proxy is not recommended for production use. It is only for private networks.
> Docker Socket Proxy exposes the Docker API to the internet. This can be a security risk if not properly secured.
## Adding Labels to Hosts
+2 -1
View File
@@ -22,7 +22,8 @@ Configurations can be done with flags or environment variables. The table below
| `--mode` | `DOZZLE_MODE` | `server` |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
| `--timeout` | `DOZZLE_TIMEOUT` | `3s` |
| `--timeout` | `DOZZLE_TIMEOUT` | `10s` |
| `--namespace` | `DOZZLE_NAMESPACE` | `""` |
> [!TIP]
> Some flags like `--remote-host` or `--remote-agent` can be used multiple times. For example, `--remote-agent tcp://167.99.1.1:7007 --remote-agent tcp://167.99.1.2:7007` or comma-separated `DOZZLE_REMOTE_AGENT=tcp://167.99.1.1:7007,tcp://167.99.1.2:7007`.
+4 -1
View File
@@ -6,7 +6,7 @@ title: Home
hero:
name: "Dozzle"
tagline: Real-time logging and monitoring for Docker in the browser
tagline: Simple Container Monitoring and Logging
actions:
- theme: brand
text: Get Started
@@ -14,6 +14,9 @@ hero:
- theme: alt
text: View on GitHub
link: https://github.com/amir20/dozzle
- theme: alt
text: Support 🙏🏼
link: https://www.buymeacoffee.com/amirraminfar
features:
- title: Self Hosted
+1 -1
View File
@@ -6,6 +6,6 @@
"dozzle": "workspace:*"
},
"dependencies": {
"@fontsource-variable/playfair-display": "^5.1.1"
"@fontsource-variable/playfair-display": "^5.2.5"
}
}
+5 -5
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@fontsource-variable/playfair-display':
specifier: ^5.1.1
version: 5.1.1
specifier: ^5.2.5
version: 5.2.5
devDependencies:
dozzle:
specifier: workspace:*
@@ -18,9 +18,9 @@ importers:
packages:
'@fontsource-variable/playfair-display@5.1.1':
resolution: {integrity: sha512-89gLVOcVRIp0aayWwTcaLGQxhxDT2ZIv7cFU/BjxcWA8iALi8SrlT7dNHdhakb8MeXc0UUw9n8k6kG+jWvX61A==}
'@fontsource-variable/playfair-display@5.2.5':
resolution: {integrity: sha512-nzSCC5pbWHjkJBqjPe81cPIC7fqJ6U5DwJ6tec7vF1bQ2DJnaKRBZluRQJuseFpACZLTN8LeN/1gBQEA7PnKOw==}
snapshots:
'@fontsource-variable/playfair-display@5.1.1': {}
'@fontsource-variable/playfair-display@5.2.5': {}
+6
View File
@@ -0,0 +1,6 @@
> [!WARNING]
> This directory contains examples that are used for testing and demonstration purposes. The configurations may vary and use different tags that are not wanted for the production environment.
This directory contains scripts to setup swarm, k8s, and other environments. There is also examples to demonstrate how to use Dozzle with different environments. I use these examples to test and demonstrate the functionality of Dozzle.
For official documentation, please visit [https://dozzle.dev](https://dozzle.dev).
+18
View File
@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dozzle-ingress
annotations:
ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host: dozzle.k3d.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dozzle-service
port:
number: 8080
+74
View File
@@ -0,0 +1,74 @@
# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-viewer
---
# clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-viewer-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
---
# clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pod-viewer-binding
subjects:
- kind: ServiceAccount
name: pod-viewer
namespace: default
roleRef:
kind: ClusterRole
name: pod-viewer-role
apiGroup: rbac.authorization.k8s.io
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
replicas: 1
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
imagePullPolicy: Never
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
- name: DOZZLE_LEVEL
value: "debug"
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: dozzle-service
spec:
type: ClusterIP
selector:
app: dozzle
ports:
- port: 8080
targetPort: 8080
protocol: TCP
+1 -1
View File
@@ -2,4 +2,4 @@ users:
amir:
email: ""
name: Amir
password: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
password: $2a$11$nXizfBMJrSwfo4ofkS62denLyCKX0X7VsmfTbMyD3thTkmuGNp8.m
+52 -21
View File
@@ -4,7 +4,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alexflint/go-arg v1.5.1
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/docker v27.5.1+incompatible
github.com/docker/docker v28.0.1+incompatible
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1
@@ -18,24 +18,28 @@ require (
github.com/spf13/afero v1.12.0
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0
golang.org/x/net v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/PuerkitoBio/goquery v1.10.1
github.com/PuerkitoBio/goquery v1.10.2
github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/jwtauth/v5 v5.3.2
github.com/puzpuzpuz/xsync/v3 v3.5.0
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.49.1
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/yuin/goldmark v1.7.8
golang.org/x/crypto v0.33.0
golang.org/x/sync v0.11.0
google.golang.org/grpc v1.70.0
golang.org/x/crypto v0.35.0
golang.org/x/sync v0.12.0
google.golang.org/grpc v1.71.0
google.golang.org/protobuf v1.36.5
k8s.io/api v0.32.2
k8s.io/apimachinery v0.32.2
k8s.io/client-go v0.32.2
k8s.io/metrics v0.32.2
)
require (
@@ -45,36 +49,63 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/segmentio/asm v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
golang.org/x/time v0.10.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gotest.tools/v3 v3.0.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
go 1.23.6
go 1.24.1
+127 -118
View File
@@ -2,16 +2,12 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -27,38 +23,31 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.4.0+incompatible h1:I9z7sQ5qyzO0BfAb9IMOawRkAGxhYsidKiTMcm0DU+A=
github.com/docker/docker v27.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U=
github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM=
github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
github.com/go-chi/jwtauth/v5 v5.3.2 h1:s+ON3ATyyMs3Me0kqyuua6Rwu+2zqIIkL0GCaMarwvs=
github.com/go-chi/jwtauth/v5 v5.3.2/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -66,25 +55,46 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -101,14 +111,13 @@ github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -117,8 +126,19 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -126,23 +146,16 @@ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2sz
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.4.1 h1:wWXLKXwzpsduC3kUSahiL45MWxkGb+AQG0dsri4iftA=
github.com/puzpuzpuz/xsync/v3 v3.4.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/samber/lo v1.48.0 h1:ELOfcaM7vdYPe0egBS2Nxa8LxkY4lR+9LBzj0l6cHJ0=
github.com/samber/lo v1.48.0/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/samber/lo v1.49.0 h1:AGnTnQrg1jpFuwECPUSoxZCfVH5W22b605kWSry3YxM=
github.com/samber/lo v1.49.0/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
@@ -151,45 +164,48 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -199,16 +215,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA=
golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -224,15 +237,15 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -241,12 +254,11 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -258,29 +270,24 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -289,14 +296,11 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -306,46 +310,31 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI=
google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -354,3 +343,23 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
k8s.io/metrics v0.32.2 h1:7t/rZzTHFrGa9f94XcgLlm3ToAuJtdlHANcJEHlYl9g=
k8s.io/metrics v0.32.2/go.mod h1:VL3nJpzcgB6L5nSljkkzoE0nilZhVgcjCfNRgoylaIQ=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+3 -3
View File
@@ -306,12 +306,12 @@ func (c *Client) FindContainer(ctx context.Context, containerID string) (contain
}, nil
}
func (c *Client) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
func (c *Client) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
in := &pb.ListContainersRequest{}
if filter != nil {
if labels != nil {
in.Filter = make(map[string]*pb.RepeatedString)
for k, v := range filter {
for k, v := range labels {
in.Filter[k] = &pb.RepeatedString{Values: v}
}
}
+3 -3
View File
@@ -46,7 +46,7 @@ func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- contai
return args.Error(0)
}
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
args := m.Called(ctx, filter)
return args.Get(0).([]container.Container), args.Error(1)
}
@@ -130,7 +130,7 @@ func init() {
FinishedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
}, nil)
server, _ := NewServer(client, certs, "test", container.ContainerFilter{})
server, _ := NewServer(client, certs, "test", container.ContainerLabels{})
go server.Serve(lis)
}
@@ -173,7 +173,7 @@ func TestListContainers(t *testing.T) {
t.Fatal(err)
}
containers, _ := rpc.ListContainers(context.Background(), container.ContainerFilter{})
containers, _ := rpc.ListContainers(context.Background(), container.ContainerLabels{})
assert.Equal(t, containers, []container.Container{
{
+66 -81
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc-gen-go v1.36.2
// protoc v5.29.3
// source: rpc.proto
@@ -22,11 +22,10 @@ const (
)
type ListContainersRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Filter map[string]*RepeatedString `protobuf:"bytes,1,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
Filter map[string]*RepeatedString `protobuf:"bytes,1,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
sizeCache protoimpl.SizeCache
}
func (x *ListContainersRequest) Reset() {
@@ -67,11 +66,10 @@ func (x *ListContainersRequest) GetFilter() map[string]*RepeatedString {
}
type RepeatedString struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
unknownFields protoimpl.UnknownFields
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RepeatedString) Reset() {
@@ -112,11 +110,10 @@ func (x *RepeatedString) GetValues() []string {
}
type ListContainersResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"`
unknownFields protoimpl.UnknownFields
Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ListContainersResponse) Reset() {
@@ -157,12 +154,11 @@ func (x *ListContainersResponse) GetContainers() []*Container {
}
type FindContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Filter map[string]*RepeatedString `protobuf:"bytes,2,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Filter map[string]*RepeatedString `protobuf:"bytes,2,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
sizeCache protoimpl.SizeCache
}
func (x *FindContainerRequest) Reset() {
@@ -210,11 +206,10 @@ func (x *FindContainerRequest) GetFilter() map[string]*RepeatedString {
}
type FindContainerResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
unknownFields protoimpl.UnknownFields
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *FindContainerResponse) Reset() {
@@ -255,13 +250,12 @@ func (x *FindContainerResponse) GetContainer() *Container {
}
type StreamLogsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
StreamTypes int32 `protobuf:"varint,3,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
StreamTypes int32 `protobuf:"varint,3,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamLogsRequest) Reset() {
@@ -316,11 +310,10 @@ func (x *StreamLogsRequest) GetStreamTypes() int32 {
}
type StreamLogsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Event *LogEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
unknownFields protoimpl.UnknownFields
Event *LogEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamLogsResponse) Reset() {
@@ -361,14 +354,13 @@ func (x *StreamLogsResponse) GetEvent() *LogEvent {
}
type LogsBetweenDatesRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LogsBetweenDatesRequest) Reset() {
@@ -430,14 +422,13 @@ func (x *LogsBetweenDatesRequest) GetStreamTypes() int32 {
}
type StreamRawBytesRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamRawBytesRequest) Reset() {
@@ -499,11 +490,10 @@ func (x *StreamRawBytesRequest) GetStreamTypes() int32 {
}
type StreamRawBytesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamRawBytesResponse) Reset() {
@@ -544,9 +534,9 @@ func (x *StreamRawBytesResponse) GetData() []byte {
}
type StreamEventsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamEventsRequest) Reset() {
@@ -580,11 +570,10 @@ func (*StreamEventsRequest) Descriptor() ([]byte, []int) {
}
type StreamEventsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Event *ContainerEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
unknownFields protoimpl.UnknownFields
Event *ContainerEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamEventsResponse) Reset() {
@@ -625,9 +614,9 @@ func (x *StreamEventsResponse) GetEvent() *ContainerEvent {
}
type StreamStatsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamStatsRequest) Reset() {
@@ -661,11 +650,10 @@ func (*StreamStatsRequest) Descriptor() ([]byte, []int) {
}
type StreamStatsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Stat *ContainerStat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"`
unknownFields protoimpl.UnknownFields
Stat *ContainerStat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamStatsResponse) Reset() {
@@ -706,9 +694,9 @@ func (x *StreamStatsResponse) GetStat() *ContainerStat {
}
type HostInfoRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HostInfoRequest) Reset() {
@@ -742,11 +730,10 @@ func (*HostInfoRequest) Descriptor() ([]byte, []int) {
}
type HostInfoResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Host *Host `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
unknownFields protoimpl.UnknownFields
Host *Host `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *HostInfoResponse) Reset() {
@@ -787,9 +774,9 @@ func (x *HostInfoResponse) GetHost() *Host {
}
type StreamContainerStartedRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamContainerStartedRequest) Reset() {
@@ -823,11 +810,10 @@ func (*StreamContainerStartedRequest) Descriptor() ([]byte, []int) {
}
type StreamContainerStartedResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
unknownFields protoimpl.UnknownFields
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamContainerStartedResponse) Reset() {
@@ -868,12 +854,11 @@ func (x *StreamContainerStartedResponse) GetContainer() *Container {
}
type ContainerActionRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Action ContainerAction `protobuf:"varint,2,opt,name=action,proto3,enum=protobuf.ContainerAction" json:"action,omitempty"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Action ContainerAction `protobuf:"varint,2,opt,name=action,proto3,enum=protobuf.ContainerAction" json:"action,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ContainerActionRequest) Reset() {
@@ -921,9 +906,9 @@ func (x *ContainerActionRequest) GetAction() ContainerAction {
}
type ContainerActionResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ContainerActionResponse) Reset() {
+61 -68
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc-gen-go v1.36.2
// protoc v5.29.3
// source: types.proto
@@ -72,26 +72,25 @@ func (ContainerAction) EnumDescriptor() ([]byte, []int) {
}
type Container struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // deprecated
State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"`
ImageId string `protobuf:"bytes,6,opt,name=ImageId,proto3" json:"ImageId,omitempty"` // deprecated
Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"`
Started *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=started,proto3" json:"started,omitempty"`
Health string `protobuf:"bytes,9,opt,name=health,proto3" json:"health,omitempty"`
Host string `protobuf:"bytes,10,opt,name=host,proto3" json:"host,omitempty"`
Tty bool `protobuf:"varint,11,opt,name=tty,proto3" json:"tty,omitempty"`
Labels map[string]string `protobuf:"bytes,12,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Stats []*ContainerStat `protobuf:"bytes,13,rep,name=stats,proto3" json:"stats,omitempty"`
Group string `protobuf:"bytes,14,opt,name=group,proto3" json:"group,omitempty"`
Command string `protobuf:"bytes,15,opt,name=command,proto3" json:"command,omitempty"`
Finished *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=finished,proto3" json:"finished,omitempty"`
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // deprecated
State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"`
ImageId string `protobuf:"bytes,6,opt,name=ImageId,proto3" json:"ImageId,omitempty"` // deprecated
Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"`
Started *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=started,proto3" json:"started,omitempty"`
Health string `protobuf:"bytes,9,opt,name=health,proto3" json:"health,omitempty"`
Host string `protobuf:"bytes,10,opt,name=host,proto3" json:"host,omitempty"`
Tty bool `protobuf:"varint,11,opt,name=tty,proto3" json:"tty,omitempty"`
Labels map[string]string `protobuf:"bytes,12,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Stats []*ContainerStat `protobuf:"bytes,13,rep,name=stats,proto3" json:"stats,omitempty"`
Group string `protobuf:"bytes,14,opt,name=group,proto3" json:"group,omitempty"`
Command string `protobuf:"bytes,15,opt,name=command,proto3" json:"command,omitempty"`
Finished *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=finished,proto3" json:"finished,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *Container) Reset() {
@@ -237,14 +236,13 @@ func (x *Container) GetFinished() *timestamppb.Timestamp {
}
type ContainerStat struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ContainerStat) Reset() {
@@ -306,17 +304,16 @@ func (x *ContainerStat) GetMemoryPercent() float64 {
}
type LogEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=containerId,proto3" json:"containerId,omitempty"`
Message *anypb.Any `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Level string `protobuf:"bytes,5,opt,name=level,proto3" json:"level,omitempty"`
Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"`
Position string `protobuf:"bytes,7,opt,name=position,proto3" json:"position,omitempty"`
unknownFields protoimpl.UnknownFields
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=containerId,proto3" json:"containerId,omitempty"`
Message *anypb.Any `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Level string `protobuf:"bytes,5,opt,name=level,proto3" json:"level,omitempty"`
Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"`
Position string `protobuf:"bytes,7,opt,name=position,proto3" json:"position,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LogEvent) Reset() {
@@ -399,11 +396,10 @@ func (x *LogEvent) GetPosition() string {
}
type SimpleMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *SimpleMessage) Reset() {
@@ -444,11 +440,10 @@ func (x *SimpleMessage) GetMessage() string {
}
type ComplexMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ComplexMessage) Reset() {
@@ -489,14 +484,13 @@ func (x *ComplexMessage) GetData() []byte {
}
type ContainerEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorId string `protobuf:"bytes,1,opt,name=actorId,proto3" json:"actorId,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
unknownFields protoimpl.UnknownFields
ActorId string `protobuf:"bytes,1,opt,name=actorId,proto3" json:"actorId,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ContainerEvent) Reset() {
@@ -558,22 +552,21 @@ func (x *ContainerEvent) GetTimestamp() *timestamppb.Timestamp {
}
type Host struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
NodeAddress string `protobuf:"bytes,3,opt,name=nodeAddress,proto3" json:"nodeAddress,omitempty"`
Swarm bool `protobuf:"varint,4,opt,name=swarm,proto3" json:"swarm,omitempty"`
Labels map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
OperatingSystem string `protobuf:"bytes,6,opt,name=operatingSystem,proto3" json:"operatingSystem,omitempty"`
OsVersion string `protobuf:"bytes,7,opt,name=osVersion,proto3" json:"osVersion,omitempty"`
OsType string `protobuf:"bytes,8,opt,name=osType,proto3" json:"osType,omitempty"`
CpuCores uint32 `protobuf:"varint,9,opt,name=cpuCores,proto3" json:"cpuCores,omitempty"`
Memory uint64 `protobuf:"varint,10,opt,name=memory,proto3" json:"memory,omitempty"`
AgentVersion string `protobuf:"bytes,11,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"`
DockerVersion string `protobuf:"bytes,12,opt,name=dockerVersion,proto3" json:"dockerVersion,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
NodeAddress string `protobuf:"bytes,3,opt,name=nodeAddress,proto3" json:"nodeAddress,omitempty"`
Swarm bool `protobuf:"varint,4,opt,name=swarm,proto3" json:"swarm,omitempty"`
Labels map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
OperatingSystem string `protobuf:"bytes,6,opt,name=operatingSystem,proto3" json:"operatingSystem,omitempty"`
OsVersion string `protobuf:"bytes,7,opt,name=osVersion,proto3" json:"osVersion,omitempty"`
OsType string `protobuf:"bytes,8,opt,name=osType,proto3" json:"osType,omitempty"`
CpuCores uint32 `protobuf:"varint,9,opt,name=cpuCores,proto3" json:"cpuCores,omitempty"`
Memory uint64 `protobuf:"varint,10,opt,name=memory,proto3" json:"memory,omitempty"`
AgentVersion string `protobuf:"bytes,11,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"`
DockerVersion string `protobuf:"bytes,12,opt,name=dockerVersion,proto3" json:"dockerVersion,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Host) Reset() {
+17 -13
View File
@@ -13,6 +13,7 @@ import (
"github.com/amir20/dozzle/internal/agent/pb"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map/v2"
"google.golang.org/grpc"
@@ -32,12 +33,13 @@ type server struct {
pb.UnimplementedAgentServiceServer
}
func newServer(client container.Client, dozzleVersion string, filter container.ContainerFilter) pb.AgentServiceServer {
func newServer(client container.Client, dozzleVersion string, labels container.ContainerLabels) pb.AgentServiceServer {
statsCollector := docker.NewDockerStatsCollector(client, labels)
return &server{
client: client,
version: dozzleVersion,
store: container.NewContainerStore(context.Background(), client, filter),
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
}
}
@@ -47,7 +49,7 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
since = in.Since.AsTime()
}
c, err := s.store.FindContainer(in.ContainerId, container.ContainerFilter{})
c, err := s.store.FindContainer(in.ContainerId, container.ContainerLabels{})
if err != nil {
return err
}
@@ -57,7 +59,8 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
return err
}
g := container.NewEventGenerator(out.Context(), reader, c)
dockerReader := docker.NewLogReader(reader, c.Tty)
g := container.NewEventGenerator(out.Context(), dockerReader, c)
for event := range g.Events {
out.Send(&pb.StreamLogsResponse{
@@ -84,7 +87,8 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
return err
}
g := container.NewEventGenerator(out.Context(), reader, c)
dockerReader := docker.NewLogReader(reader, c.Tty)
g := container.NewEventGenerator(out.Context(), dockerReader, c)
for {
select {
@@ -173,14 +177,14 @@ func (s *server) StreamStats(in *pb.StreamStatsRequest, out pb.AgentService_Stre
}
func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest) (*pb.FindContainerResponse, error) {
filter := make(container.ContainerFilter)
labels := make(container.ContainerLabels)
if in.GetFilter() != nil {
for k, v := range in.GetFilter() {
filter[k] = append(filter[k], v.GetValues()...)
labels[k] = append(labels[k], v.GetValues()...)
}
}
container, err := s.store.FindContainer(in.ContainerId, filter)
container, err := s.store.FindContainer(in.ContainerId, labels)
if err != nil {
return nil, status.Error(codes.NotFound, err.Error())
}
@@ -205,14 +209,14 @@ func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest)
}
func (s *server) ListContainers(ctx context.Context, in *pb.ListContainersRequest) (*pb.ListContainersResponse, error) {
filter := make(container.ContainerFilter)
labels := make(container.ContainerLabels)
if in.GetFilter() != nil {
for k, v := range in.GetFilter() {
filter[k] = append(filter[k], v.GetValues()...)
labels[k] = append(labels[k], v.GetValues()...)
}
}
containers, err := s.store.ListContainers(filter)
containers, err := s.store.ListContainers(labels)
if err != nil {
return nil, err
}
@@ -322,7 +326,7 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
return &pb.ContainerActionResponse{}, nil
}
func NewServer(client container.Client, certificates tls.Certificate, dozzleVersion string, filter container.ContainerFilter) (*grpc.Server, error) {
func NewServer(client container.Client, certificates tls.Certificate, dozzleVersion string, labels container.ContainerLabels) (*grpc.Server, error) {
caCertPool := x509.NewCertPool()
c, err := x509.ParseCertificate(certificates.Certificate[0])
if err != nil {
@@ -341,7 +345,7 @@ func NewServer(client container.Client, certificates tls.Certificate, dozzleVers
creds := credentials.NewTLS(tlsConfig)
grpcServer := grpc.NewServer(grpc.Creds(creds))
pb.RegisterAgentServiceServer(grpcServer, newServer(client, dozzleVersion, filter))
pb.RegisterAgentServiceServer(grpcServer, newServer(client, dozzleVersion, labels))
return grpcServer, nil
}
+4 -4
View File
@@ -24,7 +24,7 @@ type User struct {
Name string `json:"name" yaml:"name"`
Password string `json:"-" yaml:"password"`
Filter string `json:"-" yaml:"filter"`
ContainerFilter container.ContainerFilter `json:"-" yaml:"-"`
ContainerLabels container.ContainerLabels `json:"-" yaml:"-"`
}
func (u User) AvatarURL() string {
@@ -35,12 +35,12 @@ func (u User) AvatarURL() string {
return fmt.Sprintf("https://gravatar.com/avatar/%s?d=https%%3A%%2F%%2Fui-avatars.com%%2Fapi%%2F/%s/128", hashEmail(u.Email), url.QueryEscape(name))
}
func newUser(username, email, name string, filter container.ContainerFilter) User {
func newUser(username, email, name string, labels container.ContainerLabels) User {
return User{
Username: username,
Email: email,
Name: name,
ContainerFilter: filter,
ContainerLabels: labels,
}
}
@@ -197,7 +197,7 @@ func UserFromContext(ctx context.Context) *User {
}
email := claims["email"].(string)
name := claims["name"].(string)
containerFilter := container.ContainerFilter{}
containerFilter := container.ContainerLabels{}
if filter, ok := claims["filter"].(string); ok {
containerFilter, err = container.ParseContainerFilter(filter)
if err != nil {
+1 -2
View File
@@ -29,7 +29,7 @@ func (s StdType) String() string {
}
type Client interface {
ListContainers(context.Context, ContainerFilter) ([]Container, error)
ListContainers(context.Context, ContainerLabels) ([]Container, error)
FindContainer(context.Context, string) (Container, error)
ContainerLogs(context.Context, string, time.Time, StdType) (io.ReadCloser, error)
ContainerEvents(context.Context, chan<- ContainerEvent) error
@@ -38,5 +38,4 @@ type Client interface {
Ping(context.Context) error
Host() Host
ContainerActions(ctx context.Context, action ContainerAction, containerID string) error
IsSwarmMode() bool
}
+109 -34
View File
@@ -13,34 +13,40 @@ import (
"golang.org/x/sync/semaphore"
)
type StatsCollector interface {
Start(parentCtx context.Context) bool
Subscribe(ctx context.Context, stats chan<- ContainerStat)
Stop()
}
type ContainerStore struct {
containers *xsync.MapOf[string, *Container]
subscribers *xsync.MapOf[context.Context, chan<- ContainerEvent]
newContainerSubscribers *xsync.MapOf[context.Context, chan<- Container]
client Client
statsCollector *StatsCollector
statsCollector StatsCollector
wg sync.WaitGroup
connected atomic.Bool
events chan ContainerEvent
ctx context.Context
filter ContainerFilter
labels ContainerLabels
}
const defaultTimeout = 10 * time.Second
func NewContainerStore(ctx context.Context, client Client, filter ContainerFilter) *ContainerStore {
log.Debug().Str("host", client.Host().Name).Interface("filter", filter).Msg("initializing container store")
func NewContainerStore(ctx context.Context, client Client, statsCollect StatsCollector, labels ContainerLabels) *ContainerStore {
log.Debug().Str("host", client.Host().Name).Interface("labels", labels).Msg("initializing container store")
s := &ContainerStore{
containers: xsync.NewMapOf[string, *Container](),
client: client,
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerEvent](),
newContainerSubscribers: xsync.NewMapOf[context.Context, chan<- Container](),
statsCollector: NewStatsCollector(client, filter),
statsCollector: statsCollect,
wg: sync.WaitGroup{},
events: make(chan ContainerEvent),
ctx: ctx,
filter: filter,
labels: labels,
}
s.wg.Add(1)
@@ -68,7 +74,7 @@ func (s *ContainerStore) checkConnectivity() error {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if containers, err := s.client.ListContainers(ctx, s.filter); err != nil {
if containers, err := s.client.ListContainers(ctx, s.labels); err != nil {
return err
} else {
s.containers.Clear()
@@ -78,7 +84,7 @@ func (s *ContainerStore) checkConnectivity() error {
}
running := lo.Filter(containers, func(item Container, index int) bool {
return item.State != "exited"
return item.State != "exited" && !item.FullyLoaded
})
sem := semaphore.NewWeighted(maxFetchParallelism)
@@ -109,7 +115,7 @@ func (s *ContainerStore) checkConnectivity() error {
return nil
}
func (s *ContainerStore) ListContainers(filter ContainerFilter) ([]Container, error) {
func (s *ContainerStore) ListContainers(labels ContainerLabels) ([]Container, error) {
s.wg.Wait()
if err := s.checkConnectivity(); err != nil {
@@ -117,12 +123,16 @@ func (s *ContainerStore) ListContainers(filter ContainerFilter) ([]Container, er
}
containers := make([]Container, 0)
if filter.Exists() {
validContainers, err := s.client.ListContainers(s.ctx, filter)
if labels.Exists() {
validContainers, err := s.client.ListContainers(s.ctx, labels)
if err != nil {
return nil, err
}
if len(validContainers) == 0 {
log.Warn().Interface("userLabels", labels).Msg("no containers found with user labels")
}
validIDMap := lo.KeyBy(validContainers, func(item Container) string {
return item.ID
})
@@ -143,11 +153,10 @@ func (s *ContainerStore) ListContainers(filter ContainerFilter) ([]Container, er
return containers, nil
}
func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Container, error) {
func (s *ContainerStore) FindContainer(id string, labels ContainerLabels) (Container, error) {
s.wg.Wait()
if filter.Exists() {
validContainers, err := s.client.ListContainers(s.ctx, filter)
if labels.Exists() {
validContainers, err := s.client.ListContainers(s.ctx, labels)
if err != nil {
return Container{}, err
}
@@ -163,31 +172,38 @@ func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Conta
}
if container, ok := s.containers.Load(id); ok {
if container.StartedAt.IsZero() {
log.Debug().Str("id", id).Msg("container doesn't have detailed information, fetching it")
if !container.FullyLoaded {
log.Debug().Str("id", id).Msg("container is not fully loaded, fetching it")
if newContainer, ok := s.containers.Compute(id, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if newContainer, err := s.client.FindContainer(ctx, id); err == nil {
return &newContainer, false
} else {
log.Error().Err(err).Msg("failed to fetch container")
return c, false
}
}
return c, false
}); ok {
event := ContainerEvent{
Name: "update",
Host: s.client.Host().ID,
ActorID: id,
}
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
select {
case events <- event:
case <-c.Done():
s.subscribers.Delete(c)
go func() {
event := ContainerEvent{
Name: "update",
Host: newContainer.Host,
ActorID: id,
Container: newContainer,
}
return true
})
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
select {
case events <- event:
case <-c.Done():
s.subscribers.Delete(c)
}
return true
})
}()
return *newContainer, nil
}
}
@@ -243,13 +259,37 @@ func (s *ContainerStore) init() {
for {
select {
case event := <-s.events:
log.Trace().Str("event", event.Name).Str("id", event.ActorID).Msg("received container event")
log.Debug().Str("event", event.Name).Str("id", event.ActorID).Msg("received container event")
switch event.Name {
case "start":
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
case "create":
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if container, err := s.client.FindContainer(ctx, event.ActorID); err == nil {
list, _ := s.client.ListContainers(ctx, s.filter)
list, _ := s.client.ListContainers(ctx, s.labels)
// make sure the container is in the list of containers when using filter
valid := lo.ContainsBy(list, func(item Container) bool {
return item.ID == container.ID
})
if valid {
s.containers.Store(container.ID, &container)
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
select {
case containers <- container:
case <-c.Done():
}
return true
})
}
}
cancel()
case "start":
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
if container, err := s.client.FindContainer(ctx, event.ActorID); err == nil {
list, _ := s.client.ListContainers(ctx, s.labels)
// make sure the container is in the list of containers when using filter
valid := lo.ContainsBy(list, func(item Container) bool {
@@ -257,7 +297,6 @@ func (s *ContainerStore) init() {
})
if valid {
log.Debug().Str("id", container.ID).Msg("container started")
s.containers.Store(container.ID, &container)
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
select {
@@ -273,6 +312,42 @@ func (s *ContainerStore) init() {
log.Debug().Str("id", event.ActorID).Msg("container destroyed")
s.containers.Delete(event.ActorID)
case "update":
started := false
updatedContainer, _ := s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
newContainer := event.Container
if newContainer.State == "running" && c.State != "running" {
started = true
}
c.Name = newContainer.Name
c.State = newContainer.State
c.Labels = newContainer.Labels
c.StartedAt = newContainer.StartedAt
c.FinishedAt = newContainer.FinishedAt
c.Created = newContainer.Created
c.Host = newContainer.Host
return c, false
} else {
return c, true
}
})
if started {
s.subscribers.Range(func(ctx context.Context, events chan<- ContainerEvent) bool {
select {
case events <- ContainerEvent{
Name: "start",
ActorID: updatedContainer.ID,
Host: updatedContainer.Host,
}:
case <-ctx.Done():
s.subscribers.Delete(ctx)
}
return true
})
}
case "die":
s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
+49 -48
View File
@@ -14,7 +14,7 @@ type mockedClient struct {
Client
}
func (m *mockedClient) ListContainers(ctx context.Context, filter ContainerFilter) ([]Container, error) {
func (m *mockedClient) ListContainers(ctx context.Context, filter ContainerLabels) ([]Container, error) {
args := m.Called(ctx, filter)
return args.Get(0).([]Container), args.Error(1)
}
@@ -63,60 +63,61 @@ func TestContainerStore_List(t *testing.T) {
Stats: utils.NewRingBuffer[ContainerStat](300),
}, nil)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
store := NewContainerStore(ctx, client, ContainerFilter{})
containers, _ := store.ListContainers(ContainerFilter{})
collector := &fakeStatsCollector{}
store := NewContainerStore(t.Context(), client, collector, ContainerLabels{})
containers, _ := store.ListContainers(ContainerLabels{})
assert.Equal(t, containers[0].ID, "1234")
}
//TODO fix this test
// func TestContainerStore_die(t *testing.T) {
// client := new(mockedClient)
// client.On("ListContainers", mock.Anything, mock.Anything).Return([]Container{
// {
// ID: "1234",
// Name: "test",
// State: "running",
// Stats: utils.NewRingBuffer[ContainerStat](300),
// },
// }, nil)
func TestContainerStore_die(t *testing.T) {
client := new(mockedClient)
client.On("ListContainers", mock.Anything, mock.Anything).Return([]Container{
{
ID: "1234",
Name: "test",
State: "running",
Stats: utils.NewRingBuffer[ContainerStat](300),
},
}, nil)
// client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).
// Run(func(args mock.Arguments) {
// ctx := args.Get(0).(context.Context)
// events := args.Get(1).(chan<- ContainerEvent)
// events <- ContainerEvent{
// Name: "die",
// ActorID: "1234",
// Host: "localhost",
// }
// <-ctx.Done()
// })
// client.On("Host").Return(Host{
// ID: "localhost",
// })
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).
Run(func(args mock.Arguments) {
ctx := args.Get(0).(context.Context)
events := args.Get(1).(chan<- ContainerEvent)
events <- ContainerEvent{
Name: "die",
ActorID: "1234",
Host: "localhost",
}
<-ctx.Done()
})
client.On("Host").Return(Host{
ID: "localhost",
})
// client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- container.ContainerStat")).Return(nil)
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- container.ContainerStat")).Return(nil)
// client.On("FindContainer", mock.Anything, "1234").Return(Container{
// ID: "1234",
// Name: "test",
// Image: "test",
// Stats: utils.NewRingBuffer[ContainerStat](300),
// }, nil)
client.On("FindContainer", mock.Anything, "1234").Return(Container{
ID: "1234",
Name: "test",
Image: "test",
Stats: utils.NewRingBuffer[ContainerStat](300),
}, nil)
// ctx, cancel := context.WithCancel(context.Background())
// t.Cleanup(cancel)
// store := NewContainerStore(ctx, client, ContainerFilter{})
store := NewContainerStore(t.Context(), client, &fakeStatsCollector{}, ContainerLabels{})
// // Wait until we get the event
// events := make(chan ContainerEvent)
// store.SubscribeEvents(ctx, events)
// <-events
// Wait until we get the event
events := make(chan ContainerEvent)
store.SubscribeEvents(t.Context(), events)
<-events
// containers, _ := store.ListContainers(ContainerFilter{})
// assert.Equal(t, containers[0].State, "exited")
// }
containers, _ := store.ListContainers(ContainerLabels{})
assert.Equal(t, containers[0].State, "exited")
}
type fakeStatsCollector struct{}
func (f *fakeStatsCollector) Subscribe(_ context.Context, _ chan<- ContainerStat) {}
func (f *fakeStatsCollector) Start(_ context.Context) bool { return true }
func (f *fakeStatsCollector) Stop() {}
+8 -60
View File
@@ -1,14 +1,10 @@
package container
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"hash/fnv"
"io"
"strings"
"sync"
"time"
@@ -23,30 +19,26 @@ import (
type EventGenerator struct {
Events chan *LogEvent
Errors chan error
reader *bufio.Reader
reader LogReader
next *LogEvent
buffer chan *LogEvent
tty bool
wg sync.WaitGroup
containerID string
ctx context.Context
}
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
var ErrBadHeader = fmt.Errorf("dozzle/docker: unable to read header")
func NewEventGenerator(ctx context.Context, reader io.Reader, container Container) *EventGenerator {
type LogReader interface {
Read() (string, StdType, error)
}
func NewEventGenerator(ctx context.Context, reader LogReader, container Container) *EventGenerator {
generator := &EventGenerator{
reader: bufio.NewReader(reader),
reader: reader,
buffer: make(chan *LogEvent, 100),
Errors: make(chan error, 1),
Events: make(chan *LogEvent),
tty: container.Tty,
containerID: container.ID,
ctx: ctx,
}
@@ -90,7 +82,7 @@ loop:
func (g *EventGenerator) consumeReader() {
for {
message, streamType, readerError := readEvent(g.reader, g.tty)
message, streamType, readerError := g.reader.Read()
if message != "" {
logEvent := createEvent(message, streamType)
logEvent.ContainerID = g.containerID
@@ -123,50 +115,6 @@ func (g *EventGenerator) peek() *LogEvent {
}
}
func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
header := []byte{0, 0, 0, 0, 0, 0, 0, 0}
buffer := bufPool.Get().(*bytes.Buffer)
buffer.Reset()
defer bufPool.Put(buffer)
var streamType StdType = STDOUT
if tty {
message, err := reader.ReadString('\n')
if err != nil {
return message, streamType, err
}
return message, streamType, nil
} else {
n, err := io.ReadFull(reader, header)
if err != nil {
return "", streamType, err
}
if n != 8 {
log.Warn().Bytes("header", header).Msg("short read")
message, _ := reader.ReadString('\n')
return message, streamType, ErrBadHeader
}
switch header[0] {
case 1:
streamType = STDOUT
case 2:
streamType = STDERR
default:
log.Warn().Bytes("header", header).Msg("unknown stream type")
}
count := binary.BigEndian.Uint32(header[4:])
if count == 0 {
return "", streamType, nil
}
_, err = io.CopyN(buffer, reader, int64(count))
if err != nil {
return "", streamType, err
}
return buffer.String(), streamType, nil
}
}
func createEvent(message string, streamType StdType) *LogEvent {
h := fnv.New32a()
h.Write([]byte(message))
+23 -38
View File
@@ -1,12 +1,9 @@
package container
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"io"
"reflect"
"strings"
"sync"
"testing"
"time"
@@ -18,9 +15,8 @@ import (
func TestEventGenerator_Events_tty(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
g := NewEventGenerator(context.Background(), reader, Container{Tty: true})
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: true})
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil, but got nil")
@@ -29,9 +25,8 @@ func TestEventGenerator_Events_tty(t *testing.T) {
func TestEventGenerator_Events_non_tty(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil, but got nil")
@@ -40,9 +35,8 @@ func TestEventGenerator_Events_non_tty(t *testing.T) {
func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
<-g.Events
_, ok := <-g.Events
@@ -51,20 +45,31 @@ func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) {
func TestEventGenerator_Events_routines_done(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
<-g.Events
assert.False(t, waitTimeout(&g.wg, 1*time.Second), "Expected routines to be done")
}
func makeMessage(message string, stream StdType) []byte {
data := make([]byte, 8)
binary.BigEndian.PutUint32(data[4:], uint32(len(message)))
data[0] = byte(stream / 2)
data = append(data, []byte(message)...)
type mockLogReader struct {
messages []string
types []StdType
i int
}
return data
func (m *mockLogReader) Read() (string, StdType, error) {
if m.i >= len(m.messages) {
return "", 0, io.EOF
}
m.i++
return m.messages[m.i-1], m.types[m.i-1], nil
}
func makeFakeReader(message string, stream StdType) LogReader {
return &mockLogReader{
messages: []string{message},
types: []StdType{stream},
}
}
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
@@ -156,23 +161,3 @@ func Test_createEvent(t *testing.T) {
})
}
}
type mockReadCloser struct {
bytes []byte
}
func (m mockReadCloser) Read(p []byte) (int, error) {
return copy(p, m.bytes), nil
}
func Benchmark_readEvent(b *testing.B) {
b.ReportAllocs()
data := makeMessage("2020-05-13T18:55:37.772853839Z {\"key\": \"value\"}\n", STDOUT)
reader := bufio.NewReader(mockReadCloser{bytes: data})
for i := 0; i < b.N; i++ {
readEvent(reader, true)
}
}
+16 -20
View File
@@ -20,23 +20,18 @@ var logLevels = [][]string{
{"fatal", "sev", "severe", "crit", "critical"},
}
var plainLevels = map[string][]*regexp.Regexp{}
var bracketLevels = map[string][]*regexp.Regexp{}
var plainLevels = map[string]*regexp.Regexp{}
var bracketLevels = map[string]*regexp.Regexp{}
var separatorLevels = map[string]*regexp.Regexp{}
var timestampRegex = regexp.MustCompile(`^(?:\d{4}[-/]\d{2}[-/]\d{2}(?:[T ](?:\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?|\d{2}:\d{2}(?:AM|PM)))?\s+)`)
func init() {
for _, levelGroup := range logLevels {
first := levelGroup[0]
for _, level := range levelGroup {
plainLevels[first] = append(plainLevels[first], regexp.MustCompile("(?i)^"+level+"[^a-z]"))
}
}
for _, levelGroup := range logLevels {
first := levelGroup[0]
for _, level := range levelGroup {
bracketLevels[first] = append(bracketLevels[first], regexp.MustCompile("(?i)\\[ ?"+level+" ?\\]"))
}
levelsGroup := "(?:" + strings.Join(levelGroup, "|") + ")"
plainLevels[first] = regexp.MustCompile("(?i)^" + levelsGroup + "[^a-z]")
bracketLevels[first] = regexp.MustCompile("(?i)\\[ ?" + levelsGroup + " ?\\]")
separatorLevels[first] = regexp.MustCompile("(?i) " + levelsGroup + "[/-]")
}
SupportedLogLevels = make(map[string]struct{}, len(logLevels)+1)
@@ -54,17 +49,18 @@ func guessLogLevel(logEvent *LogEvent) string {
for _, levelGroup := range logLevels {
first := levelGroup[0]
// Look for the level at the beginning of the message
for _, regex := range plainLevels[first] {
if regex.MatchString(value) {
return first
}
if plainLevels[first].MatchString(value) {
return first
}
// Look for the level in brackets
for _, regex := range bracketLevels[first] {
if regex.MatchString(value) {
return first
}
if bracketLevels[first].MatchString(value) {
return first
}
// Look for the level with a separator after
if separatorLevels[first].MatchString(value) {
return first
}
// Look for the level in the middle of the message that are uppercase and surrounded by quotes
+3
View File
@@ -14,6 +14,8 @@ func TestGuessLogLevel(t *testing.T) {
expected string
}{
{"2024/12/30 12:21AM INF this is a test", "info"},
{"2025-01-07 22:00:08,059: DEBUG/MainProcess TaskPool: ", "debug"},
{"Some test with error-test", "error"},
{"2024-12-30T17:43:16Z DBG loggging debug from here", "debug"},
{"2025-01-07 15:40:15,784 LL=\"ERROR\" some message", "error"},
{"2025-01-07 15:40:15,784 LL=\"WARN\" some message", "warn"},
@@ -35,6 +37,7 @@ func TestGuessLogLevel(t *testing.T) {
{"123 ERROR Something went wrong", "error"},
{"123 Something went wrong", "unknown"},
{"DBG Something went wrong", "debug"},
{"DBG with more error=msg", "debug"},
{"inf Something went wrong", "info"},
{"crit: Something went wrong", "fatal"},
{orderedmap.New[string, string](
+20 -18
View File
@@ -11,20 +11,21 @@ import (
// Container represents an internal representation of docker containers
type Container struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Command string `json:"command"`
Created time.Time `json:"created"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"`
State string `json:"state"`
Health string `json:"health,omitempty"`
Host string `json:"host,omitempty"`
Tty bool `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
Group string `json:"group,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Command string `json:"command"`
Created time.Time `json:"created"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"`
State string `json:"state"`
Health string `json:"health,omitempty"`
Host string `json:"host,omitempty"`
Tty bool `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
Group string `json:"group,omitempty"`
FullyLoaded bool `json:"-,omitempty"`
}
// ContainerStat represent stats instant for a container
@@ -42,12 +43,13 @@ type ContainerEvent struct {
ActorID string `json:"actorId"`
ActorAttributes map[string]string `json:"actorAttributes,omitempty"`
Time time.Time `json:"time"`
Container *Container `json:"-"`
}
type ContainerFilter map[string][]string
type ContainerLabels map[string][]string
func ParseContainerFilter(commaValues string) (ContainerFilter, error) {
filter := make(ContainerFilter)
func ParseContainerFilter(commaValues string) (ContainerLabels, error) {
filter := make(ContainerLabels)
if commaValues == "" {
return filter, nil
}
@@ -65,7 +67,7 @@ func ParseContainerFilter(commaValues string) (ContainerFilter, error) {
return filter, nil
}
func (f ContainerFilter) Exists() bool {
func (f ContainerLabels) Exists() bool {
return len(f) > 0
}
+32 -36
View File
@@ -17,7 +17,6 @@ import (
docker "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
@@ -25,10 +24,10 @@ import (
)
type DockerCLI interface {
ContainerList(context.Context, docker.ListOptions) ([]types.Container, error)
ContainerList(context.Context, docker.ListOptions) ([]docker.Summary, error)
ContainerLogs(context.Context, string, docker.LogsOptions) (io.ReadCloser, error)
Events(context.Context, events.ListOptions) (<-chan events.Message, <-chan error)
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
ContainerInspect(ctx context.Context, containerID string) (docker.InspectResponse, error)
ContainerStats(ctx context.Context, containerID string, stream bool) (docker.StatsResponseReader, error)
Ping(ctx context.Context) (types.Ping, error)
ContainerStart(ctx context.Context, containerID string, options docker.StartOptions) error
@@ -37,13 +36,13 @@ type DockerCLI interface {
Info(ctx context.Context) (system.Info, error)
}
type httpClient struct {
type DockerClient struct {
cli DockerCLI
host container.Host
info system.Info
}
func NewClient(cli DockerCLI, host container.Host) container.Client {
func NewClient(cli DockerCLI, host container.Host) *DockerClient {
info, err := cli.Info(context.Background())
if err != nil {
log.Error().Err(err).Msg("Failed to get docker info")
@@ -60,15 +59,15 @@ func NewClient(cli DockerCLI, host container.Host) container.Client {
host.DockerVersion = info.ServerVersion
host.Swarm = info.Swarm.NodeID != ""
return &httpClient{
return &DockerClient{
cli: cli,
host: host,
info: info,
}
}
// NewClientWithFilters creates a new instance of Client with docker filters
func NewLocalClient(hostname string) (container.Client, error) {
// NewLocalClient creates a new instance of Client with docker filters
func NewLocalClient(hostname string) (*DockerClient, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation(), client.WithUserAgent("Docker-Client/Dozzle"))
if err != nil {
@@ -93,7 +92,7 @@ func NewLocalClient(hostname string) (container.Client, error) {
return NewClient(cli, host), nil
}
func NewRemoteClient(host container.Host) (container.Client, error) {
func NewRemoteClient(host container.Host) (*DockerClient, error) {
if host.URL.Scheme != "tcp" {
return nil, fmt.Errorf("invalid scheme: %s", host.URL.Scheme)
}
@@ -123,7 +122,7 @@ func NewRemoteClient(host container.Host) (container.Client, error) {
}
// Finds a container by id, skipping the filters
func (d *httpClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
func (d *DockerClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
log.Debug().Str("id", id).Msg("Finding container")
if json, err := d.cli.ContainerInspect(ctx, id); err == nil {
return newContainerFromJSON(json, d.host.ID), nil
@@ -133,7 +132,7 @@ func (d *httpClient) FindContainer(ctx context.Context, id string) (container.Co
}
func (d *httpClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
func (d *DockerClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
switch action {
case container.Start:
return d.cli.ContainerStart(ctx, containerID, docker.StartOptions{})
@@ -146,10 +145,10 @@ func (d *httpClient) ContainerActions(ctx context.Context, action container.Cont
}
}
func (d *httpClient) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
log.Debug().Interface("filter", filter).Str("host", d.host.Name).Msg("Listing containers")
func (d *DockerClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
log.Debug().Interface("labels", labels).Str("host", d.host.Name).Msg("Listing containers")
filterArgs := filters.NewArgs()
for key, values := range filter {
for key, values := range labels {
for _, value := range values {
filterArgs.Add(key, value)
}
@@ -175,7 +174,7 @@ func (d *httpClient) ListContainers(ctx context.Context, filter container.Contai
return containers, nil
}
func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
func (d *DockerClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
response, err := d.cli.ContainerStats(ctx, id, true)
if err != nil {
@@ -225,7 +224,7 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
}
}
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
func (d *DockerClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
log.Debug().Str("id", id).Time("since", since).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Streaming logs for container")
sinceQuery := since.Add(-50 * time.Millisecond).Format(time.RFC3339Nano)
@@ -246,7 +245,7 @@ func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Ti
return reader, nil
}
func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- container.ContainerEvent) error {
func (d *DockerClient) ContainerEvents(ctx context.Context, messages chan<- container.ContainerEvent) error {
dockerMessages, err := d.cli.Events(ctx, events.ListOptions{})
for {
@@ -270,7 +269,7 @@ func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- contai
}
}
func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
func (d *DockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
log.Debug().Str("id", id).Time("from", from).Time("to", to).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Fetching logs between dates for container")
options := docker.LogsOptions{
ShowStdout: stdType&container.STDOUT != 0,
@@ -288,21 +287,17 @@ func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, f
return reader, nil
}
func (d *httpClient) Ping(ctx context.Context) error {
func (d *DockerClient) Ping(ctx context.Context) error {
_, err := d.cli.Ping(ctx)
return err
}
func (d *httpClient) Host() container.Host {
func (d *DockerClient) Host() container.Host {
log.Debug().Str("host", d.host.Name).Msg("Fetching host")
return d.host
}
func (d *httpClient) IsSwarmMode() bool {
return d.info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive
}
func newContainer(c types.Container, host string) container.Container {
func newContainer(c docker.Summary, host string) container.Container {
name := "no name"
if c.Labels["dev.dozzle.name"] != "" {
name = c.Labels["dev.dozzle.name"]
@@ -328,7 +323,7 @@ func newContainer(c types.Container, host string) container.Container {
}
}
func newContainerFromJSON(c types.ContainerJSON, host string) container.Container {
func newContainerFromJSON(c docker.InspectResponse, host string) container.Container {
name := "no name"
if c.Config.Labels["dev.dozzle.name"] != "" {
name = c.Config.Labels["dev.dozzle.name"]
@@ -342,16 +337,17 @@ func newContainerFromJSON(c types.ContainerJSON, host string) container.Containe
}
container := container.Container{
ID: c.ID[:12],
Name: name,
Image: c.Config.Image,
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
State: c.State.Status,
Host: host,
Labels: c.Config.Labels,
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
Group: group,
Tty: c.Config.Tty,
ID: c.ID[:12],
Name: name,
Image: c.Config.Image,
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
State: c.State.Status,
Host: host,
Labels: c.Config.Labels,
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
Group: group,
Tty: c.Config.Tty,
FullyLoaded: true,
}
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
+24 -26
View File
@@ -11,7 +11,6 @@ import (
"testing"
"github.com/amir20/dozzle/internal/container"
"github.com/docker/docker/api/types"
docker "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/system"
"github.com/stretchr/testify/assert"
@@ -24,14 +23,13 @@ type mockedProxy struct {
DockerCLI
}
func (m *mockedProxy) ContainerList(context.Context, docker.ListOptions) ([]types.Container, error) {
func (m *mockedProxy) ContainerList(context.Context, docker.ListOptions) ([]docker.Summary, error) {
args := m.Called()
containers, ok := args.Get(0).([]types.Container)
containers, ok := args.Get(0).([]docker.Summary)
if !ok && args.Get(0) != nil {
panic("containers is not of type []types.Container")
panic("containers is not of type []docker.Summary")
}
return containers, args.Error(1)
}
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options docker.LogsOptions) (io.ReadCloser, error) {
@@ -43,9 +41,9 @@ func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options dock
return reader, args.Error(1)
}
func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string) (docker.InspectResponse, error) {
args := m.Called(ctx, containerID)
return args.Get(0).(types.ContainerJSON), args.Error(1)
return args.Get(0).(docker.InspectResponse), args.Error(1)
}
func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, stream bool) (docker.StatsResponseReader, error) {
@@ -90,9 +88,9 @@ func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string,
func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), container.ContainerFilter{})
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
assert.Empty(t, list, "list should be empty")
require.NoError(t, err, "error should not return an error.")
@@ -102,9 +100,9 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
func Test_dockerClient_ListContainers_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), container.ContainerFilter{})
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
assert.Nil(t, list, "list should be nil")
require.Error(t, err, "test.")
@@ -112,7 +110,7 @@ func Test_dockerClient_ListContainers_error(t *testing.T) {
}
func Test_dockerClient_ListContainers_happy(t *testing.T) {
containers := []types.Container{
containers := []docker.Summary{
{
ID: "abcdefghijklmnopqrst",
Names: []string{"/z_test_container"},
@@ -125,9 +123,9 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), container.ContainerFilter{})
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
require.NoError(t, err, "error should not return an error.")
Ids := []string{"1234567890_a", "abcdefghijkl"}
@@ -159,7 +157,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
Since: "2020-12-31T23:59:59.95Z"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
logReader, _ := client.ContainerLogs(context.Background(), id, since, container.STDALL)
actual, _ := io.ReadAll(logReader)
@@ -173,7 +171,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
reader, err := client.ContainerLogs(context.Background(), id, time.Time{}, container.STDALL)
@@ -185,11 +183,11 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
func Test_dockerClient_FindContainer_happy(t *testing.T) {
proxy := new(mockedProxy)
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &docker.Config{Tty: false}}
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := docker.InspectResponse{ContainerJSONBase: &docker.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &docker.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
container, err := client.FindContainer(context.Background(), "abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
@@ -201,8 +199,8 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
func Test_dockerClient_FindContainer_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(types.ContainerJSON{}, errors.New("not found"))
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(docker.InspectResponse{}, errors.New("not found"))
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
_, err := client.FindContainer(context.Background(), "not_valid")
require.Error(t, err, "error should be thrown")
@@ -212,10 +210,10 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
func Test_dockerClient_ContainerActions_happy(t *testing.T) {
proxy := new(mockedProxy)
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &docker.Config{Tty: false}}
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := docker.InspectResponse{ContainerJSONBase: &docker.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &docker.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
proxy.On("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
@@ -240,8 +238,8 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
func Test_dockerClient_ContainerActions_error(t *testing.T) {
proxy := new(mockedProxy)
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
proxy.On("ContainerInspect", mock.Anything, "random-id").Return(types.ContainerJSON{}, errors.New("not found"))
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
proxy.On("ContainerInspect", mock.Anything, "random-id").Return(docker.InspectResponse{}, errors.New("not found"))
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerStop", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerRestart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
+101
View File
@@ -0,0 +1,101 @@
package docker
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"io"
"sync"
"github.com/amir20/dozzle/internal/container"
)
var ErrBadHeader = errors.New("bad header")
type StdType int
const (
stdout StdType = iota
stderr
)
type LogReader struct {
reader *bufio.Reader
tty bool
pool *sync.Pool
}
func NewLogReader(r io.Reader, tty bool) *LogReader {
return &LogReader{
reader: bufio.NewReader(r),
tty: tty,
pool: &sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 4096))
},
},
}
}
func (d *LogReader) Read() (string, container.StdType, error) {
message, stdType, err := d.readEvent()
if err != nil {
return "", 0, err
}
var std container.StdType
switch stdType {
case stdout:
std = container.STDOUT
case stderr:
std = container.STDERR
}
return message, std, nil
}
func (d *LogReader) readEvent() (string, StdType, error) {
header := []byte{0, 0, 0, 0, 0, 0, 0, 0}
buffer := d.pool.Get().(*bytes.Buffer)
buffer.Reset()
defer d.pool.Put(buffer)
var streamType StdType = stdout
if d.tty {
message, err := d.reader.ReadString('\n')
if err != nil {
return message, streamType, err
}
return message, streamType, nil
} else {
n, err := io.ReadFull(d.reader, header)
if err != nil {
return "", streamType, err
}
if n != 8 {
message, _ := d.reader.ReadString('\n')
return message, streamType, ErrBadHeader
}
switch header[0] {
case 1:
streamType = stdout
case 2:
streamType = stderr
}
count := binary.BigEndian.Uint32(header[4:])
if count == 0 {
return "", streamType, nil
}
_, err = io.CopyN(buffer, d.reader, int64(count))
if err != nil {
return "", streamType, err
}
return buffer.String(), streamType, nil
}
}
@@ -1,4 +1,4 @@
package container
package docker
import (
"context"
@@ -8,35 +8,36 @@ import (
"sync/atomic"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog/log"
)
type StatsCollector struct {
stream chan ContainerStat
subscribers *xsync.MapOf[context.Context, chan<- ContainerStat]
client Client
type DockerStatsCollector struct {
stream chan container.ContainerStat
subscribers *xsync.MapOf[context.Context, chan<- container.ContainerStat]
client container.Client
cancelers *xsync.MapOf[string, context.CancelFunc]
stopper context.CancelFunc
timer *time.Timer
mu sync.Mutex
totalStarted atomic.Int32
filter ContainerFilter
labels container.ContainerLabels
}
var timeToStop = 6 * time.Hour
func NewStatsCollector(client Client, filter ContainerFilter) *StatsCollector {
return &StatsCollector{
stream: make(chan ContainerStat),
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerStat](),
func NewDockerStatsCollector(client container.Client, labels container.ContainerLabels) *DockerStatsCollector {
return &DockerStatsCollector{
stream: make(chan container.ContainerStat),
subscribers: xsync.NewMapOf[context.Context, chan<- container.ContainerStat](),
client: client,
cancelers: xsync.NewMapOf[string, context.CancelFunc](),
filter: filter,
labels: labels,
}
}
func (c *StatsCollector) Subscribe(ctx context.Context, stats chan<- ContainerStat) {
func (c *DockerStatsCollector) Subscribe(ctx context.Context, stats chan<- container.ContainerStat) {
c.subscribers.Store(ctx, stats)
go func() {
<-ctx.Done()
@@ -44,7 +45,7 @@ func (c *StatsCollector) Subscribe(ctx context.Context, stats chan<- ContainerSt
}()
}
func (c *StatsCollector) forceStop() {
func (c *DockerStatsCollector) forceStop() {
c.mu.Lock()
defer c.mu.Unlock()
if c.stopper != nil {
@@ -54,7 +55,7 @@ func (c *StatsCollector) forceStop() {
}
}
func (c *StatsCollector) Stop() {
func (c *DockerStatsCollector) Stop() {
c.mu.Lock()
defer c.mu.Unlock()
if c.totalStarted.Add(-1) == 0 {
@@ -64,7 +65,7 @@ func (c *StatsCollector) Stop() {
}
}
func (c *StatsCollector) reset() {
func (c *DockerStatsCollector) reset() {
c.mu.Lock()
defer c.mu.Unlock()
if c.timer != nil {
@@ -73,7 +74,7 @@ func (c *StatsCollector) reset() {
c.timer = nil
}
func streamStats(parent context.Context, sc *StatsCollector, id string) {
func streamStats(parent context.Context, sc *DockerStatsCollector, id string) {
ctx, cancel := context.WithCancel(parent)
sc.cancelers.Store(id, cancel)
log.Debug().Str("container", id).Str("host", sc.client.Host().Name).Msg("starting to stream stats")
@@ -86,7 +87,7 @@ func streamStats(parent context.Context, sc *StatsCollector, id string) {
}
// Start starts the stats collector and blocks until it's stopped. It returns true if the collector was stopped, false if it was already running
func (sc *StatsCollector) Start(parentCtx context.Context) bool {
func (sc *DockerStatsCollector) Start(parentCtx context.Context) bool {
sc.reset()
sc.totalStarted.Add(1)
@@ -99,8 +100,8 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
ctx, sc.stopper = context.WithCancel(parentCtx)
sc.mu.Unlock()
timeoutCtx, cancel := context.WithTimeout(parentCtx, defaultTimeout)
if containers, err := sc.client.ListContainers(timeoutCtx, sc.filter); err == nil {
timeoutCtx, cancel := context.WithTimeout(parentCtx, 3*time.Second) // 3 seconds to list containers is hard limit
if containers, err := sc.client.ListContainers(timeoutCtx, sc.labels); err == nil {
for _, c := range containers {
if c.State == "running" {
go streamStats(ctx, sc, c.ID)
@@ -111,7 +112,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
}
cancel()
events := make(chan ContainerEvent)
events := make(chan container.ContainerEvent)
go func() {
log.Debug().Str("host", sc.client.Host().Name).Msg("starting to listen to docker events")
@@ -142,7 +143,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
log.Info().Str("host", sc.client.Host().Name).Msg("stopped container stats collector")
return true
case stat := <-sc.stream:
sc.subscribers.Range(func(c context.Context, stats chan<- ContainerStat) bool {
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
select {
case stats <- stat:
case <-c.Done():
@@ -1,16 +1,47 @@
package container
package docker
import (
"context"
"testing"
"github.com/amir20/dozzle/internal/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func startedCollector(ctx context.Context) *StatsCollector {
type mockedClient struct {
mock.Mock
container.Client
}
func (m *mockedClient) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
args := m.Called(ctx, filter)
return args.Get(0).([]container.Container), args.Error(1)
}
func (m *mockedClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
args := m.Called(ctx, id)
return args.Get(0).(container.Container), args.Error(1)
}
func (m *mockedClient) ContainerEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
args := m.Called(ctx, events)
return args.Error(0)
}
func (m *mockedClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
args := m.Called(ctx, id, stats)
return args.Error(0)
}
func (m *mockedClient) Host() container.Host {
args := m.Called()
return args.Get(0).(container.Host)
}
func startedCollector(ctx context.Context) *DockerStatsCollector {
client := new(mockedClient)
client.On("ListContainers", mock.Anything, mock.Anything).Return([]Container{
client.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
{
ID: "1234",
Name: "test",
@@ -26,17 +57,17 @@ func startedCollector(ctx context.Context) *StatsCollector {
client.On("ContainerStats", mock.Anything, mock.Anything, mock.AnythingOfType("chan<- container.ContainerStat")).
Return(nil).
Run(func(args mock.Arguments) {
stats := args.Get(2).(chan<- ContainerStat)
stats <- ContainerStat{
stats := args.Get(2).(chan<- container.ContainerStat)
stats <- container.ContainerStat{
ID: "1234",
}
})
client.On("Host").Return(Host{
client.On("Host").Return(container.Host{
ID: "localhost",
})
collector := NewStatsCollector(client, ContainerFilter{})
stats := make(chan ContainerStat)
collector := NewDockerStatsCollector(client, container.ContainerLabels{})
stats := make(chan container.ContainerStat)
collector.Subscribe(ctx, stats)
+1 -1
View File
@@ -14,7 +14,7 @@ func RPCRequest(ctx context.Context, addr string, certs tls.Certificate) error {
if err != nil {
log.Fatal().Err(err).Msg("Failed to create agent client")
}
containers, err := client.ListContainers(ctx, container.ContainerFilter{})
containers, err := client.ListContainers(ctx, container.ContainerLabels{})
log.Trace().Int("containers", len(containers)).Msg("Healtcheck RPC request completed")
return err
}
+252
View File
@@ -0,0 +1,252 @@
package k8s
import (
"context"
"fmt"
"io"
"strings"
"time"
"os"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/utils"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/rs/zerolog/log"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
type K8sClient struct {
Clientset *kubernetes.Clientset
namespace string
config *rest.Config
host container.Host
}
func NewK8sClient(namespace string) (*K8sClient, error) {
var config *rest.Config
var err error
// Check if we're running in cluster
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
config, err = rest.InClusterConfig()
if err != nil {
return nil, err
}
log.Info().Msg("Running in-cluster mode")
} else {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = os.Getenv("HOME") + "/.kube/config"
}
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
}
log.Info().Msgf("Running in local mode with kubeconfig: %s", kubeconfig)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
nodes, err := clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
if len(nodes.Items) == 0 {
return nil, fmt.Errorf("nodes not found")
}
node := nodes.Items[0]
return &K8sClient{
Clientset: clientset,
namespace: namespace,
config: config,
host: container.Host{
ID: node.Status.NodeInfo.MachineID,
Name: node.Name,
},
}, nil
}
func podToContainers(pod *corev1.Pod) []container.Container {
started := time.Time{}
if pod.Status.StartTime != nil {
started = pod.Status.StartTime.Time
}
var containers []container.Container
for _, c := range pod.Spec.Containers {
containers = append(containers, container.Container{
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
Name: pod.Name + "/" + c.Name,
Image: c.Image,
Created: pod.CreationTimestamp.Time,
State: phaseToState(pod.Status.Phase),
StartedAt: started,
Command: strings.Join(c.Command, " "),
Host: pod.Spec.NodeName,
Tty: c.TTY,
Stats: utils.NewRingBuffer[container.ContainerStat](300),
FullyLoaded: true,
})
}
return containers
}
func (k *K8sClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
selector := ""
if labels.Exists() {
for key, values := range labels {
for _, value := range values {
if selector != "" {
selector += ","
}
selector += fmt.Sprintf("%s=%s", key, value)
}
}
log.Debug().Str("selector", selector).Msg("Listing containers with labels")
}
pods, err := k.Clientset.CoreV1().Pods(k.namespace).List(ctx, metav1.ListOptions{LabelSelector: selector})
if err != nil {
return nil, err
}
var containers []container.Container
for _, pod := range pods.Items {
containers = append(containers, podToContainers(&pod)...)
}
return containers, nil
}
func phaseToState(phase corev1.PodPhase) string {
switch phase {
case corev1.PodPending:
return "created"
case corev1.PodRunning:
return "running"
case corev1.PodSucceeded:
return "exited"
case corev1.PodFailed:
return "exited"
case corev1.PodUnknown:
return "unknown"
default:
return "unknown"
}
}
func (k *K8sClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
log.Debug().Str("id", id).Msg("Finding container")
namespace, podName, containerName := parsePodContainerID(id)
pod, err := k.Clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return container.Container{}, err
}
for _, c := range podToContainers(pod) {
if c.ID == id {
return c, nil
}
}
return container.Container{}, fmt.Errorf("container %s not found in pod %s", containerName, podName)
}
func (k *K8sClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
namespace, podName, containerName := parsePodContainerID(id)
var lines int64 = 500
opts := &corev1.PodLogOptions{
Container: containerName,
Follow: true,
Previous: false,
Timestamps: true,
SinceTime: &metav1.Time{Time: since},
TailLines: &lines,
}
return k.Clientset.CoreV1().Pods(namespace).GetLogs(podName, opts).Stream(ctx)
}
func (k *K8sClient) ContainerLogsBetweenDates(ctx context.Context, id string, start time.Time, end time.Time, stdType container.StdType) (io.ReadCloser, error) {
namespace, podName, containerName := parsePodContainerID(id)
opts := &corev1.PodLogOptions{
Container: containerName,
Follow: false,
Timestamps: true,
SinceTime: &metav1.Time{Time: start},
}
return k.Clientset.CoreV1().Pods(namespace).GetLogs(podName, opts).Stream(ctx)
}
func (k *K8sClient) ContainerEvents(ctx context.Context, ch chan<- container.ContainerEvent) error {
watch, err := k.Clientset.CoreV1().Pods(k.namespace).Watch(ctx, metav1.ListOptions{})
if err != nil {
return err
}
for event := range watch.ResultChan() {
log.Debug().Interface("event.type", event.Type).Msg("Received kubernetes event")
pod, ok := event.Object.(*corev1.Pod)
if !ok {
continue
}
name := ""
switch event.Type {
case "ADDED":
name = "create"
case "DELETED":
name = "destroy"
case "MODIFIED":
name = "update"
}
for _, c := range podToContainers(pod) {
ch <- container.ContainerEvent{
Name: name,
ActorID: c.ID,
Host: pod.Spec.NodeName,
Time: time.Now(),
Container: &c,
}
}
}
return nil
}
func (k *K8sClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
panic("not implemented")
}
func (k *K8sClient) Ping(ctx context.Context) error {
_, err := k.Clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{Limit: 1})
return err
}
func (k *K8sClient) Host() container.Host {
return k.host
}
func (k *K8sClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
// Implementation for container actions (start, stop, restart, etc.)
return nil
}
// Helper function to parse pod and container names from container ID
func parsePodContainerID(id string) (string, string, string) {
parts := strings.Split(id, ":")
return parts[0], parts[1], parts[2]
}
+27
View File
@@ -0,0 +1,27 @@
package k8s
import (
"bufio"
"io"
"github.com/amir20/dozzle/internal/container"
)
type LogReader struct {
reader *bufio.Reader
}
func NewLogReader(reader io.ReadCloser) *LogReader {
return &LogReader{
reader: bufio.NewReader(reader),
}
}
func (r *LogReader) Read() (string, container.StdType, error) {
line, err := r.reader.ReadString('\n')
if err != nil {
return "", 0, err
}
return line, container.STDOUT, nil
}
+124
View File
@@ -0,0 +1,124 @@
package k8s
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metricsclient "k8s.io/metrics/pkg/client/clientset/versioned"
)
var timeToStop = 2 * time.Hour
type K8sStatsCollector struct {
client *K8sClient
metrics *metricsclient.Clientset
subscribers *xsync.MapOf[context.Context, chan<- container.ContainerStat]
stopper context.CancelFunc
timer *time.Timer
mu sync.Mutex
totalStarted atomic.Int32
labels container.ContainerLabels
}
func NewK8sStatsCollector(client *K8sClient, labels container.ContainerLabels) (*K8sStatsCollector, error) {
metricsClient, err := metricsclient.NewForConfig(client.config)
if err != nil {
return nil, err
}
return &K8sStatsCollector{
subscribers: xsync.NewMapOf[context.Context, chan<- container.ContainerStat](),
client: client,
labels: labels,
metrics: metricsClient,
}, nil
}
func (c *K8sStatsCollector) Subscribe(ctx context.Context, stats chan<- container.ContainerStat) {
c.subscribers.Store(ctx, stats)
go func() {
<-ctx.Done()
c.subscribers.Delete(ctx)
}()
}
func (c *K8sStatsCollector) Stop() {
c.mu.Lock()
defer c.mu.Unlock()
if c.totalStarted.Add(-1) == 0 {
c.timer = time.AfterFunc(timeToStop, func() {
c.forceStop()
})
}
}
func (c *K8sStatsCollector) forceStop() {
c.mu.Lock()
defer c.mu.Unlock()
if c.stopper != nil {
c.stopper()
c.stopper = nil
log.Debug().Msg("stopped container k8s stats collector")
}
}
func (c *K8sStatsCollector) reset() {
c.mu.Lock()
defer c.mu.Unlock()
if c.timer != nil {
c.timer.Stop()
}
c.timer = nil
}
// Start starts the stats collector and blocks until it's stopped. It returns true if the collector was stopped, false if it was already running
func (sc *K8sStatsCollector) Start(parentCtx context.Context) bool {
sc.reset()
sc.totalStarted.Add(1)
sc.mu.Lock()
if sc.stopper != nil {
sc.mu.Unlock()
return false
}
var ctx context.Context
ctx, sc.stopper = context.WithCancel(parentCtx)
sc.mu.Unlock()
ticker := time.NewTicker(1 * time.Second)
for {
select {
case <-ticker.C:
metricList, err := sc.metrics.MetricsV1beta1().PodMetricses(sc.client.namespace).List(ctx, metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
for _, pod := range metricList.Items {
for _, c := range pod.Containers {
stat := container.ContainerStat{
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
}
log.Trace().Interface("stat", stat).Msg("k8s stats")
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
select {
case stats <- stat:
case <-c.Done():
sc.subscribers.Delete(c)
}
return true
})
}
}
case <-ctx.Done():
return true
}
}
}
+67
View File
@@ -0,0 +1,67 @@
package cli
import (
"context"
"embed"
"fmt"
"io"
"net"
"os"
"os/signal"
"syscall"
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/docker"
"github.com/rs/zerolog/log"
)
type AgentCmd struct {
Addr string `arg:"--agent-addr,env:DOZZLE_AGENT_ADDR" default:":7007" help:"sets the host:port to bind for the agent"`
}
func (a *AgentCmd) Run(args Args, embeddedCerts embed.FS) error {
if args.Mode != "server" {
return fmt.Errorf("agent command is only available in server mode")
}
client, err := docker.NewLocalClient(args.Hostname)
if err != nil {
return fmt.Errorf("failed to create docker client: %w", err)
}
certs, err := ReadCertificates(embeddedCerts)
if err != nil {
return fmt.Errorf("failed to read certificates: %w", err)
}
listener, err := net.Listen("tcp", args.Agent.Addr)
if err != nil {
return fmt.Errorf("failed to listen: %w", err)
}
tempFile, err := os.CreateTemp("./", "agent-*.addr")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
io.WriteString(tempFile, listener.Addr().String())
go StartEvent(args, "", client, "agent")
server, err := agent.NewServer(client, certs, args.Version(), args.Filter)
if err != nil {
return fmt.Errorf("failed to create agent server: %w", err)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
log.Info().Msgf("Dozzle agent version %s", args.Version())
log.Info().Msgf("Agent listening on %s", listener.Addr().String())
if err := server.Serve(listener); err != nil {
log.Error().Err(err).Msg("failed to serve")
}
}()
<-ctx.Done()
stop()
log.Info().Msg("Shutting down agent")
server.Stop()
log.Debug().Str("file", tempFile.Name()).Msg("Removing temp file")
os.Remove(tempFile.Name())
return nil
}
@@ -0,0 +1,38 @@
package cli
import (
"context"
"embed"
"fmt"
"github.com/amir20/dozzle/internal/agent"
"github.com/rs/zerolog/log"
)
type AgentTestCmd struct {
Address string `arg:"positional"`
}
func (at *AgentTestCmd) Run(args Args, embeddedCerts embed.FS) error {
certs, err := ReadCertificates(embeddedCerts)
if err != nil {
return fmt.Errorf("error reading certificates: %w", err)
}
log.Info().Str("endpoint", args.AgentTest.Address).Msg("Connecting to agent")
agent, err := agent.NewClient(args.AgentTest.Address, certs)
if err != nil {
return fmt.Errorf("error connecting to agent: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
defer cancel()
host, err := agent.Host(ctx)
if err != nil {
return fmt.Errorf("error fetching host info for agent: %w", err)
}
log.Info().Str("endpoint", args.AgentTest.Address).Str("version", host.AgentVersion).Str("name", host.Name).Str("id", host.ID).Msg("Successfully connected to agent")
return nil
}
+4 -17
View File
@@ -1,6 +1,7 @@
package cli
import (
"embed"
"strings"
"time"
@@ -29,29 +30,15 @@ type Args struct {
Mode string `arg:"env:DOZZLE_MODE" default:"server" help:"sets the mode to run in (server, swarm)"`
TimeoutString string `arg:"--timeout,env:DOZZLE_TIMEOUT" default:"10s" help:"sets the timeout for docker client"`
Timeout time.Duration `arg:"-"`
Namespace string `arg:"env:DOZZLE_NAMESPACE" default:"" help:"sets the namespace to use in k8s"`
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"`
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
AgentTest *AgentTestCmd `arg:"subcommand:agent-test" help:"tests an agent"`
}
type HealthcheckCmd struct {
}
type AgentCmd struct {
Addr string `arg:"--agent-addr,env:DOZZLE_AGENT_ADDR" default:":7007" help:"sets the host:port to bind for the agent"`
}
type AgentTestCmd struct {
Address string `arg:"positional"`
}
type GenerateCmd struct {
Username string `arg:"positional"`
Password string `arg:"--password, -p" help:"sets the password for the user"`
Name string `arg:"--name, -n" help:"sets the display name for the user"`
Email string `arg:"--email, -e" help:"sets the email for the user"`
Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."`
type Runnable interface {
Run(args Args, embeddedCerts embed.FS) error
}
func (Args) Version() string {
+5 -3
View File
@@ -6,12 +6,13 @@ import (
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
container_support "github.com/amir20/dozzle/internal/support/container"
docker_support "github.com/amir20/dozzle/internal/support/docker"
"github.com/rs/zerolog/log"
)
func CreateMultiHostService(embeddedCerts embed.FS, args Args) (container.Client, *docker_support.MultiHostService) {
var clients []docker_support.ClientService
func CreateMultiHostService(embeddedCerts embed.FS, args Args) *docker_support.MultiHostService {
var clients []container_support.ClientService
if len(args.RemoteHost) > 0 {
log.Info().Msg(`Consider using Dozzle's remote agent to manage remote hosts. See https://dozzle.dev/guide/agent for more information`)
}
@@ -47,6 +48,7 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (container.Client
log.Debug().Msg("Adding local Docker Engine")
clients = append(clients, docker_support.NewDockerClientService(localClient, args.Filter))
}
go StartEvent(args, "server", localClient, "")
}
certs, err := ReadCertificates(embeddedCerts)
@@ -55,5 +57,5 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (container.Client
}
clientManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs, clients...)
return localClient, docker_support.NewMultiHostService(clientManager, args.Timeout)
return docker_support.NewMultiHostService(clientManager, args.Timeout)
}
+38
View File
@@ -0,0 +1,38 @@
package cli
import (
"embed"
"fmt"
"os"
"github.com/amir20/dozzle/internal/auth"
)
type GenerateCmd struct {
Username string `arg:"positional"`
Password string `arg:"--password, -p" help:"sets the password for the user"`
Name string `arg:"--name, -n" help:"sets the display name for the user"`
Email string `arg:"--email, -e" help:"sets the email for the user"`
Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."`
}
func (g *GenerateCmd) Run(args Args, embeddedCerts embed.FS) error {
StartEvent(args, "", nil, "generate")
if args.Generate.Username == "" || args.Generate.Password == "" {
return fmt.Errorf("username and password are required")
}
buffer := auth.GenerateUsers(auth.User{
Username: args.Generate.Username,
Password: args.Generate.Password,
Name: args.Generate.Name,
Email: args.Generate.Email,
Filter: args.Generate.Filter,
}, true)
if _, err := os.Stdout.Write(buffer.Bytes()); err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
}
return nil
}
+50
View File
@@ -0,0 +1,50 @@
package cli
import (
"context"
"embed"
"fmt"
"os"
"path/filepath"
"github.com/amir20/dozzle/internal/healthcheck"
)
type HealthcheckCmd struct {
}
func (h *HealthcheckCmd) Run(args Args, embeddedCerts embed.FS) error {
files, err := os.ReadDir(".")
if err != nil {
return fmt.Errorf("failed to read directory: %w", err)
}
agentAddress := ""
for _, file := range files {
if match, _ := filepath.Match("agent-*.addr", file.Name()); match {
data, err := os.ReadFile(file.Name())
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
agentAddress = string(data)
break
}
}
if agentAddress == "" {
if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil {
return fmt.Errorf("failed to make request: %w", err)
}
} else {
certs, err := ReadCertificates(embeddedCerts)
if err != nil {
return fmt.Errorf("failed to read certificates: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
defer cancel()
if err := healthcheck.RPCRequest(ctx, agentAddress, certs); err != nil {
return fmt.Errorf("failed to make request: %w", err)
}
}
return nil
}
+1
View File
@@ -10,6 +10,7 @@ import (
func ConfigureLogger(level string) {
if level, err := zerolog.ParseLevel(level); err == nil {
zerolog.SetGlobalLevel(level)
log.Logger = log.With().Str("version", Version).Logger()
} else {
panic(err)
}
@@ -1,4 +1,4 @@
package docker_support
package container_support
import (
"context"
@@ -22,7 +22,7 @@ func NewAgentService(client *agent.Client) ClientService {
}
}
func (a *agentService) FindContainer(ctx context.Context, id string, filter container.ContainerFilter) (container.Container, error) {
func (a *agentService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
return a.client.FindContainer(ctx, id)
}
@@ -38,9 +38,9 @@ func (a *agentService) StreamLogs(ctx context.Context, container container.Conta
return a.client.StreamContainerLogs(ctx, container.ID, from, stdTypes, events)
}
func (a *agentService) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
log.Debug().Interface("filter", filter).Msg("Listing containers from agent")
return a.client.ListContainers(ctx, filter)
func (a *agentService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
log.Debug().Interface("labels", labels).Msg("Listing containers from agent")
return a.client.ListContainers(ctx, labels)
}
func (a *agentService) Host(ctx context.Context) (container.Host, error) {
@@ -0,0 +1,28 @@
package container_support
import (
"context"
"io"
"time"
"github.com/amir20/dozzle/internal/container"
)
type ContainerFilter = func(*container.Container) bool
type ClientService interface {
FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error)
ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error)
Host(ctx context.Context) (container.Host, error)
ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error
LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error)
RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error)
// Subscriptions
SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat)
SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent)
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container)
// Blocking streaming functions that should be used in a goroutine
StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error
}
@@ -1,4 +1,4 @@
package docker_support
package container_support
import (
"context"
@@ -8,23 +8,30 @@ import (
"github.com/amir20/dozzle/internal/container"
)
type containerService struct {
type ContainerService struct {
clientService ClientService
Container container.Container
}
func (c *containerService) RawLogs(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
func NewContainerService(clientService ClientService, container container.Container) *ContainerService {
return &ContainerService{
clientService: clientService,
Container: container,
}
}
func (c *ContainerService) RawLogs(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
return c.clientService.RawLogs(ctx, c.Container, from, to, stdTypes)
}
func (c *containerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
func (c *ContainerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
return c.clientService.LogsBetweenDates(ctx, c.Container, from, to, stdTypes)
}
func (c *containerService) StreamLogs(ctx context.Context, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
func (c *ContainerService) StreamLogs(ctx context.Context, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
return c.clientService.StreamLogs(ctx, c.Container, from, stdTypes, events)
}
func (c *containerService) Action(ctx context.Context, action container.ContainerAction) error {
func (c *ContainerService) Action(ctx context.Context, action container.ContainerAction) error {
return c.clientService.ContainerAction(ctx, c.Container, action)
}
-99
View File
@@ -1,99 +0,0 @@
package docker_support
import (
"context"
"io"
"time"
"github.com/amir20/dozzle/internal/container"
)
type ClientService interface {
FindContainer(ctx context.Context, id string, filter container.ContainerFilter) (container.Container, error)
ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error)
Host(ctx context.Context) (container.Host, error)
ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error
LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error)
RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error)
// Subscriptions
SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat)
SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent)
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container)
// Blocking streaming functions that should be used in a goroutine
StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error
}
type dockerClientService struct {
client container.Client
store *container.ContainerStore
}
func NewDockerClientService(client container.Client, filter container.ContainerFilter) ClientService {
return &dockerClientService{
client: client,
store: container.NewContainerStore(context.Background(), client, filter),
}
}
func (d *dockerClientService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
return d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
}
func (d *dockerClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
reader, err := d.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
if err != nil {
return nil, err
}
g := container.NewEventGenerator(ctx, reader, c)
return g.Events, nil
}
func (d *dockerClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
reader, err := d.client.ContainerLogs(ctx, c.ID, from, stdTypes)
if err != nil {
return err
}
g := container.NewEventGenerator(ctx, reader, c)
for event := range g.Events {
events <- event
}
select {
case e := <-g.Errors:
return e
default:
return nil
}
}
func (d *dockerClientService) FindContainer(ctx context.Context, id string, filter container.ContainerFilter) (container.Container, error) {
return d.store.FindContainer(id, filter)
}
func (d *dockerClientService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
return d.client.ContainerActions(ctx, action, container.ID)
}
func (d *dockerClientService) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
return d.store.ListContainers(filter)
}
func (d *dockerClientService) Host(ctx context.Context) (container.Host, error) {
return d.client.Host(), nil
}
func (d *dockerClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
d.store.SubscribeStats(ctx, stats)
}
func (d *dockerClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
d.store.SubscribeEvents(ctx, events)
}
func (d *dockerClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
d.store.SubscribeNewContainers(ctx, containers)
}
+111
View File
@@ -0,0 +1,111 @@
package docker_support
import (
"context"
"io"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
"github.com/docker/docker/pkg/stdcopy"
"github.com/rs/zerolog/log"
)
type DockerClientService struct {
client container.Client
store *container.ContainerStore
}
func NewDockerClientService(client container.Client, labels container.ContainerLabels) *DockerClientService {
statsCollector := docker.NewDockerStatsCollector(client, labels)
return &DockerClientService{
client: client,
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
}
}
func (d *DockerClientService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
reader, err := d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
if err != nil {
return nil, err
}
in, out := io.Pipe()
go func() {
if container.Tty {
if _, err := io.Copy(out, reader); err != nil {
log.Error().Err(err).Msgf("error copying logs for container %s", container.ID)
}
} else {
if _, err := stdcopy.StdCopy(out, out, reader); err != nil {
log.Error().Err(err).Msgf("error copying logs for container %s", container.ID)
}
}
out.Close()
}()
return in, nil
}
func (d *DockerClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
reader, err := d.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
if err != nil {
return nil, err
}
dockerReader := docker.NewLogReader(reader, c.Tty)
g := container.NewEventGenerator(ctx, dockerReader, c)
return g.Events, nil
}
func (d *DockerClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
reader, err := d.client.ContainerLogs(ctx, c.ID, from, stdTypes)
if err != nil {
return err
}
dockerReader := docker.NewLogReader(reader, c.Tty)
g := container.NewEventGenerator(ctx, dockerReader, c)
for event := range g.Events {
events <- event
}
select {
case e := <-g.Errors:
return e
default:
return nil
}
}
func (d *DockerClientService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
return d.store.FindContainer(id, labels)
}
func (d *DockerClientService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
return d.client.ContainerActions(ctx, action, container.ID)
}
func (d *DockerClientService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
return d.store.ListContainers(labels)
}
func (d *DockerClientService) Host(ctx context.Context) (container.Host, error) {
return d.client.Host(), nil
}
func (d *DockerClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
d.store.SubscribeStats(ctx, stats)
}
func (d *DockerClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
d.store.SubscribeEvents(ctx, events)
}
func (d *DockerClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
d.store.SubscribeNewContainers(ctx, containers)
}
+18 -22
View File
@@ -6,11 +6,10 @@ import (
"time"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/rs/zerolog/log"
)
type ContainerFilter = func(*container.Container) bool
type HostUnavailableError struct {
Host container.Host
Err error
@@ -21,9 +20,9 @@ func (h *HostUnavailableError) Error() string {
}
type ClientManager interface {
Find(id string) (ClientService, bool)
List() []ClientService
RetryAndList() ([]ClientService, []error)
Find(id string) (container_support.ClientService, bool)
List() []container_support.ClientService
RetryAndList() ([]container_support.ClientService, []error)
Subscribe(ctx context.Context, channel chan<- container.Host)
Hosts(ctx context.Context) []container.Host
LocalClients() []container.Client
@@ -43,25 +42,22 @@ func NewMultiHostService(manager ClientManager, timeout time.Duration) *MultiHos
return m
}
func (m *MultiHostService) FindContainer(host string, id string, filter container.ContainerFilter) (*containerService, error) {
func (m *MultiHostService) FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error) {
client, ok := m.manager.Find(host)
if !ok {
return nil, fmt.Errorf("host %s not found", host)
}
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
container, err := client.FindContainer(ctx, id, filter)
container, err := client.FindContainer(ctx, id, labels)
if err != nil {
return nil, err
}
return &containerService{
clientService: client,
Container: container,
}, nil
return container_support.NewContainerService(client, container), nil
}
func (m *MultiHostService) ListContainersForHost(host string, filter container.ContainerFilter) ([]container.Container, error) {
func (m *MultiHostService) ListContainersForHost(host string, labels container.ContainerLabels) ([]container.Container, error) {
client, ok := m.manager.Find(host)
if !ok {
return nil, fmt.Errorf("host %s not found", host)
@@ -69,17 +65,17 @@ func (m *MultiHostService) ListContainersForHost(host string, filter container.C
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
return client.ListContainers(ctx, filter)
return client.ListContainers(ctx, labels)
}
func (m *MultiHostService) ListAllContainers(filter container.ContainerFilter) ([]container.Container, []error) {
func (m *MultiHostService) ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error) {
containers := make([]container.Container, 0)
clients, errors := m.manager.RetryAndList()
for _, client := range clients {
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
list, err := client.ListContainers(ctx, filter)
list, err := client.ListContainers(ctx, labels)
if err != nil {
host, _ := client.Host(ctx)
log.Debug().Err(err).Str("host", host.Name).Msg("error listing containers")
@@ -94,8 +90,8 @@ func (m *MultiHostService) ListAllContainers(filter container.ContainerFilter) (
return containers, errors
}
func (m *MultiHostService) ListAllContainersFiltered(userFilter container.ContainerFilter, filter ContainerFilter) ([]container.Container, []error) {
containers, err := m.ListAllContainers(userFilter)
func (m *MultiHostService) ListAllContainersFiltered(userLabels container.ContainerLabels, filter container_support.ContainerFilter) ([]container.Container, []error) {
containers, err := m.ListAllContainers(userLabels)
filtered := make([]container.Container, 0, len(containers))
for _, container := range containers {
if filter(&container) {
@@ -112,7 +108,7 @@ func (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events c
}
}
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter ContainerFilter) {
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
newContainers := make(chan container.Container)
for _, client := range m.manager.List() {
client.SubscribeContainersStarted(ctx, newContainers)
@@ -135,10 +131,6 @@ func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, conta
}()
}
func (m *MultiHostService) TotalClients() int {
return len(m.manager.List())
}
func (m *MultiHostService) Hosts() []container.Host {
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
@@ -161,3 +153,7 @@ func (m *MultiHostService) SubscribeAvailableHosts(ctx context.Context, hosts ch
func (m *MultiHostService) LocalClients() []container.Client {
return m.manager.LocalClients()
}
func (m *MultiHostService) TotalClients() int {
return len(m.manager.List())
}
@@ -9,6 +9,8 @@ import (
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/puzpuzpuz/xsync/v3"
"github.com/samber/lo"
lop "github.com/samber/lo/parallel"
@@ -17,7 +19,7 @@ import (
)
type RetriableClientManager struct {
clients map[string]ClientService
clients map[string]container_support.ClientService
failedAgents []string
certs tls.Certificate
mu sync.RWMutex
@@ -25,8 +27,8 @@ type RetriableClientManager struct {
timeout time.Duration
}
func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls.Certificate, clients ...ClientService) *RetriableClientManager {
clientMap := make(map[string]ClientService)
func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls.Certificate, clients ...container_support.ClientService) *RetriableClientManager {
clientMap := make(map[string]container_support.ClientService)
for _, client := range clients {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
@@ -64,7 +66,7 @@ func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls
if _, ok := clientMap[host.ID]; ok {
log.Warn().Str("host", host.Name).Str("id", host.ID).Msg("duplicate host with same ID found")
} else {
clientMap[host.ID] = NewAgentService(agent)
clientMap[host.ID] = container_support.NewAgentService(agent)
}
}
@@ -86,7 +88,7 @@ func (m *RetriableClientManager) Subscribe(ctx context.Context, channel chan<- c
}()
}
func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
func (m *RetriableClientManager) RetryAndList() ([]container_support.ClientService, []error) {
m.mu.Lock()
errors := make([]error, 0)
if len(m.failedAgents) > 0 {
@@ -110,7 +112,7 @@ func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
continue
}
m.clients[host.ID] = NewAgentService(agent)
m.clients[host.ID] = container_support.NewAgentService(agent)
m.subscribers.Range(func(ctx context.Context, channel chan<- container.Host) bool {
host.Available = true
@@ -133,14 +135,14 @@ func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
return m.List(), errors
}
func (m *RetriableClientManager) List() []ClientService {
func (m *RetriableClientManager) List() []container_support.ClientService {
m.mu.RLock()
defer m.mu.RUnlock()
return lo.Values(m.clients)
}
func (m *RetriableClientManager) Find(id string) (ClientService, bool) {
func (m *RetriableClientManager) Find(id string) (container_support.ClientService, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -155,7 +157,7 @@ func (m *RetriableClientManager) String() string {
func (m *RetriableClientManager) Hosts(ctx context.Context) []container.Host {
clients := m.List()
hosts := lop.Map(clients, func(client ClientService, _ int) container.Host {
hosts := lop.Map(clients, func(client container_support.ClientService, _ int) container.Host {
host, err := client.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("host", host.Name).Msg("error fetching host info for client")
@@ -186,7 +188,7 @@ func (m *RetriableClientManager) LocalClients() []container.Client {
clients := make([]container.Client, 0)
for _, service := range services {
if clientService, ok := service.(*dockerClientService); ok {
if clientService, ok := service.(*DockerClientService); ok {
clients = append(clients, clientService.client)
}
}
+13 -10
View File
@@ -11,6 +11,9 @@ import (
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/puzpuzpuz/xsync/v3"
"github.com/samber/lo"
lop "github.com/samber/lo/parallel"
@@ -19,7 +22,7 @@ import (
)
type SwarmClientManager struct {
clients map[string]ClientService
clients map[string]container_support.ClientService
certs tls.Certificate
mu sync.RWMutex
subscribers *xsync.MapOf[context.Context, chan<- container.Host]
@@ -47,9 +50,9 @@ func localIPs() []string {
return ips
}
func NewSwarmClientManager(localClient container.Client, certs tls.Certificate, timeout time.Duration, agentManager *RetriableClientManager, filter container.ContainerFilter) *SwarmClientManager {
clientMap := make(map[string]ClientService)
localService := NewDockerClientService(localClient, filter)
func NewSwarmClientManager(localClient *docker.DockerClient, certs tls.Certificate, timeout time.Duration, agentManager *RetriableClientManager, labels container.ContainerLabels) *SwarmClientManager {
clientMap := make(map[string]container_support.ClientService)
localService := NewDockerClientService(localClient, labels)
clientMap[localClient.Host().ID] = localService
id, ok := os.LookupEnv("HOSTNAME")
@@ -90,7 +93,7 @@ func (m *SwarmClientManager) Subscribe(ctx context.Context, channel chan<- conta
}()
}
func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
func (m *SwarmClientManager) RetryAndList() ([]container_support.ClientService, []error) {
m.mu.Lock()
ips, err := net.LookupIP(fmt.Sprintf("tasks.%s", m.name))
@@ -104,7 +107,7 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
}
clients := lo.Values(m.clients)
endpoints := lo.KeyBy(clients, func(client ClientService) string {
endpoints := lo.KeyBy(clients, func(client container_support.ClientService) string {
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
host, _ := client.Host(ctx)
@@ -155,7 +158,7 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
continue
}
client := NewAgentService(agent)
client := container_support.NewAgentService(agent)
m.clients[host.ID] = client
log.Info().Stringer("ip", ip).Str("id", host.ID).Str("name", host.Name).Msg("added new swarm agent")
@@ -182,7 +185,7 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
return m.List(), errors
}
func (m *SwarmClientManager) List() []ClientService {
func (m *SwarmClientManager) List() []container_support.ClientService {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -192,7 +195,7 @@ func (m *SwarmClientManager) List() []ClientService {
return append(agents, clients...)
}
func (m *SwarmClientManager) Find(id string) (ClientService, bool) {
func (m *SwarmClientManager) Find(id string) (container_support.ClientService, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -210,7 +213,7 @@ func (m *SwarmClientManager) Hosts(ctx context.Context) []container.Host {
clients := lo.Values(m.clients)
m.mu.RUnlock()
swarmNodes := lop.Map(clients, func(client ClientService, _ int) container.Host {
swarmNodes := lop.Map(clients, func(client container_support.ClientService, _ int) container.Host {
host, err := client.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("id", host.ID).Msg("error getting host from client")
+133
View File
@@ -0,0 +1,133 @@
package k8s_support
import (
"context"
"fmt"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/k8s"
container_support "github.com/amir20/dozzle/internal/support/container"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type K8sClusterService struct {
client *K8sClientService
timeout time.Duration
hosts []container.Host
}
func NewK8sClusterService(client *k8s.K8sClient, timeout time.Duration) (*K8sClusterService, error) {
hosts := make([]container.Host, 0)
nodes, err := client.Clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
if len(nodes.Items) == 0 {
return nil, fmt.Errorf("nodes not found")
}
for _, node := range nodes.Items {
hosts = append(hosts, container.Host{
ID: node.Name,
Name: node.Name,
MemTotal: node.Status.Capacity.Memory().Value(),
NCPU: int(node.Status.Capacity.Cpu().Value()),
DockerVersion: node.Status.NodeInfo.ContainerRuntimeVersion,
Type: "k8s",
Available: true,
})
}
return &K8sClusterService{
client: NewK8sClientService(client, container.ContainerLabels{}),
timeout: timeout,
hosts: hosts,
}, nil
}
func (m *K8sClusterService) FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error) {
container, err := m.client.FindContainer(context.Background(), id, labels)
if err != nil {
return nil, err
}
return container_support.NewContainerService(m.client, container), nil
}
func (m *K8sClusterService) ListContainersForHost(host string, labels container.ContainerLabels) ([]container.Container, error) {
containers, err := m.client.ListContainers(context.Background(), labels)
if err != nil {
return nil, err
}
filteredContainers := make([]container.Container, 0)
for _, container := range containers {
if container.Host == host {
filteredContainers = append(filteredContainers, container)
}
}
return filteredContainers, nil
}
func (m *K8sClusterService) ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error) {
containers, err := m.client.ListContainers(context.Background(), labels)
if err != nil {
return nil, []error{err}
}
return containers, nil
}
func (m *K8sClusterService) ListAllContainersFiltered(userLabels container.ContainerLabels, filter container_support.ContainerFilter) ([]container.Container, []error) {
containers, err := m.ListAllContainers(userLabels)
filtered := make([]container.Container, 0, len(containers))
for _, container := range containers {
if filter(&container) {
filtered = append(filtered, container)
}
}
return filtered, err
}
func (m *K8sClusterService) SubscribeEventsAndStats(ctx context.Context, events chan<- container.ContainerEvent, stats chan<- container.ContainerStat) {
m.client.SubscribeEvents(ctx, events)
m.client.SubscribeStats(ctx, stats)
}
func (m *K8sClusterService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
newContainers := make(chan container.Container)
m.client.SubscribeContainersStarted(ctx, newContainers)
go func() {
<-ctx.Done()
close(newContainers)
}()
go func() {
for container := range newContainers {
if filter(&container) {
select {
case containers <- container:
case <-ctx.Done():
return
}
}
}
}()
}
func (m *K8sClusterService) Hosts() []container.Host {
return m.hosts
}
func (m *K8sClusterService) LocalHost() (container.Host, error) {
return m.client.client.Host(), nil
}
func (m *K8sClusterService) SubscribeAvailableHosts(ctx context.Context, hosts chan<- container.Host) {
}
func (m *K8sClusterService) LocalClients() []container.Client {
return []container.Client{m.client.client}
}
+92
View File
@@ -0,0 +1,92 @@
package k8s_support
import (
"context"
"io"
"github.com/rs/zerolog/log"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/k8s"
)
type K8sClientService struct {
client *k8s.K8sClient
store *container.ContainerStore
}
func NewK8sClientService(client *k8s.K8sClient, labels container.ContainerLabels) *K8sClientService {
statsCollector, err := k8s.NewK8sStatsCollector(client, labels)
if err != nil {
log.Fatal().Err(err).Msg("Could not create k8s stats collector")
}
return &K8sClientService{
client: client,
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
}
}
func (k *K8sClientService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
return k.store.FindContainer(id, labels)
}
func (k *K8sClientService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
return k.store.ListContainers(labels)
}
func (k *K8sClientService) Host(ctx context.Context) (container.Host, error) {
return k.client.Host(), nil
}
func (k *K8sClientService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
return k.client.ContainerActions(ctx, action, container.ID)
}
func (k *K8sClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
reader, err := k.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
if err != nil {
return nil, err
}
k8sReader := k8s.NewLogReader(reader)
g := container.NewEventGenerator(ctx, k8sReader, c)
return g.Events, nil
}
func (k *K8sClientService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
return k.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
}
func (k *K8sClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
reader, err := k.client.ContainerLogs(ctx, c.ID, from, stdTypes)
if err != nil {
return err
}
k8sReader := k8s.NewLogReader(reader)
g := container.NewEventGenerator(ctx, k8sReader, c)
for event := range g.Events {
events <- event
}
select {
case e := <-g.Errors:
return e
default:
return nil
}
}
func (k *K8sClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
k.store.SubscribeStats(ctx, stats)
}
func (k *K8sClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
k.store.SubscribeEvents(ctx, events)
}
func (k *K8sClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
k.store.SubscribeNewContainers(ctx, containers)
}
+5 -9
View File
@@ -1,9 +1,7 @@
package web
import (
"context"
"net/http"
"time"
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/container"
@@ -15,15 +13,15 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
action := chi.URLParam(r, "action")
id := chi.URLParam(r, "id")
usersFilter := h.config.Filter
userLabels := h.config.Labels
if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context())
if user.ContainerFilter.Exists() {
usersFilter = user.ContainerFilter
if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels
}
}
containerService, err := h.multiHostService.FindContainer(hostKey(r), id, usersFilter)
containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels)
if err != nil {
log.Error().Err(err).Msg("error while trying to find container")
http.Error(w, err.Error(), http.StatusNotFound)
@@ -37,9 +35,7 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := containerService.Action(ctx, parsedAction); err != nil {
if err := containerService.Action(r.Context(), parsedAction); err != nil {
log.Error().Err(err).Msg("error while trying to perform container action")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+2 -2
View File
@@ -9,8 +9,8 @@ import (
func (h *handler) debugStore(w http.ResponseWriter, r *http.Request) {
respone := make(map[string]interface{})
respone["hosts"] = h.multiHostService.Hosts()
containers, errors := h.multiHostService.ListAllContainers(container.ContainerFilter{})
respone["hosts"] = h.hostService.Hosts()
containers, errors := h.hostService.ListAllContainers(container.ContainerLabels{})
respone["containers"] = containers
respone["errors"] = errors
+10 -18
View File
@@ -10,7 +10,6 @@ import (
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
@@ -23,11 +22,11 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
return
}
usersFilter := h.config.Filter
userLabels := h.config.Labels
if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context())
if user.ContainerFilter.Exists() {
usersFilter = user.ContainerFilter
if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels
}
}
@@ -66,7 +65,7 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
host := parts[0]
id := parts[1]
containerService, err := h.multiHostService.FindContainer(host, id, usersFilter)
containerService, err := h.hostService.FindContainer(host, id, userLabels)
if err != nil {
log.Error().Err(err).Msgf("error finding container %s", id)
http.Error(w, fmt.Sprintf("error finding container %s: %v", id, err), http.StatusBadRequest)
@@ -90,19 +89,12 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
return
}
// Copy logs directly to zip entry
if containerService.Container.Tty {
if _, err := io.Copy(f, reader); err != nil {
log.Error().Err(err).Msgf("error copying logs for container %s", id)
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
return
}
} else {
if _, err := stdcopy.StdCopy(f, f, reader); err != nil {
log.Error().Err(err).Msgf("error copying logs for container %s", id)
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
return
}
// Copy logs to zip file
_, err = io.Copy(f, reader)
if err != nil {
log.Error().Err(err).Msgf("error copying logs for container %s", id)
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
return
}
}
}
+14 -18
View File
@@ -24,18 +24,18 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
stats := make(chan container.ContainerStat)
availableHosts := make(chan container.Host)
h.multiHostService.SubscribeEventsAndStats(r.Context(), events, stats)
h.multiHostService.SubscribeAvailableHosts(r.Context(), availableHosts)
h.hostService.SubscribeEventsAndStats(r.Context(), events, stats)
h.hostService.SubscribeAvailableHosts(r.Context(), availableHosts)
usersFilter := h.config.Filter
userLabels := h.config.Labels
if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context())
if user.ContainerFilter.Exists() {
usersFilter = user.ContainerFilter
if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels
}
}
allContainers, errors := h.multiHostService.ListAllContainers(usersFilter)
allContainers, errors := h.hostService.ListAllContainers(userLabels)
for _, err := range errors {
log.Warn().Err(err).Msg("error listing containers")
@@ -68,12 +68,12 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
log.Debug().Str("event", event.Name).Str("id", event.ActorID).Msg("container event from store")
switch event.Name {
case "start", "die", "destroy", "rename":
if event.Name == "start" || event.Name == "rename" {
log.Debug().Str("action", event.Name).Str("id", event.ActorID).Msg("container event")
if containers, err := h.multiHostService.ListContainersForHost(event.Host, usersFilter); err == nil {
if containers, err := h.hostService.ListContainersForHost(event.Host, userLabels); err == nil {
log.Debug().Str("host", event.Host).Int("count", len(containers)).Msg("updating containers for host")
if err := sseWriter.Event("containers-changed", containers); err != nil {
log.Error().Err(err).Msg("error writing containers to event stream")
return
@@ -87,15 +87,11 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
}
case "update":
log.Debug().Str("id", event.ActorID).Msg("container updated")
if containerService, err := h.multiHostService.FindContainer(event.Host, event.ActorID, usersFilter); err == nil {
if err := sseWriter.Event("container-updated", containerService.Container); err != nil {
log.Error().Err(err).Msg("error writing event to event stream")
return
}
if err := sseWriter.Event("container-updated", event.Container); err != nil {
log.Error().Err(err).Msg("error writing event to event stream")
return
}
case "health_status: healthy", "health_status: unhealthy":
log.Debug().Str("container", event.ActorID).Str("health", event.Name).Msg("container health status")
healthy := "unhealthy"
if event.Name == "health_status: healthy" {
healthy = "healthy"
@@ -123,7 +119,7 @@ func sendBeaconEvent(h *handler, r *http.Request, runningContainers int) {
b := types.BeaconEvent{
AuthProvider: string(h.config.Authorization.Provider),
Browser: r.Header.Get("User-Agent"),
Clients: h.multiHostService.TotalClients(),
Clients: len(h.hostService.Hosts()),
HasActions: h.config.EnableActions,
HasCustomAddress: h.config.Addr != ":8080",
HasCustomBase: h.config.Base != "/",
@@ -133,7 +129,7 @@ func sendBeaconEvent(h *handler, r *http.Request, runningContainers int) {
Version: h.config.Version,
}
local, err := h.multiHostService.LocalHost()
local, err := h.hostService.LocalHost()
if err == nil {
b.ServerID = local.ID
}
+1 -1
View File
@@ -54,7 +54,7 @@ func Test_handler_streamEvents_happy(t *testing.T) {
})
// This is needed so that the server is initialized for store
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(mockedClient, container.ContainerFilter{}))
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(mockedClient, container.ContainerLabels{}))
multiHostService := docker_support.NewMultiHostService(manager, 3*time.Second)
server := CreateServer(multiHostService, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
+1 -1
View File
@@ -9,7 +9,7 @@ import (
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
log.Debug().Msg("Executing healthcheck")
clients := h.multiHostService.LocalClients()
clients := h.hostService.LocalClients()
for _, client := range clients {
if err := client.Ping(r.Context()); err != nil {
log.Error().Err(err).Str("host", client.Host().Name).Msg("error pinging host")
+1 -1
View File
@@ -43,7 +43,7 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
base = h.config.Base
}
hosts := h.multiHostService.Hosts()
hosts := h.hostService.Hosts()
sort.Slice(hosts, func(i, j int) bool {
return hosts[i].Name < hosts[j].Name
})
+14 -13
View File
@@ -18,6 +18,7 @@ import (
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/amir20/dozzle/internal/support/search"
support_web "github.com/amir20/dozzle/internal/support/web"
"github.com/amir20/dozzle/internal/utils"
@@ -47,15 +48,15 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
return
}
usersFilter := h.config.Filter
usersLabels := h.config.Labels
if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context())
if user.ContainerFilter.Exists() {
usersFilter = user.ContainerFilter
if user.ContainerLabels.Exists() {
usersLabels = user.ContainerLabels
}
}
containerService, err := h.multiHostService.FindContainer(hostKey(r), id, usersFilter)
containerService, err := h.hostService.FindContainer(hostKey(r), id, usersLabels)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
@@ -227,7 +228,7 @@ func (h *handler) streamHostLogs(w http.ResponseWriter, r *http.Request) {
})
}
func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request, containerFilter ContainerFilter) {
func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request, containerFilter container_support.ContainerFilter) {
var stdTypes container.StdType
if r.URL.Query().Has("stdout") {
stdTypes |= container.STDOUT
@@ -248,15 +249,15 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
return
}
usersFilter := h.config.Filter
userLabels := h.config.Labels
if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context())
if user.ContainerFilter.Exists() {
usersFilter = user.ContainerFilter
if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels
}
}
existingContainers, errs := h.multiHostService.ListAllContainersFiltered(usersFilter, containerFilter)
existingContainers, errs := h.hostService.ListAllContainersFiltered(userLabels, containerFilter)
if len(errs) > 0 {
log.Warn().Err(errs[0]).Msg("error while listing containers")
}
@@ -290,7 +291,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
events := make([]*container.LogEvent, 0)
stillRunning := false
for _, container := range existingContainers {
containerService, err := h.multiHostService.FindContainer(container.Host, container.ID, usersFilter)
containerService, err := h.hostService.FindContainer(container.Host, container.ID, userLabels)
if err != nil {
log.Error().Err(err).Msg("error while finding container")
@@ -337,7 +338,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
}
streamLogs := func(c container.Container) {
containerService, err := h.multiHostService.FindContainer(c.Host, c.ID, usersFilter)
containerService, err := h.hostService.FindContainer(c.Host, c.ID, userLabels)
if err != nil {
log.Error().Err(err).Msg("error while finding container")
return
@@ -369,7 +370,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
}
newContainers := make(chan container.Container)
h.multiHostService.SubscribeContainersStarted(r.Context(), newContainers, containerFilter)
h.hostService.SubscribeContainersStarted(r.Context(), newContainers, containerFilter)
ticker := time.NewTicker(5 * time.Second)
sseWriter.Ping()
@@ -388,7 +389,7 @@ loop:
}
sseWriter.Message(logEvent)
case c := <-newContainers:
if _, err := h.multiHostService.FindContainer(c.Host, c.ID, usersFilter); err == nil {
if _, err := h.hostService.FindContainer(c.Host, c.ID, userLabels); err == nil {
events <- &container.ContainerEvent{ActorID: c.ID, Name: "container-started", Host: c.Host}
go streamLogs(c)
}
+1 -1
View File
@@ -21,7 +21,7 @@ import (
)
func Test_handler_streamLogs_happy(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
id := "123456"
req, err := http.NewRequestWithContext(ctx, "GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil)
+23 -12
View File
@@ -1,6 +1,7 @@
package web
import (
"context"
"io/fs"
"time"
@@ -9,7 +10,7 @@ import (
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/container"
docker_support "github.com/amir20/dozzle/internal/support/docker"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@@ -34,7 +35,7 @@ type Config struct {
Dev bool
Authorization Authorization
EnableActions bool
Filter container.ContainerFilter
Labels container.ContainerLabels
}
type Authorization struct {
@@ -48,20 +49,30 @@ type Authorizer interface {
CreateToken(string, string) (string, error)
}
type handler struct {
content fs.FS
config *Config
multiHostService *docker_support.MultiHostService
type HostService interface {
FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error)
ListContainersForHost(host string, labels container.ContainerLabels) ([]container.Container, error)
ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error)
ListAllContainersFiltered(userFilter container.ContainerLabels, filter container_support.ContainerFilter) ([]container.Container, []error)
SubscribeEventsAndStats(ctx context.Context, events chan<- container.ContainerEvent, stats chan<- container.ContainerStat)
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter)
Hosts() []container.Host
LocalHost() (container.Host, error)
SubscribeAvailableHosts(ctx context.Context, hosts chan<- container.Host)
LocalClients() []container.Client
}
type MultiHostService = docker_support.MultiHostService
type ContainerFilter = docker_support.ContainerFilter
type handler struct {
content fs.FS
config *Config
hostService HostService
}
func CreateServer(multiHostService *MultiHostService, content fs.FS, config Config) *http.Server {
func CreateServer(hostService HostService, content fs.FS, config Config) *http.Server {
handler := &handler{
content: content,
config: &config,
multiHostService: multiHostService,
content: content,
config: &config,
hostService: hostService,
}
return &http.Server{Addr: config.Addr, Handler: createRouter(handler)}
+6 -6
View File
@@ -38,8 +38,8 @@ func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- contai
return args.Error(0)
}
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
args := m.Called(ctx, filter)
func (m *MockedClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
args := m.Called(ctx, labels)
return args.Get(0).([]container.Container), args.Error(1)
}
@@ -86,12 +86,12 @@ func createHandler(client container.Client, content fs.FS, config Config) *chi.M
content = afero.NewIOFS(fs)
}
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(client, container.ContainerFilter{}))
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(client, container.ContainerLabels{}))
multiHostService := docker_support.NewMultiHostService(manager, 3*time.Second)
return createRouter(&handler{
multiHostService: multiHostService,
content: content,
config: &config,
hostService: multiHostService,
content: content,
config: &config,
})
}
+5 -20
View File
@@ -13,14 +13,9 @@ label:
containers: Containere
container: Ingen containere | 1 container | {count} containere
service: Ingen tjenester | 1 tjeneste | {count} tjenester
services: Tjenester
running-containers: Kørende Containere
all-containers: Alle Containere
total-containers: Samlede Containere
running: Kører
total-cpu-usage: Samlet CPU Forbrug
total-mem-usage: Samlet Hukommelsesforbrug
dozzle-version: Dozzle Version
all: Alle
host: Vært
hosts: Værter
password: Kodeord
@@ -32,12 +27,9 @@ label:
avg-mem: Gns. MEM (%)
pinned: Fastgjort
per-page: Rækker per side
swarm-mode: Swarm Tilstand
services: Tjenester
host-menu: Værter og Containere
swarm-menu: Tjenester og Stacks
group-menu: Brugerdefinerede Grupper
tooltip:
search: Søg containere (⌘ + k, ⌃k)
pin-column: Fastgør som kolonne
@@ -52,13 +44,11 @@ error:
events-stream:
title: Uventet fejl
message: >-
Dozzle UI kunne ikke forbinde til API. Tjek venligst dine netværks indstillinger.
Hvis du bruger en reverse proxy, vær sikker på at den er konfigureret korrekt.
Dozzle UI kunne ikke forbinde til API. Tjek venligst dine netværks indstillinger. Hvis du bruger en reverse proxy, vær sikker på at den er konfigureret korrekt.
events-timeout:
title: Der er noget galt
message: >-
Dozzle UI timed ud inden der blev forbundet til API. Tjek venligst din
internet forbindelse og prøv igen.
Dozzle UI timed ud inden der blev forbundet til API. Tjek venligst din internet forbindelse og prøv igen.
alert:
redirected:
title: Omdirigeret til ny container
@@ -66,8 +56,7 @@ alert:
similar-container-found:
title: Lignende container fundet
message: >-
Dozzle fandt en lignende container {containerId} som kører på den samme
host og vil automatisk skifte til den, med mindre du trykker 'Cancel'.
Dozzle fandt en lignende container {containerId} som kører på den samme host og vil automatisk skifte til den, med mindre du trykker 'Cancel'.
title:
page-not-found: Side ikke fundet
login: Godkendelse Krævet
@@ -89,9 +78,6 @@ settings:
show-timesamps: Vis tids stempler
soft-wrap: Blødt bræk på linjer
datetime-format: Overskriv dato og tids format
12-24-format: >-
Som standard vil Dozzle bruge din browsers lokalitet til at formatere tiden. Du kan
tvinge 12 eller 24 timers format.
font-size: Tekststørrelse at bruge til logs
color-scheme: Farve skema
options: Muligheder
@@ -100,8 +86,7 @@ settings:
search: Aktiver søgning med Dozzle ved brug af
using-version: Du bruger Dozzle {version}.
update-available: >-
Ny version er tilgængelig! Opdater til <a href="{href}" target="_blank"
rel="noreferrer noopener">{nextVersion}</a>.
Ny version er tilgængelig! Opdater til <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Vis stdout og stderr mærker
automatic-redirect: Automatisk omdiriger til nye containere med det samme navn
compact: Slå kompakt tilstand til for logs
+4 -17
View File
@@ -12,8 +12,8 @@ toolbar:
label:
containers: Container
container: Keine Container | 1 Container | {count} Container
total-containers: Gesamte Container
service: Kein Service | 1 Service | {count} Services
services: Service
running-containers: Laufende Container
all-containers: Alle Container
host: Host
@@ -27,16 +27,9 @@ label:
avg-mem: Avg. MEM (%)
pinned: Angepinnt
per-page: Zeilen pro Seite
swarm-mode: Swarm Modus
services: Service
running: Laufende
host-menu: Hosts und Container
swarm-menu: Services und Stacks
group-menu: Benutzerdefinierte Gruppen
total-cpu-usage: Gesamte CPU Auslastung
total-mem-usage: Gesamte Speicher Auslastung
dozzle-version: Dozzle Version
all: Alle
tooltip:
search: Suche Container (⌘ + k, ⌃k)
pin-column: Als Spalte anheften
@@ -51,13 +44,11 @@ error:
events-stream:
title: Unerwarteter Fehler
message: >-
Das Dozzle UI konnte keine Verbindung zur API herstellen. Bitte überprüfen Sie Ihre Netzwerkeinstellungen.
Wenn Sie einen Reverse-Proxy verwenden, stellen Sie sicher, dass er korrekt konfiguriert ist.
Das Dozzle UI konnte keine Verbindung zur API herstellen. Bitte überprüfen Sie Ihre Netzwerkeinstellungen. Wenn Sie einen Reverse-Proxy verwenden, stellen Sie sicher, dass er korrekt konfiguriert ist.
events-timeout:
title: Etwas stimmt nicht
message: >-
Das Dozzle UI hat beim Verbinden mit der API eine Zeitüberschreitung. Bitte überprüfen Sie die
Netzwerkverbindung und versuchen Sie es erneut.
Das Dozzle UI hat beim Verbinden mit der API eine Zeitüberschreitung. Bitte überprüfen Sie die Netzwerkverbindung und versuchen Sie es erneut.
alert:
redirected:
title: Zu neuem Container umgeleitet
@@ -65,8 +56,7 @@ alert:
similar-container-found:
title: Ähnlicher Container gefunden
message: >-
Dozzle hat einen ähnlichen Container {containerId} gefunden, der auf dem
gleichen Host ausgeführt wird. Möchtest Du zu ihm wechseln?
Dozzle hat einen ähnlichen Container {containerId} gefunden, der auf dem gleichen Host ausgeführt wird. Möchtest Du zu ihm wechseln?
title:
page-not-found: Seite nicht gefunden
login: Authentifizierung erforderlich
@@ -88,9 +78,6 @@ settings:
show-timesamps: Zeige Zeitstempel
soft-wrap: Zeilenumbruch
datetime-format: Datums- und Zeitformat
12-24-format: >-
Standardmäßig verwendet Dozzle die Spracheinstellungen des Browsers, um die
Zeit anzuzeigen. Sie können das 12- oder 24-Stunden-Format erzwingen.
font-size: Schriftgröße für Logs
color-scheme: Farbschema
options: Optionen
+3 -15
View File
@@ -13,14 +13,9 @@ label:
containers: Contenedores
container: No contenedores | 1 contenedor | {count} contenedores
service: Sin servicios | 1 servicio | {count} servicios
services: Servicios
running-containers: Contenedores en ejecución
all-containers: Todos los contenedores
total-containers: Contenedores Totales
running: En ejecución
total-cpu-usage: Uso total del Procesador
total-mem-usage: Uso total de la Memoria
dozzle-version: Versión de Dozzle
all: Todo
host: Host
hosts: Hosts
password: Contraseña
@@ -32,8 +27,6 @@ label:
avg-mem: Promedio de MEM (%)
pinned: Fijado
per-page: Filas por página
swarm-mode: Modo Swarm
services: Servicios
host-menu: Hosts y Contenedores
swarm-menu: Servicios y Stacks
group-menu: Grupos Personalizados
@@ -51,8 +44,7 @@ error:
events-stream:
title: Error inesperado
message: >-
Dozzle UI no pudo conectar con la API. Por favor, compruebe la configuración de su red.
Si está utilizando un proxy inverso, por favor, asegúrese de que está configurado correctamente.
Dozzle UI no pudo conectar con la API. Por favor, compruebe la configuración de su red. Si está utilizando un proxy inverso, por favor, asegúrese de que está configurado correctamente.
events-timeout:
title: Algo no está bien
message: >-
@@ -86,9 +78,6 @@ settings:
show-timesamps: Mostrar marcas de tiempo
soft-wrap: Líneas de texto con ajuste suave
datetime-format: Sobrescribir el formato de fecha y hora
12-24-format: >-
Por defecto, Dozzle utilizará la configuración regional de tu navegador para formatear la hora. Usted puede
forzar el estilo de 12 o 24 horas.
font-size: Tamaño de letra a utilizar para los registros
color-scheme: Esquema de colores
options: Opciones
@@ -97,8 +86,7 @@ settings:
search: Activar la búsqueda con Dozzle mediante
using-version: Estás usando Dozzle {version}.
update-available: >-
¡La nueva versión está disponible! Actualizar a la
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
¡La nueva versión está disponible! Actualizar a la <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Mostrar etiquetas de salida estándar y salida de error estándar
automatic-redirect: Redireccionar automáticamente a nuevos contenedores con el mismo nombre
compact: Modo compacto
+11 -22
View File
@@ -7,10 +7,13 @@ toolbar:
stop: Stop
start: Démarrer
restart: Redémarrer
show-hostname: Show hostname
show-container-name: Show container name
label:
containers: Conteneurs
container: Pas de conteneur | 1 conteneur | {count} conteneurs
service: Pas de service | 1 service | {count} services
containers: Conteneurs
services: Services
running-containers: Conteneurs en execution
all-containers: Tous les conteneurs
host: Hôte
@@ -24,12 +27,9 @@ label:
avg-mem: Moyenne MEM (%)
pinned: Epinglé
per-page: Colonnes par page
swarm-mode: Mode Swarm
services: Services
host-menu: Hôtes et Conteneurs
swarm-menu: Services et Stacks
group-menu: Groupes Personnalisés
tooltip:
search: Recherche de conteneurs (⌘ + k, ⌃k)
pin-column: Epinglé en colonne
@@ -44,13 +44,11 @@ error:
events-stream:
title: Erreur inattendue
message: >-
L'interface utilisateur de Dozzle n'a pas pu se connecter à l'API. Veuillez vérifier les paramètres de votre réseau.
Si vous utilisez un proxy inversé, assurez-vous qu'il est configuré correctement.
L'interface utilisateur de Dozzle n'a pas pu se connecter à l'API. Veuillez vérifier les paramètres de votre réseau. Si vous utilisez un proxy inversé, assurez-vous qu'il est configuré correctement.
events-timeout:
title: Quelque chose ne va pas
message: >-
La connexion de l'interface utilisateur de Dozzle à l'API a mis trop de temps. Veuillez vérifier la connexion
et réessayez.
La connexion de l'interface utilisateur de Dozzle à l'API a mis trop de temps. Veuillez vérifier la connexion et réessayez.
alert:
redirected:
title: Redirigé vers le nouveau conteneur
@@ -58,8 +56,7 @@ alert:
similar-container-found:
title: Conteneur similaire trouvé
message: >-
Dozzle a trouvé un conteneur similaire {containerId} qui s'exécute sur le même hôte
et basculera automatiquement vers lui à moins que vous ne cliquiez sur "Annuler".
Dozzle a trouvé un conteneur similaire {containerId} qui s'exécute sur le même hôte et basculera automatiquement vers lui à moins que vous ne cliquiez sur "Annuler".
title:
page-not-found: Page non trouvée
login: Authentification requise
@@ -69,6 +66,8 @@ button:
logout: Déconnexion
login: Connexion
settings: Paramètres
cancel: Cancel
redirect: Redirect
placeholder:
search-containers: Recherche de conteneurs (⌘ + k, ⌃k)
search: Recherche
@@ -87,11 +86,10 @@ settings:
search: Activer la recherche avec Dozzle en utilisant
using-version: Vous utilisez Dozzle {version}.
update-available: >-
Nouvelle version disponible! Mise à jour de <a href="{href}" target="_blank"
rel="noreferrer noopener">{nextVersion}</a>.
Nouvelle version disponible! Mise à jour de <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Afficher les étiquettes stdout et stderr
automatic-redirect: Redirection automatique vers de nouveaux conteneurs portant le même nom
cancel: Annuler
compact: Activer le mode compact pour les journaux
log:
preview: Voici un aperçu des logs
warning: Un log d'avertissement ressemble à ceci
@@ -101,15 +99,6 @@ settings:
start-line: Ceci est un message d'erreur multiligne
middle-line: avec une deuxième ligne
end-line: et enfin une troisième ligne.
compact: Activer le mode compact pour les journaux
releases:
features: une nouvelle fonctionnalité | {count} fonctionnalités
bugFixes: un correctif | {count} correctifs
breaking: un changement majeur | {count} changements majeurs
three_parts: "{first}, {second} et {third}"
two_parts: "{first} avec {second}"
latest: Plus récent
no_releases: Vous avez la dernière version
releases:
features: une nouvelle fonctionnalité | {count} fonctionnalités
bugFixes: correction d'un bug | {count} bugs
+19 -32
View File
@@ -11,14 +11,11 @@ toolbar:
show-container-name: Mostra nome container
label:
containers: Container
container: No containers | 1 container | {count} containers
service: No services | 1 service | {count} services
services: Services
running-containers: Container in esecuzione
all-containers: Tutti i Container
total-containers: Container Totali
running: In esecuzione
total-cpu-usage: Utilizzo Totale della CPU
total-mem-usage: Utilizzo Totale della Memoria
dozzle-version: Versione Dozzle
all: Tutto
host: Host
hosts: Hosts
password: Password
@@ -29,15 +26,16 @@ label:
avg-cpu: CPU (%)
avg-mem: MEM (%)
pinned: Spuntato
per-page: Rows per page
host-menu: Host e Container
swarm-menu: Servizi e Stack
group-menu: Gruppi Personalizzati
tooltip:
search: Ricerca container (⌘ + k, ⌃k)
pin-column: Blocca come colonna
merge-services: Unisci tutti i servizi in una vista
merge-containers: Unisci tutti i container in una vista
merge-hosts: Unisci tutti i container su questo host in una vista
search: Ricerca container (⌘ + k, ⌃k)
pin-column: Blocca come colonna
error:
page-not-found: Questa pagina non esiste
invalid-auth: Username o password non valide
@@ -46,29 +44,8 @@ error:
events-stream:
title: Errore inaspettato
message: >-
Dozzle UI non è stato in grado di connettersi alle API. Controlla la tua configurazione di rete.
Se stai utilizzando un reverse proxy, assicurati di averlo configurato correttamente.
Dozzle UI non è stato in grado di connettersi alle API. Controlla la tua configurazione di rete. Se stai utilizzando un reverse proxy, assicurati di averlo configurato correttamente.
events-timeout:
alert:
redirected:
title: Reindirizzato a nuovo container
message: Dozzle ti ha automaticamente reindirizzato al nuovo container {containerId}.
similar-container-found:
title: Trovato container simile
message: >-
Dozzle ha trovato un container simile {containerId} in esecuzione sullo stesso
host e passerà automaticamente ad esso a meno che non si faccia clic su 'Annulla'.
settings:
log:
preview: Questa è un'anteprima dei log
warning: Un log di avviso appare così
complex: Questa è una voce di log complessa in formato json
simple: Questo è un messaggio molto lungo che normalmente andrebbe a capo. La disattivazione del soft wrap disabiliterebbe questo comportamento. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
multi-line-error:
start-line: Questo è un messaggio di errore multilinea
middle-line: con una seconda riga
end-line: e infine una terza riga.
title: Qualcosa non è corretto
message: >-
Dozzle UI timed out durante la connessione alle API. Controlla la configurazione di rete e riprova.
@@ -89,6 +66,8 @@ button:
logout: Logout
login: Login
settings: Configurazione
cancel: Cancel
redirect: Redirect
placeholder:
search-containers: Ricerca container (⌘ + k, ⌃k)
search: Cerca
@@ -107,11 +86,19 @@ settings:
search: Abilita la ricerca attraverso Dozzle
using-version: Stai utilizzando Dozzle {version}.
update-available: >-
Nuova versione disponibile! Aggiorna a <a href="{href}" target="_blank"
rel="noreferrer noopener">{nextVersion}</a>.
Nuova versione disponibile! Aggiorna a <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Visualizza stdout e stderr
automatic-redirect: Automaticamente reindirizza al nuovo container con lo stesso nome
compact: Abilita il compact mode per i log
log:
preview: This is a preview of the logs
warning: A warning log looks like this
complex: This is a complex log entry as json
simple: This is a very very long message which would wrap by default. Disabling soft wraps would disable this. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
multi-line-error:
start-line: This is a multi line error message
middle-line: with a second line
end-line: and finally third line.
releases:
features: nuova funzione | {count} funzioni
bugFixes: nuovo bug fix | {count} fix
+116
View File
@@ -0,0 +1,116 @@
toolbar:
clear: Limpar
download: Baixar
search: Pesquisar
show: Mostrar {std}
show-all: Mostrar tudo
stop: Parar
start: Iniciar
restart: Reiniciar
show-hostname: Mostrar nome do host
show-container-name: Mostrar nome do container
label:
containers: Contêineres
container: Nenhum container | 1 container | {count} containers
service: Nenhum serviço | 1 serviço | {count} serviços
services: Serviços
running-containers: Containers em Execução
all-containers: Todos os Containers
host: Host
hosts: Hosts
password: Senha
username: Usuário
container-name: Nome do Container
status: Status
created: Criado
avg-cpu: Média CPU (%)
avg-mem: Média MEM (%)
pinned: Fixado
per-page: Linhas por página
host-menu: Hosts e Containers
swarm-menu: Serviços e Stacks
group-menu: Grupos Personalizados
tooltip:
search: Pesquisar containers (⌘ + k, ⌃k)
pin-column: Fixar como coluna
merge-services: Mesclar todos os serviços em uma visualização
merge-containers: Mesclar todos os containers em uma visualização
merge-hosts: Mesclar todos os containers neste host em uma visualização
error:
page-not-found: Esta página não existe
invalid-auth: Usuário ou senha inválidos
logs-skipped: \{total} entradas ignoradas
container-not-found: Container não encontrado
events-stream:
title: Erro Inesperado
message: >-
A interface do Dozzle não conseguiu se conectar à API. Verifique suas configurações de rede. Se você estiver usando um proxy reverso, certifique-se de que ele esteja configurado corretamente.
events-timeout:
title: Algo não está certo
message: >-
A interface do Dozzle atingiu o tempo limite ao conectar-se à API. Verifique a conexão de rede e tente novamente.
alert:
redirected:
title: Redirecionado para novo container
message: O Dozzle redirecionou você automaticamente para o novo container {containerId}.
similar-container-found:
title: Container similar encontrado
message: >-
O Dozzle encontrou um container similar {containerId} que está em execução no mesmo host e mudará automaticamente para ele, a menos que você clique em 'Cancelar'.
title:
page-not-found: Página não encontrada
login: Autenticação Requerida
dashboard: 1 container | {count} containers
settings: Configurações
button:
logout: Sair
login: Entrar
settings: Configurações
cancel: Cancelar
redirect: Redirecionar
placeholder:
search-containers: Pesquisar containers (⌘ + k, ⌃k)
search: Pesquisar
settings:
display: Exibição
locale: Sobrescrever idioma
small-scrollbars: Usar barras de rolagem menores
show-timesamps: Mostrar marcas de tempo
soft-wrap: Quebrar linhas automaticamente
datetime-format: Sobrescrever formato de data e hora
font-size: Tamanho da fonte para logs
color-scheme: Esquema de cores
options: Opções
show-stopped-containers: Mostrar containers parados
about: Sobre
search: Habilitar pesquisa com Dozzle usando
using-version: Você está usando Dozzle {version}.
update-available: >-
Nova versão disponível! Atualize para <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Mostrar rótulos stdout e stderr
automatic-redirect: Redirecionar automaticamente para novos containers com o mesmo nome
compact: Ativar modo compacto para logs
log:
preview: Esta é uma prévia dos logs
warning: Um log de aviso se parece com isto
complex: Esta é uma entrada de log complexa como json
simple: Esta é uma mensagem muito longa que seria quebrada por padrão. Desabilitar quebras suaves desabilitaria isso. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
multi-line-error:
start-line: Esta é uma mensagem de erro multi-linha
middle-line: com uma segunda linha
end-line: e finalmente a terceira linha.
releases:
features: uma nova funcionalidade | {count} funcionalidades
bugFixes: uma correção de bug | {count} correções
breaking: uma mudança incompatível | {count} mudanças incompatíveis
three_parts: "{first}, {second} e {third}"
two_parts: "{first} com {second}"
latest: Mais recente
no_releases: Você tem a versão mais recente
log_actions:
copy_log: Copiar log
jump_to_context: Ir para contexto
toasts:
copied:
title: Copiado
message: Log copiado para a área de transferência
+32 -13
View File
@@ -1,21 +1,21 @@
toolbar:
clear: Очистить
show-hostname: Показать имя хоста
show-container-name: Показать имя контейнера
download: Скачать
search: Поиск
show: Показать только {std}
show-all: Показать все потоки
stop: Stop
start: Start
restart: Restart
show-hostname: Показать имя хоста
show-container-name: Показать имя контейнера
label:
containers: Контейнеры
container: No containers | 1 container | {count} containers
service: No services | 1 service | {count} services
services: Services
running-containers: Запущенные контейнеры
all-containers: Все контейнеры
total-containers: Всего Контейнеров
running: Запущенные
total-cpu-usage: Использование процессора
total-mem-usage: Использование памяти
dozzle-version: Версия Dozzle
all: Все
host: Хост
hosts: Хосты
password: Пароль
@@ -25,12 +25,17 @@ label:
created: Создан
avg-cpu: средний процессор (%)
avg-mem: средняя память (%)
pinned: Pinned
per-page: Rows per page
host-menu: Хосты и Контейнеры
swarm-menu: Сервисы и Стеки
group-menu: Пользовательские Группы
tooltip:
search: Поиск контейнеров (⌘ + k, ⌃k)
pin-column: Закрепить столбец
merge-services: Merge all services into one view
merge-containers: Merge all containers into one view
merge-hosts: Merge all containers on this host into one view
error:
page-not-found: Эта страница не доступна.
invalid-auth: Имя пользователя или пароль неверны.
@@ -39,8 +44,7 @@ error:
events-stream:
title: Неожиданная ошибка
message: >-
Dozzle UI не смог подключиться к API. Пожалуйста, проверьте настройки сети.
Если вы используете обратный прокси, убедитесь, что он настроен правильно.
Dozzle UI не смог подключиться к API. Пожалуйста, проверьте настройки сети. Если вы используете обратный прокси, убедитесь, что он настроен правильно.
events-timeout:
title: Что-то не так
message: >-
@@ -62,6 +66,8 @@ button:
logout: Выйти
login: Войти
settings: Настройки
cancel: Cancel
redirect: Redirect
placeholder:
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
search: Поиск
@@ -71,13 +77,11 @@ settings:
small-scrollbars: Уменьшенная полоса прокрутки
show-timesamps: Показывать временные метки
soft-wrap: Плавный перенос текста
12-24-format: >-
Формат времени. По умолчанию, Dozzle будет использовать локализацию вашего браузера.
datetime-format: Формат даты и времени
font-size: Размер шрифта
color-scheme: Цветовая схема
options: Опции
show-stopped-containers: Показывать остановленные контейнеры
datetime-format: Формат даты и времени
about: Информация
search: Включить поиск с помощью Dozzle, используя
using-version: Вы используете версию Dozzle {version}.
@@ -95,3 +99,18 @@ settings:
start-line: Это многострочное сообщение об ошибке
middle-line: со второй строкой
end-line: и наконец третьей строкой.
releases:
features: one new feature | {count} features
bugFixes: one bug fix | {count} fixes
breaking: one breaking change | {count} breaking changes
three_parts: "{first}, {second} and {third}"
two_parts: "{first} with {second}"
latest: Latest
no_releases: You have the latest version
log_actions:
copy_log: Copy log
jump_to_context: Jump to context
toasts:
copied:
title: Copied
message: Log copied to clipboard
+4 -12
View File
@@ -10,17 +10,12 @@ toolbar:
show-hostname: 显示主机名
show-container-name: 显示容器名称
label:
service: 无服务 | 1 个服务 | {count} 个服务
total-containers: 容器总数
total-cpu-usage: CPU总使用率
total-mem-usage: 内存总使用率
containers: 容器
container: 无容器 | 1 容器 | {count} 容器
running: 运行中
dozzle-version: Dozzle 版本
service: 无服务 | 1 个服务 | {count} 个服务
services: 服务
running-containers: 运行中的容器
all-containers: 所有容器
all: 全部
host: 主机
hosts: 主机
password: 密码
@@ -32,9 +27,6 @@ label:
avg-mem: 平均MEM (%)
pinned: 固定
per-page: 每页行数
swarm-mode: 集群模式
services: 服务
custom-groups: 自定义组
host-menu: 主机和容器
swarm-menu: 服务和堆栈
group-menu: 自定义组
@@ -43,6 +35,7 @@ tooltip:
pin-column: 固定为列
merge-services: 合并所有服务到一个视图
merge-containers: 合并所有容器到一个视图
merge-hosts: Merge all containers on this host into one view
error:
page-not-found: 此页面不存在。
invalid-auth: 用户名和密码无效。
@@ -51,8 +44,7 @@ error:
events-stream:
title: 意外错误
message: >-
Dozzle UI无法连接到API。请检查您的网络设置。
如果您使用反向代理,请确保其正确配置。
Dozzle UI无法连接到API。请检查您的网络设置。 如果您使用反向代理,请确保其正确配置。
events-timeout:
title: 出了点问题
message: >-
+32 -127
View File
@@ -3,7 +3,6 @@ package main
import (
"context"
"embed"
"io"
"io/fs"
"net"
@@ -16,11 +15,11 @@ import (
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/healthcheck"
"github.com/amir20/dozzle/internal/k8s"
"github.com/amir20/dozzle/internal/support/cli"
docker_support "github.com/amir20/dozzle/internal/support/docker"
k8s_support "github.com/amir20/dozzle/internal/support/k8s"
"github.com/amir20/dozzle/internal/web"
"github.com/rs/zerolog/log"
)
@@ -36,119 +35,13 @@ func main() {
cli.ValidateEnvVars(cli.Args{}, cli.AgentCmd{})
args, subcommand := cli.ParseArgs()
if subcommand != nil {
switch subcommand.(type) {
case *cli.AgentCmd:
client, err := docker.NewLocalClient(args.Hostname)
if err != nil {
log.Fatal().Err(err).Msg("Could not create docker client")
}
certs, err := cli.ReadCertificates(certs)
if err != nil {
log.Fatal().Err(err).Msg("Could not read certificates")
}
listener, err := net.Listen("tcp", args.Agent.Addr)
if err != nil {
log.Fatal().Err(err).Msg("failed to listen")
}
tempFile, err := os.CreateTemp("./", "agent-*.addr")
if err != nil {
log.Fatal().Err(err).Msg("failed to create temp file")
}
io.WriteString(tempFile, listener.Addr().String())
go cli.StartEvent(args, "", client, "agent")
server, err := agent.NewServer(client, certs, args.Version(), args.Filter)
if err != nil {
log.Fatal().Err(err).Msg("failed to create agent server")
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
log.Info().Msgf("Dozzle agent version %s", args.Version())
log.Info().Msgf("Agent listening on %s", listener.Addr().String())
if err := server.Serve(listener); err != nil {
log.Error().Err(err).Msg("failed to serve")
}
}()
<-ctx.Done()
stop()
log.Info().Msg("Shutting down agent")
server.Stop()
log.Debug().Str("file", tempFile.Name()).Msg("Removing temp file")
os.Remove(tempFile.Name())
case *cli.HealthcheckCmd:
files, err := os.ReadDir(".")
if err != nil {
log.Fatal().Err(err).Msg("Failed to read directory")
}
agentAddress := ""
for _, file := range files {
if match, _ := filepath.Match("agent-*.addr", file.Name()); match {
data, err := os.ReadFile(file.Name())
if err != nil {
log.Fatal().Err(err).Msg("Failed to read file")
}
agentAddress = string(data)
break
}
}
if agentAddress == "" {
if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil {
log.Fatal().Err(err).Msg("Failed to make request")
}
} else {
certs, err := cli.ReadCertificates(certs)
if err != nil {
log.Fatal().Err(err).Msg("Could not read certificates")
}
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
defer cancel()
if err := healthcheck.RPCRequest(ctx, agentAddress, certs); err != nil {
log.Fatal().Err(err).Msg("Failed to make request")
}
}
case *cli.GenerateCmd:
cli.StartEvent(args, "", nil, "generate")
if args.Generate.Username == "" || args.Generate.Password == "" {
log.Fatal().Msg("Username and password are required")
}
buffer := auth.GenerateUsers(auth.User{
Username: args.Generate.Username,
Password: args.Generate.Password,
Name: args.Generate.Name,
Email: args.Generate.Email,
Filter: args.Generate.Filter,
}, true)
if _, err := os.Stdout.Write(buffer.Bytes()); err != nil {
log.Fatal().Err(err).Msg("Failed to write to stdout")
}
case *cli.AgentTestCmd:
certs, err := cli.ReadCertificates(certs)
if err != nil {
log.Fatal().Err(err).Msg("Could not read certificates")
}
log.Info().Str("endpoint", args.AgentTest.Address).Msg("Connecting to agent")
agent, err := agent.NewClient(args.AgentTest.Address, certs)
if err != nil {
log.Fatal().Err(err).Str("endpoint", args.AgentTest.Address).Msg("error connecting to agent")
}
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
defer cancel()
host, err := agent.Host(ctx)
if err != nil {
log.Fatal().Err(err).Str("endpoint", args.AgentTest.Address).Msg("error fetching host info for agent")
}
log.Info().Str("endpoint", args.AgentTest.Address).Str("version", host.AgentVersion).Str("name", host.Name).Str("id", host.ID).Msg("Successfully connected to agent")
runnable, ok := subcommand.(cli.Runnable)
if !ok {
log.Fatal().Msg("Invalid command")
}
err := runnable.Run(args, certs)
if err != nil {
log.Fatal().Err(err).Msg("Failed to run command")
}
os.Exit(0)
@@ -160,17 +53,16 @@ func main() {
log.Info().Msgf("Dozzle version %s", args.Version())
var multiHostService *docker_support.MultiHostService
var hostService web.HostService
if args.Mode == "server" {
var localClient container.Client
localClient, multiHostService = cli.CreateMultiHostService(certs, args)
multiHostService := cli.CreateMultiHostService(certs, args)
if multiHostService.TotalClients() == 0 {
log.Fatal().Msg("Could not connect to any Docker Engine")
} else {
log.Info().Int("clients", multiHostService.TotalClients()).Msg("Connected to Docker")
}
go cli.StartEvent(args, "server", localClient, "")
hostService = multiHostService
} else if args.Mode == "swarm" {
localClient, err := docker.NewLocalClient("")
if err != nil {
@@ -182,7 +74,7 @@ func main() {
}
agentManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs)
manager := docker_support.NewSwarmClientManager(localClient, certs, args.Timeout, agentManager, args.Filter)
multiHostService = docker_support.NewMultiHostService(manager, args.Timeout)
hostService = docker_support.NewMultiHostService(manager, args.Timeout)
log.Info().Msg("Starting in swarm mode")
listener, err := net.Listen("tcp", ":7007")
if err != nil {
@@ -194,16 +86,29 @@ func main() {
}
go cli.StartEvent(args, "swarm", localClient, "")
go func() {
log.Info().Msgf("Dozzle agent version %s", args.Version())
log.Info().Msgf("Dozzle agent version in swarm mode %s", args.Version())
if err := server.Serve(listener); err != nil {
log.Error().Err(err).Msg("failed to serve")
}
}()
} else if args.Mode == "k8s" {
localClient, err := k8s.NewK8sClient(args.Namespace)
if err != nil {
log.Fatal().Err(err).Msg("Could not create k8s client")
}
clusterService, err := k8s_support.NewK8sClusterService(localClient, args.Timeout)
if err != nil {
log.Fatal().Err(err).Msg("Could not create k8s cluster service")
}
go cli.StartEvent(args, "k8s", localClient, "")
hostService = clusterService
} else {
log.Fatal().Str("mode", args.Mode).Msg("Invalid mode")
}
srv := createServer(args, multiHostService)
srv := createServer(args, hostService)
go func() {
log.Info().Msgf("Accepting connections on %s", args.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
@@ -224,7 +129,7 @@ func main() {
log.Debug().Msg("shut down complete")
}
func createServer(args cli.Args, multiHostService *docker_support.MultiHostService) *http.Server {
func createServer(args cli.Args, hostService web.HostService) *http.Server {
_, dev := os.LookupEnv("DEV")
var provider web.AuthProvider = web.NONE
@@ -286,7 +191,7 @@ func createServer(args cli.Args, multiHostService *docker_support.MultiHostServi
TTL: authTTL,
},
EnableActions: args.EnableActions,
Filter: args.Filter,
Labels: args.Filter,
}
assets, err := fs.Sub(content, "dist")
@@ -313,5 +218,5 @@ func createServer(args cli.Args, multiHostService *docker_support.MultiHostServi
}
}
return web.CreateServer(multiHostService, assets, config)
return web.CreateServer(hostService, assets, config)
}
+30 -30
View File
@@ -1,12 +1,12 @@
{
"name": "dozzle",
"version": "8.10.7",
"version": "8.11.7",
"description": "Realtime log viewer for docker containers.",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
"url": "https://github.com/amir20/dozzle/issues"
},
"packageManager": "pnpm@10.2.1",
"packageManager": "pnpm@10.5.2",
"type": "module",
"repository": {
"type": "git",
@@ -30,22 +30,22 @@
},
"dependencies": {
"@duckdb/duckdb-wasm": "1.29.0",
"@iconify-json/carbon": "^1.2.6",
"@iconify-json/carbon": "^1.2.8",
"@iconify-json/cil": "^1.2.2",
"@iconify-json/ic": "^1.2.2",
"@iconify-json/material-symbols": "^1.2.14",
"@iconify-json/material-symbols": "^1.2.15",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/mdi-light": "^1.2.2",
"@iconify-json/octicon": "^1.2.4",
"@iconify-json/octicon": "^1.2.5",
"@iconify-json/ph": "^1.2.2",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@oddbird/css-anchor-positioning": "^0.4.0",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "4.0.5",
"@vueuse/components": "^12.5.0",
"@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^12.5.0",
"@vueuse/router": "^12.5.0",
"@tailwindcss/vite": "4.0.12",
"@vueuse/components": "^12.8.2",
"@vueuse/core": "^12.8.2",
"@vueuse/integrations": "^12.8.2",
"@vueuse/router": "^12.8.2",
"ansi-to-html": "^0.7.2",
"d3-array": "^3.2.4",
"d3-ease": "^3.0.1",
@@ -53,34 +53,34 @@
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"d3-transition": "^3.0.1",
"daisyui": "5.0.0-beta.7",
"daisyui": "5.0.0",
"date-fns": "^4.1.0",
"entities": "^6.0.0",
"fuse.js": "^7.1.0",
"lodash.debounce": "^4.0.8",
"pinia": "^2.3.1",
"pinia": "^3.0.1",
"sortablejs": "^1.15.6",
"splitpanes": "^3.1.8",
"strip-ansi": "^7.1.0",
"tailwindcss": "4.0.5",
"unplugin-auto-import": "^19.0.0",
"unplugin-icons": "^22.0.0",
"unplugin-vue-components": "^28.0.0",
"unplugin-vue-macros": "^2.14.2",
"unplugin-vue-router": "^0.11.2",
"vite": "6.1.0",
"tailwindcss": "4.0.12",
"unplugin-auto-import": "^19.1.1",
"unplugin-icons": "^22.1.0",
"unplugin-vue-components": "^28.4.1",
"unplugin-vue-macros": "^2.14.5",
"unplugin-vue-router": "^0.12.0",
"vite": "6.2.1",
"vite-plugin-compression2": "^1.3.3",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-svg-loader": "^5.1.0",
"vitepress": "1.6.3",
"vue": "^3.5.13",
"vue-i18n": "^11.1.1",
"vue-i18n": "^11.1.2",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@apache-arrow/esnext-esm": "^19.0.0",
"@pinia/testing": "^0.1.7",
"@playwright/test": "^1.50.1",
"@apache-arrow/esnext-esm": "^19.0.1",
"@pinia/testing": "^1.0.0",
"@playwright/test": "^1.51.0",
"@types/d3-array": "^3.2.1",
"@types/d3-ease": "^3.0.2",
"@types/d3-scale": "^4.0.9",
@@ -88,24 +88,24 @@
"@types/d3-shape": "^3.1.7",
"@types/d3-transition": "^3.0.9",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^22.13.1",
"@types/node": "^22.13.9",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "^3.5.13",
"@vue/test-utils": "^2.4.6",
"bumpp": "^10.0.2",
"bumpp": "^10.0.3",
"c8": "^10.1.3",
"concurrently": "^9.1.2",
"eventsourcemock": "^2.0.0",
"jsdom": "^26.0.0",
"lint-staged": "^15.4.3",
"prettier": "^3.5.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"simple-git-hooks": "^2.11.1",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"vitest": "^3.0.5",
"vue-component-type-helpers": "^2.2.0",
"vue-tsc": "^2.2.0"
"typescript": "^5.8.2",
"vitest": "^3.0.8",
"vue-component-type-helpers": "^2.2.8",
"vue-tsc": "^2.2.8"
},
"lint-staged": {
"*.{js,vue,css,ts,html,md}": [
+1177 -941
View File
File diff suppressed because it is too large Load Diff