From 872729a93b8d22ccd43903b2f18a1d587b72879d Mon Sep 17 00:00:00 2001 From: Amir Raminfar Date: Fri, 24 Feb 2023 09:42:58 -0800 Subject: [PATCH] Adds support for multiple hosts (#2059) * Adds support for multiple hosts * Adds UI for drop down * Adds support for TLS and remove SSH * Changes dropdown to only show up with >1 hosts * Fixes js tests * Fixes go tests * Fixes download link * Updates readme * Removes unused imports * Fixes spaces --- README.md | 34 ++++++++--- analytics/types.go | 19 ++++--- assets/auto-imports.d.ts | 2 + .../LogViewer/LogActionsToolbar.vue | 2 +- .../LogViewer/LogEventSource.spec.ts | 40 ++++++------- assets/components/SideMenu.vue | 19 +++++++ assets/composables/eventsource.ts | 4 +- assets/composables/storage.ts | 3 + assets/stores/config.ts | 3 + assets/stores/container.ts | 56 ++++++++++++------- docker/client.go | 47 ++++++++++++++++ index.html | 3 +- main.go | 40 ++++++++----- web/events.go | 34 ++++++----- web/logs.go | 10 ++-- web/routes.go | 28 ++++++++-- web/routes_logs_test.go | 45 ++++++++++++--- web/routes_test.go | 5 +- 18 files changed, 285 insertions(+), 109 deletions(-) create mode 100644 assets/composables/storage.ts diff --git a/README.md b/README.md index 721aeabf..da369f65 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,33 @@ The simplest way to use dozzle is to run the docker container. Also, mount the D Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`. -### With Docker swarm +### Connecting to remote hosts - docker service create \ - --name=dozzle \ - --publish=8888:8080 \ - --constraint=node.role==manager \ - --mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ - amir20/dozzle:latest +Dozzle supports connecting to multiple remote hosts via `tcp://` using TLS or without. Appropriate certs need to be mounted for Dozzle to be able to successfully connect. At this point, `ssh://` is not supported because Dozzle docker image does not ship with any ssh clients. + +To configure remote hosts, `--remote-host` or `DOZZLE_REMOTE_HOST` need to provided and the `pem` files need to be mounted to `/cert` directory. The `/cert` directory expects to have `/certs/{ca,cert,key}.pem` or `/certs/{host}/{ca,cert,key}.pem` in case of multiple hosts. + +Below are examples of using `--remote-host` via CLI: + + $ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/certs:/certs -p 8080:8080 amir20/dozzle --remote-host tcp://167.99.1.1:2376 + +Multiple `--remote-host` flags can be used to specify multiple hosts. + +Or to use compose: + + version: "3" + services: + dozzle: + image: amir20/dozzle:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /path/to/certs:/certs + ports: + - 8080:8080 + environment: + DOZZLE_REMOTE_HOST: tcp://167.99.1.1:2376,tcp://167.99.1.2:2376 + +You need to make sure appropriate certs are provided in `/certs/167.99.1.1/{ca,cert,key}.pem` and `/certs/167.99.1.2/{ca,cert,key}.pem` for both hosts to work. ### With Docker compose @@ -129,6 +148,7 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can | `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` | | `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` | | `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false | +| `--remote-host` | `DOZZLE_REMOTE_HOST` | | ## Troubleshooting and FAQs diff --git a/analytics/types.go b/analytics/types.go index 6368ce78..f84affea 100644 --- a/analytics/types.go +++ b/analytics/types.go @@ -1,17 +1,18 @@ package analytics type StartEvent struct { - ClientId string `json:"-"` - Version string `json:"version"` - FilterLength int `json:"filterLength"` - CustomAddress bool `json:"customAddress"` - CustomBase bool `json:"customBase"` - Protected bool `json:"protected"` - HasHostname bool `json:"hasHostname"` + ClientId string `json:"-"` + Version string `json:"version"` + FilterLength int `json:"filterLength"` + RemoteHostLength int `json:"remoteHostLength"` + CustomAddress bool `json:"customAddress"` + CustomBase bool `json:"customBase"` + Protected bool `json:"protected"` + HasHostname bool `json:"hasHostname"` } type RequestEvent struct { - ClientId string `json:"-"` - TotalContainers int `json:"totalContainers"` + ClientId string `json:"-"` + TotalContainers int `json:"totalContainers"` RunningContainers int `json:"runningContainers"` } diff --git a/assets/auto-imports.d.ts b/assets/auto-imports.d.ts index b7b6cf99..ad8c4edc 100644 --- a/assets/auto-imports.d.ts +++ b/assets/auto-imports.d.ts @@ -109,6 +109,7 @@ declare global { const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const search: typeof import('./composables/settings')['search'] + const sessionHost: typeof import('./composables/storage')['sessionHost'] const setActivePinia: typeof import('pinia')['setActivePinia'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const setTitle: typeof import('./composables/title')['setTitle'] @@ -431,6 +432,7 @@ declare module 'vue' { readonly resolveRef: UnwrapRef readonly resolveUnref: UnwrapRef readonly search: UnwrapRef + readonly sessionHost: UnwrapRef readonly setActivePinia: UnwrapRef readonly setMapStoreSuffix: UnwrapRef readonly setTitle: UnwrapRef diff --git a/assets/components/LogViewer/LogActionsToolbar.vue b/assets/components/LogViewer/LogActionsToolbar.vue index 3ff06fce..67c59042 100644 --- a/assets/components/LogViewer/LogActionsToolbar.vue +++ b/assets/components/LogViewer/LogActionsToolbar.vue @@ -12,7 +12,7 @@ - +
diff --git a/assets/components/LogViewer/LogEventSource.spec.ts b/assets/components/LogViewer/LogEventSource.spec.ts index 7ae58d65..baeabd46 100644 --- a/assets/components/LogViewer/LogEventSource.spec.ts +++ b/assets/components/LogViewer/LogEventSource.spec.ts @@ -48,7 +48,7 @@ describe("", () => { ) { settings.value.hourStyle = hourStyle; search.searchFilter.value = searchFilter; - if(searchFilter){ + if (searchFilter) { search.showSearch.value = true; } @@ -91,22 +91,22 @@ describe("", () => { test("should connect to EventSource", async () => { const wrapper = createLogEventSource(); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); - expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(1); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); + expect(sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].readyState).toBe(1); wrapper.unmount(); }); test("should close EventSource", async () => { const wrapper = createLogEventSource(); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); wrapper.unmount(); - expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(2); + expect(sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].readyState).toBe(2); }); test("should parse messages", async () => { const wrapper = createLogEventSource(); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); - sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`, }); @@ -121,8 +121,8 @@ describe("", () => { describe("render html correctly", () => { test("should render messages", async () => { const wrapper = createLogEventSource(); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); - sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`, }); @@ -134,8 +134,8 @@ describe("", () => { test("should render messages with color", async () => { const wrapper = createLogEventSource(); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); - sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}', }); @@ -147,8 +147,8 @@ describe("", () => { test("should render messages with html entities", async () => { const wrapper = createLogEventSource(); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); - sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ data: `{"ts":1560336942459, "m":"foo bar", "id":1}`, }); @@ -160,8 +160,8 @@ describe("", () => { test("should render dates with 12 hour style", async () => { const wrapper = createLogEventSource({ hourStyle: "12" }); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); - sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ data: `{"ts":1560336942459, "m":"foo bar", "id":1}`, }); @@ -173,8 +173,8 @@ describe("", () => { test("should render dates with 24 hour style", async () => { const wrapper = createLogEventSource({ hourStyle: "24" }); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); - sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ data: `{"ts":1560336942459, "m":"foo bar", "id":1}`, }); @@ -186,11 +186,11 @@ describe("", () => { test("should render messages with filter", async () => { const wrapper = createLogEventSource({ searchFilter: "test" }); - sources["/api/logs/stream?id=abc&lastEventId="].emitOpen(); - sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ data: `{"ts":1560336942459, "m":"foo bar", "id":1}`, }); - sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ + sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ data: `{"ts":1560336942459, "m":"test bar", "id":2}`, }); diff --git a/assets/components/SideMenu.vue b/assets/components/SideMenu.vue index 449da93e..b923f5a2 100644 --- a/assets/components/SideMenu.vue +++ b/assets/components/SideMenu.vue @@ -8,10 +8,27 @@ + {{ hostname }} +
+ + + + + {{ value }} + + +
@@ -71,6 +88,7 @@