Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f02de2070 | |||
| bb085affb2 | |||
| 1d022b51b5 | |||
| eb31b46e6f | |||
| 066541288a | |||
| eba7d92ba6 | |||
| 579ae12920 | |||
| ba9531dc59 | |||
| c4cc233720 | |||
| 6c1f59c7e3 | |||
| ebc02900b6 | |||
| 4e5f7ae1cf | |||
| 55f076382e | |||
| cae8f2740d | |||
| c75b5a598d | |||
| 701f16fcc2 | |||
| 49d53ce255 | |||
| e57d038221 | |||
| c74384d012 | |||
| 5a068133bf | |||
| e85a4aefa2 | |||
| b0e43333d2 | |||
| d2c71e2dc9 | |||
| 7fdaa3e4f0 | |||
| 0344e2adb4 | |||
| 7431c3455f | |||
| 1fe46e605a | |||
| e4d1a18d37 | |||
| a0a2b7175c | |||
| b88fc3c5d1 | |||
| a8bc1dd4c2 | |||
| 29fcb53fac | |||
| 4dfd57612d | |||
| 51b9ad5f00 | |||
| f4b53fd09d | |||
| 5d5628530b | |||
| 4d0904bb2c | |||
| 13ebc06812 | |||
| 7fbf760e9a | |||
| 9a2935d3d4 | |||
| fd24659903 | |||
| d2a43b7e6f | |||
| 2a07ca39db | |||
| 74e44bd5f2 | |||
| be11c32139 | |||
| e55df82023 | |||
| 78c1d72f3d | |||
| caa3bb14ab | |||
| 2881f25a1a | |||
| 11a972b048 | |||
| 8fc308d294 | |||
| 33e7b84061 | |||
| 470d6ed0d9 | |||
| 01f3c5ee15 | |||
| 79f8efd909 | |||
| 021d3f7a99 | |||
| bf79b213c6 | |||
| f216587c9b | |||
| 803b66188e | |||
| 37d2b07720 | |||
| 9879004ae3 | |||
| 0c5c08deeb | |||
| 89a6fbfab2 | |||
| 5b1f3892a7 | |||
| 37c6033371 | |||
| 4f748bf9e1 | |||
| c0b7f960f4 | |||
| f35e2c0ace | |||
| 69e434f14b | |||
| f47f60e51c | |||
| 55775f651f | |||
| 59c72f91ce | |||
| 732df996bf | |||
| a7f602449b | |||
| 84c062c13d | |||
| 194fcd0dea | |||
| ae1c0207de | |||
| 3ca2308be8 | |||
| bf000a1028 | |||
| 9b2cf1254e | |||
| f4bc3bdc21 | |||
| 0e135d0eac | |||
| 8f314f5e6f | |||
| f1dad96b86 | |||
| 32bc9a407b | |||
| a0e3baabbf | |||
| b40b23b76f | |||
| aa2a9dc7a0 | |||
| 6fe883f4be | |||
| 10721f5d07 | |||
| f6ef1f7381 | |||
| 32f9364f2b | |||
| 233c546f92 | |||
| 666ad7ba05 |
@@ -9,13 +9,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
@@ -42,13 +42,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
@@ -79,11 +79,11 @@ jobs:
|
||||
steps:
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: amir20/dozzle
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: TAG=${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
- name: Release to Github
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -14,11 +14,11 @@ jobs:
|
||||
steps:
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: amir20/dozzle
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: TAG=${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Sample workflow for building and deploying a VitePress site to GitHub Pages
|
||||
#
|
||||
name: Deploy VitePress site to Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Not needed if lastUpdated is not enabled
|
||||
- uses: pnpm/action-setup@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: pnpm # or pnpm / yarn
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Build with VitePress
|
||||
run: |
|
||||
pnpm docs:build
|
||||
touch docs/.vitepress/dist/.nojekyll
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
@@ -0,0 +1,20 @@
|
||||
name: "Lint PR"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -12,13 +12,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
@@ -65,13 +65,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
certs
|
||||
data
|
||||
dist
|
||||
node_modules
|
||||
.cache
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine as node
|
||||
FROM --platform=$BUILDPLATFORM node:21-alpine as node
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
@@ -22,7 +22,7 @@ COPY public ./public
|
||||
# Build assets
|
||||
RUN pnpm build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21.1-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21.3-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache ca-certificates && mkdir /dozzle
|
||||
|
||||
@@ -36,10 +36,7 @@ RUN go mod download
|
||||
COPY --from=node /build/dist ./dist
|
||||
|
||||
# Copy all other files
|
||||
COPY analytics ./analytics
|
||||
COPY healthcheck ./healthcheck
|
||||
COPY docker ./docker
|
||||
COPY web ./web
|
||||
COPY internal ./internal
|
||||
COPY main.go ./
|
||||
|
||||
# Args
|
||||
@@ -49,11 +46,12 @@ ARG TARGETOS TARGETARCH
|
||||
# Build binary
|
||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle
|
||||
|
||||
RUN mkdir /data
|
||||
|
||||
FROM scratch
|
||||
|
||||
ENV PATH /bin
|
||||
|
||||
COPY --from=builder /data /data
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /dozzle/dozzle /dozzle
|
||||
|
||||
|
||||
@@ -15,71 +15,27 @@ https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b2
|
||||
- Search logs using regex 🔦
|
||||
- Small memory footprint 🏎
|
||||
- Split screen for viewing multiple logs
|
||||
- Download logs easy
|
||||
- Download logs easily
|
||||
- Live stats with memory and CPU usage
|
||||
- Authentication with username and password 🚨
|
||||
- Multi-user authentication with support for proxy forward authorization 🚨
|
||||
|
||||
Dozzle should work for most. It has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
|
||||
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
|
||||
|
||||
Dozzle doesn't cost any money and aims to focus on real-time debugging.
|
||||
## Getting Started
|
||||
|
||||
## Getting Dozzle
|
||||
|
||||
Dozzle is a very small Docker container (4 MB compressed). Pull the latest release from the index:
|
||||
Dozzle is a small container (4 MB compressed). Pull the latest release with:
|
||||
|
||||
$ docker pull amir20/dozzle:latest
|
||||
|
||||
## Using Dozzle
|
||||
### Running Dozzle
|
||||
|
||||
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
|
||||
|
||||
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
|
||||
|
||||
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`.
|
||||
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/).
|
||||
|
||||
### Connecting with Docker compose
|
||||
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
container_name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 9999:8080
|
||||
|
||||
### Connecting to remote hosts
|
||||
|
||||
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.
|
||||
|
||||
### Adding health check
|
||||
|
||||
Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `healthcheck` can be enabled manually.
|
||||
Here is the Docker Compose file:
|
||||
|
||||
version: "3"
|
||||
services:
|
||||
@@ -90,43 +46,44 @@ Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `health
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_LEVEL: trace
|
||||
healthcheck:
|
||||
test: [ "CMD", "/dozzle", "healthcheck" ]
|
||||
interval: 3s
|
||||
timeout: 30s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
#### Security
|
||||
For advanced options like [authentication](https://dozzle.dev/guide/authentication), [remote hosts](https://dozzle.dev/guide/remote-hosts) or common [questions](https://dozzle.dev/guide/faq) see documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
|
||||
|
||||
You can control the device Dozzle binds to by passing `--addr` parameter. For example,
|
||||
## Technical Details
|
||||
|
||||
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --addr localhost:1224
|
||||
Dozzle users automatic API negotiation which works with most Docker configurations. Dozzle also works with [Colima](https://github.com/abiosoft/colima) and [Podman](https://podman.io/).
|
||||
|
||||
will bind to `localhost` on port `1224`. You can then use a reverse proxy to control who can see dozzle.
|
||||
### Installation on podman
|
||||
|
||||
If you wish to restrict the containers shown you can pass the `--filter` parameter. For example,
|
||||
By default Podman doesn't have a background process but you can enable this for Dozzle to work.
|
||||
|
||||
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --filter name=foo
|
||||
Verify first if your podman installation has enabled remote socket:
|
||||
|
||||
this would then only allow you to view containers with a name starting with "foo". You can use other filters like `status` as well, please check the official docker [command line docs](https://docs.docker.com/engine/reference/commandline/ps/#filtering) for available filters. Multiple `--filter` arguments can be provided.
|
||||
```
|
||||
podman info
|
||||
```
|
||||
|
||||
#### Authentication
|
||||
When you get under the key remote socket output like this, its already enabled:
|
||||
|
||||
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use [docker secrets](https://docs.docker.com/engine/swarm/secrets/) `--usernamefile` and `--passwordfile`.
|
||||
```
|
||||
remoteSocket:
|
||||
exists: true
|
||||
path: /run/user/1000/podman/podman.sock
|
||||
```
|
||||
|
||||
#### Changing base URL
|
||||
If it's not enabled please follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
|
||||
|
||||
Dozzle by default mounts to "/". If you want to control the base path you can use the `--base` option. For example, if you want to mount at "/foobar",
|
||||
then you can override by using `--base /foobar`. See env variables below for using `DOZZLE_BASE` to change this.
|
||||
Once you have the podman remote socket you can run Dozzle on podman.
|
||||
|
||||
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest --base /foobar
|
||||
```
|
||||
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 amir20/dozzle:latest
|
||||
```
|
||||
|
||||
Dozzle will be available at [http://localhost:8080/foobar/](http://localhost:8080/foobar/).
|
||||
## Security
|
||||
|
||||
#### Analytics collected
|
||||
Dozzle supports file based authentication and forward proxy like [Authelia](https://www.authelia.com/). These are documented at https://dozzle.dev/guide/authentication.
|
||||
|
||||
## Analytics collected
|
||||
|
||||
Dozzle collects anonymous user configurations using Google Analytics. Why? Dozzle is an open source project with no funding. As a result, there is no time to do user studies of Dozzle. Analytics is collected to prioritize features and fixes based on how people use Dozzle. This data is completely public and can be viewed live using [ Data Studio dashboard](https://datastudio.google.com/s/naeIu0MiWsY).
|
||||
|
||||
@@ -150,78 +107,6 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
|
||||
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
|
||||
|
||||
## Troubleshooting and FAQs
|
||||
|
||||
<details>
|
||||
<summary>I installed Dozzle, but logs are slow or they never load. Help!</summary>
|
||||
|
||||
Dozzle uses Server Sent Events (SSE) which connects to a server using a HTTP stream without closing the connection. If any proxy tries to buffer this connection, then Dozzle never receives the data and hangs forever waiting for the reverse proxy to flush the buffer. Since version `1.23.0`, Dozzle sends the `X-Accel-Buffering: no` header which should stop reverse proxies buffering. However, some proxies may ignore this header. In those cases, you need to explicitly disable any buffering.
|
||||
|
||||
Below is an example with nginx and using `proxy_pass` to disable buffering.
|
||||
|
||||
```
|
||||
server {
|
||||
...
|
||||
|
||||
location / {
|
||||
proxy_pass http://<dozzle.container.ip.address>:8080;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://<dozzle.container.ip.address>:8080;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>What data does Dozzle collect?</summary>
|
||||
|
||||
Dozzle does collect some analytics. Analytics is anonymous usage tracking of the features which are used the most. See the section above on how to disable any analytic collection.
|
||||
|
||||
In the browser, Dozzle has a [strict](https://github.com/amir20/dozzle/blob/master/web/csp.go#L9) Content Security Policy which only allows the following policies:
|
||||
|
||||
- Allow connect to `api.github.com` to fetch most recent version.
|
||||
- Only allow `<script>` and `<style>` files from `self`
|
||||
|
||||
Dozzle opens all links with `rel="noopener"`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>We have tools that uses Dozzle when a new container is created. How can I get a direct link to a container by name?</summary>
|
||||
|
||||
Dozzle has a [special route](https://github.com/amir20/dozzle/blob/master/assets/pages/Show.vue) that can be used to search containers by name and then forward to that container. For example, if you have a container with name `"foo.bar"` and id `abc123`, you can send your users to `/show?name=foo.bar` which will be forwarded to `/container/abc123`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>I installed Dozzle but memory consumption doesn't show up!</summary>
|
||||
|
||||
_This is an issue specific to ARM devices_
|
||||
|
||||
Dozzle uses the Docker API to gather information about the containers' memory usage. If the memory usage is not showing up, then it is likely that the Docker API is not returning the memory usage.
|
||||
|
||||
You can verify this by running `docker info`, and you should see the following:
|
||||
|
||||
```
|
||||
WARNING: No memory limit support
|
||||
WARNING: No swap limit support
|
||||
```
|
||||
|
||||
In this case, you'll need to add the following line to your `/boot/cmdline.txt` file and reboot your device.
|
||||
|
||||
```
|
||||
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -232,6 +117,6 @@ To Build and test locally:
|
||||
|
||||
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
|
||||
2. Install [Go](https://go.dev/doc/install).
|
||||
3. Install [reflex](https://github.com/cespare/reflex) with `go get -u github.com/cespare/reflex` outside of dozzle.
|
||||
3. Install [reflex](https://github.com/cespare/reflex) with `go install github.com/cespare/reflex@latest`.
|
||||
4. Install node modules `pnpm install`.
|
||||
5. Do `pnpm dev`
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Dozzle is checked daily with https://docs.docker.com/scout/ and its dependencies are updated regularly. If you find a security bug, please update to the latest. For vulnerability on latest images please file an issue on Github.
|
||||
@@ -12,14 +12,14 @@ declare global {
|
||||
const $ref: typeof import('vue/macros')['$ref']
|
||||
const $shallowRef: typeof import('vue/macros')['$shallowRef']
|
||||
const $toRef: typeof import('vue/macros')['$toRef']
|
||||
const DEFAULT_SETTINGS: typeof import('./composables/settings')['DEFAULT_SETTINGS']
|
||||
const DEFAULT_SETTINGS: typeof import('./stores/settings')['DEFAULT_SETTINGS']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const arrayEquals: typeof import('./utils/index')['arrayEquals']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const automaticRedirect: typeof import('./composables/settings')['automaticRedirect']
|
||||
const collapseNav: typeof import('./composables/settings')['collapseNav']
|
||||
const automaticRedirect: typeof import('./stores/settings')['automaticRedirect']
|
||||
const collapseNav: typeof import('./stores/settings')['collapseNav']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
@@ -58,9 +58,10 @@ declare global {
|
||||
const getDeep: typeof import('./utils/index')['getDeep']
|
||||
const globalShowPopup: typeof import('./composables/popup')['globalShowPopup']
|
||||
const h: typeof import('vue')['h']
|
||||
const hourStyle: typeof import('./composables/settings')['hourStyle']
|
||||
const hourStyle: typeof import('./stores/settings')['hourStyle']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isMobile: typeof import('./composables/media')['isMobile']
|
||||
const isObject: typeof import('./utils/index')['isObject']
|
||||
@@ -68,7 +69,7 @@ declare global {
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const lightTheme: typeof import('./composables/settings')['lightTheme']
|
||||
const lightTheme: typeof import('./stores/settings')['lightTheme']
|
||||
const logicAnd: typeof import('@vueuse/math')['logicAnd']
|
||||
const logicNot: typeof import('@vueuse/math')['logicNot']
|
||||
const logicOr: typeof import('@vueuse/math')['logicOr']
|
||||
@@ -79,7 +80,7 @@ declare global {
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const menuWidth: typeof import('./composables/settings')['menuWidth']
|
||||
const menuWidth: typeof import('./stores/settings')['menuWidth']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
@@ -105,6 +106,7 @@ declare global {
|
||||
const pinnedContainers: typeof import('./composables/storage')['pinnedContainers']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const provideContainerContext: typeof import('./composables/containerContext')['provideContainerContext']
|
||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
@@ -121,21 +123,21 @@ declare global {
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const search: typeof import('./composables/settings')['search']
|
||||
const search: typeof import('./stores/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']
|
||||
const settings: typeof import('./composables/settings')['settings']
|
||||
const settings: typeof import('./stores/settings')['settings']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const showAllContainers: typeof import('./composables/settings')['showAllContainers']
|
||||
const showStd: typeof import('./composables/settings')['showStd']
|
||||
const showTimestamp: typeof import('./composables/settings')['showTimestamp']
|
||||
const size: typeof import('./composables/settings')['size']
|
||||
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
|
||||
const softWrap: typeof import('./composables/settings')['softWrap']
|
||||
const showAllContainers: typeof import('./stores/settings')['showAllContainers']
|
||||
const showStd: typeof import('./stores/settings')['showStd']
|
||||
const showTimestamp: typeof import('./stores/settings')['showTimestamp']
|
||||
const size: typeof import('./stores/settings')['size']
|
||||
const smallerScrollbars: typeof import('./stores/settings')['smallerScrollbars']
|
||||
const softWrap: typeof import('./stores/settings')['softWrap']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const stripVersion: typeof import('./utils/index')['stripVersion']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
@@ -356,11 +358,12 @@ declare global {
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
const withBase: typeof import('./stores/config')['withBase']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
}
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
@@ -373,14 +376,14 @@ declare module 'vue' {
|
||||
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
|
||||
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
|
||||
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
|
||||
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
|
||||
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./composables/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
@@ -417,9 +420,10 @@ declare module 'vue' {
|
||||
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
|
||||
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
|
||||
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
|
||||
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
|
||||
@@ -427,7 +431,7 @@ declare module 'vue' {
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
|
||||
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
@@ -435,7 +439,7 @@ declare module 'vue' {
|
||||
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
|
||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
|
||||
readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
@@ -461,6 +465,7 @@ declare module 'vue' {
|
||||
readonly pinnedContainers: UnwrapRef<typeof import('./composables/storage')['pinnedContainers']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly provideContainerContext: UnwrapRef<typeof import('./composables/containerContext')['provideContainerContext']>
|
||||
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
@@ -477,21 +482,21 @@ declare module 'vue' {
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
|
||||
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
|
||||
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
|
||||
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
|
||||
readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
|
||||
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
|
||||
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
|
||||
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
|
||||
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
|
||||
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
|
||||
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
|
||||
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
|
||||
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
|
||||
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
|
||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||
@@ -699,6 +704,7 @@ declare module 'vue' {
|
||||
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
@@ -710,14 +716,14 @@ declare module '@vue/runtime-core' {
|
||||
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
|
||||
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
|
||||
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
|
||||
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
|
||||
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./composables/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
@@ -754,9 +760,10 @@ declare module '@vue/runtime-core' {
|
||||
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
|
||||
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
|
||||
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
|
||||
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
|
||||
@@ -764,7 +771,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
|
||||
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
@@ -772,7 +779,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
|
||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
|
||||
readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
@@ -798,6 +805,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly pinnedContainers: UnwrapRef<typeof import('./composables/storage')['pinnedContainers']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly provideContainerContext: UnwrapRef<typeof import('./composables/containerContext')['provideContainerContext']>
|
||||
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
@@ -814,21 +822,21 @@ declare module '@vue/runtime-core' {
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
|
||||
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
|
||||
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
|
||||
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
|
||||
readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
|
||||
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
|
||||
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
|
||||
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
|
||||
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
|
||||
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
|
||||
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
|
||||
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
|
||||
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
|
||||
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
|
||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||
@@ -1036,5 +1044,6 @@ declare module '@vue/runtime-core' {
|
||||
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,16 +44,16 @@ declare module 'vue' {
|
||||
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
|
||||
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
|
||||
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
|
||||
'Mdi:check': typeof import('~icons/mdi/check')['default']
|
||||
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
|
||||
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
|
||||
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
|
||||
'Mdi:close': typeof import('~icons/mdi/close')['default']
|
||||
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
|
||||
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
|
||||
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
|
||||
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
|
||||
'Mdi:lightChevronRight': typeof import('~icons/mdi-light/chevron-right')['default']
|
||||
'Mdi:lightCog': typeof import('~icons/mdi-light/cog')['default']
|
||||
'Mdi:lightLogout': typeof import('~icons/mdi-light/logout')['default']
|
||||
'Mdi:lightMagnify': typeof import('~icons/mdi-light/magnify')['default']
|
||||
'MdiLight:check': typeof import('~icons/mdi-light/check')['default']
|
||||
'Mdi:logout': typeof import('~icons/mdi/logout')['default']
|
||||
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
|
||||
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
|
||||
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
|
||||
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="dropdown dropdown-open w-full">
|
||||
<div class="input input-primary flex h-auto items-center">
|
||||
<mdi:light-magnify class="flex h-8 w-8" />
|
||||
<mdi:magnify class="flex h-8 w-8" />
|
||||
<input
|
||||
tabindex="0"
|
||||
class="input input-ghost input-lg flex-1 px-1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex gap-4" v-if="container.stat">
|
||||
<stat-monitor :data="memoryData" label="mem" :stat-value="formatBytes(unref(container.stat).memoryUsage)" />
|
||||
<stat-monitor :data="cpuData" label="load" :stat-value="unref(container.stat).cpu + '%'" />
|
||||
<stat-monitor :data="cpuData" label="load" :stat-value="Math.max(0, unref(container.stat).cpu) + '%'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<a @click.prevent="showSearch = true">
|
||||
<mdi:light-magnify /> {{ $t("toolbar.search") }}
|
||||
<mdi:magnify /> {{ $t("toolbar.search") }}
|
||||
<key-shortcut char="f"></key-shortcut>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createTestingPinia } from "@pinia/testing";
|
||||
import EventSource, { sources } from "eventsourcemock";
|
||||
import LogEventSource from "./LogEventSource.vue";
|
||||
import LogViewer from "./LogViewer.vue";
|
||||
import { settings } from "@/composables/settings";
|
||||
import { settings } from "@/stores/settings";
|
||||
import { useSearchFilter } from "@/composables/search";
|
||||
import { vi, describe, expect, beforeEach, test, afterEach } from "vitest";
|
||||
import { computed, nextTick } from "vue";
|
||||
@@ -14,6 +14,7 @@ import { containerContext } from "@/composables/containerContext";
|
||||
vi.mock("@/stores/config", () => ({
|
||||
__esModule: true,
|
||||
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
|
||||
withBase: (path: string) => path,
|
||||
}));
|
||||
|
||||
/**
|
||||
@@ -88,7 +89,7 @@ describe("<LogEventSource />", () => {
|
||||
});
|
||||
}
|
||||
|
||||
const sourceUrl = "/api/logs/stream/localhost/abc?lastEventId=&stdout=1&stderr=1";
|
||||
const sourceUrl = "/api/logs/stream/localhost/abc?stdout=1&stderr=1";
|
||||
|
||||
test("renders correctly", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
<script lang="ts" setup>
|
||||
const loadingMore = defineEmit<[value: boolean]>();
|
||||
|
||||
const { container, streamConfig } = useContainerContext();
|
||||
const { messages, loadOlderLogs } = useLogStream(container, streamConfig);
|
||||
const { messages, loadOlderLogs } = useLogStream();
|
||||
|
||||
const beforeLoading = () => loadingMore(true);
|
||||
const afterLoading = () => loadingMore(false);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
@click="scrollToBottom()"
|
||||
v-show="paused"
|
||||
>
|
||||
<mdi:light-chevron-double-down />
|
||||
<mdi:chevron-double-down />
|
||||
</button>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-if="search"
|
||||
>
|
||||
<div class="input input-primary flex h-auto items-center">
|
||||
<mdi:light-magnify />
|
||||
<mdi:magnify />
|
||||
<input
|
||||
class="input flex-1"
|
||||
type="text"
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
data-testid="settings"
|
||||
class="btn btn-circle btn-sm"
|
||||
>
|
||||
<mdi:light-cog />
|
||||
<mdi:cog />
|
||||
</router-link>
|
||||
<a :href="`${base}/logout`" :title="$t('button.logout')" v-if="secured" class="btn btn-circle btn-sm">
|
||||
<mdi:light-logout />
|
||||
<mdi:logout />
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
@@ -30,7 +30,7 @@
|
||||
@click="$emit('search')"
|
||||
:title="$t('tooltip.search')"
|
||||
>
|
||||
<mdi:light-magnify />
|
||||
<mdi:magnify />
|
||||
Search
|
||||
<key-shortcut char="k"></key-shortcut>
|
||||
</a>
|
||||
|
||||
@@ -14,7 +14,7 @@ const dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month
|
||||
const use12Hour = $computed(() => ({ auto: undefined, "12": true, "24": false })[hourStyle.value]);
|
||||
const timeFormatter = $computed(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour }),
|
||||
new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: use12Hour }),
|
||||
);
|
||||
|
||||
const dateStr = $computed(() => dateFormatter.format(props.date));
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<slot>
|
||||
<li v-for="item in options">
|
||||
<a @click="modelValue = item.value">
|
||||
<mdi-light:check class="w-4" v-if="modelValue == item.value" />
|
||||
<mdi:check class="w-4" v-if="modelValue == item.value" />
|
||||
<div v-else class="w-4"></div>
|
||||
{{ item.label }}
|
||||
</a>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
|
||||
<mdi:light-magnify class="h-5 w-5" />
|
||||
<mdi:magnify class="h-5 w-5" />
|
||||
</a>
|
||||
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
|
||||
<input type="checkbox" v-model="show" />
|
||||
@@ -30,10 +30,10 @@
|
||||
v-if="config.hosts.length > 1"
|
||||
/>
|
||||
<router-link :to="{ name: 'settings' }" class="btn btn-outline btn-sm">
|
||||
<mdi:light-cog /> {{ $t("button.settings") }}
|
||||
<mdi:cog /> {{ $t("button.settings") }}
|
||||
</router-link>
|
||||
<a class="btn btn-outline btn-sm" :href="`${base}/logout`" :title="$t('button.logout')" v-if="secured">
|
||||
<mdi:light-logout /> {{ $t("button.logout") }}
|
||||
<mdi:logout /> {{ $t("button.logout") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<label class="label inline-flex cursor-pointer gap-4 font-normal">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary hover:bg-primary-focus"
|
||||
:checked="modelValue"
|
||||
@click="toggle()"
|
||||
/>
|
||||
<input type="checkbox" class="toggle toggle-primary hover:bg-primary-focus" v-model="modelValue" />
|
||||
<slot />
|
||||
</label>
|
||||
</template>
|
||||
@@ -14,6 +9,4 @@
|
||||
const { modelValue } = defineModels<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const toggle = useToggle(modelValue);
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
DockerEventLogEntry,
|
||||
SkippedLogsEntry,
|
||||
} from "@/models/LogEntry";
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
function parseMessage(data: string): LogEntry<string | JSONObject> {
|
||||
const e = JSON.parse(data, (key, value) => {
|
||||
@@ -21,15 +20,12 @@ function parseMessage(data: string): LogEntry<string | JSONObject> {
|
||||
return asLogEntry(e);
|
||||
}
|
||||
|
||||
type LogStreamConfig = {
|
||||
stdout: boolean;
|
||||
stderr: boolean;
|
||||
};
|
||||
|
||||
export function useLogStream(container: Ref<Container>, streamConfig: LogStreamConfig) {
|
||||
export function useLogStream() {
|
||||
const { container, streamConfig } = useContainerContext();
|
||||
let messages: LogEntry<string | JSONObject>[] = $ref([]);
|
||||
let buffer: LogEntry<string | JSONObject>[] = $ref([]);
|
||||
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
|
||||
let containerId = container.value.id;
|
||||
|
||||
function flushNow() {
|
||||
if (messages.length > config.maxLogs) {
|
||||
@@ -57,50 +53,54 @@ export function useLogStream(container: Ref<Container>, streamConfig: LogStreamC
|
||||
}
|
||||
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
|
||||
let es: EventSource | null = null;
|
||||
let lastEventId = "";
|
||||
|
||||
function close() {
|
||||
if (es) {
|
||||
es.close();
|
||||
console.debug(`EventSource closed for ${containerId}`);
|
||||
es = null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
flushBuffer.cancel();
|
||||
messages = [];
|
||||
buffer = [];
|
||||
console.debug(`Clearing messages for ${containerId}`);
|
||||
}
|
||||
|
||||
function connect({ clear } = { clear: true }) {
|
||||
es?.close();
|
||||
close();
|
||||
|
||||
if (clear) {
|
||||
flushBuffer.cancel();
|
||||
messages = [];
|
||||
buffer = [];
|
||||
lastEventId = "";
|
||||
clearMessages();
|
||||
}
|
||||
|
||||
const params = {
|
||||
lastEventId,
|
||||
} as { lastEventId: string; stdout?: string; stderr?: string };
|
||||
const params = Object.entries(streamConfig)
|
||||
.filter(([, value]) => value)
|
||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
|
||||
|
||||
if (streamConfig.stdout) {
|
||||
params.stdout = "1";
|
||||
}
|
||||
if (streamConfig.stderr) {
|
||||
params.stderr = "1";
|
||||
}
|
||||
containerId = container.value.id;
|
||||
|
||||
console.debug(`Connecting to ${containerId} with params`, params);
|
||||
|
||||
es = new EventSource(
|
||||
`${config.base}/api/logs/stream/${container.value.host}/${container.value.id}?${new URLSearchParams(
|
||||
params,
|
||||
).toString()}`,
|
||||
withBase(`/api/logs/stream/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`),
|
||||
);
|
||||
es.addEventListener("container-stopped", () => {
|
||||
es?.close();
|
||||
es = null;
|
||||
close();
|
||||
buffer.push(new DockerEventLogEntry("Container stopped", new Date(), "container-stopped"));
|
||||
|
||||
flushBuffer();
|
||||
flushBuffer.flush();
|
||||
});
|
||||
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
|
||||
es.onmessage = (e) => {
|
||||
lastEventId = e.lastEventId;
|
||||
if (e.data) {
|
||||
buffer.push(parseMessage(e.data));
|
||||
flushBuffer();
|
||||
}
|
||||
};
|
||||
es.onerror = () => clearMessages();
|
||||
}
|
||||
|
||||
async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
|
||||
@@ -112,23 +112,13 @@ export function useLogStream(container: Ref<Container>, streamConfig: LogStreamC
|
||||
const delta = to.getTime() - last.getTime();
|
||||
const from = new Date(to.getTime() + delta);
|
||||
|
||||
const params = {
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
} as { from: string; to: string; stdout?: string; stderr?: string };
|
||||
|
||||
if (streamConfig.stdout) {
|
||||
params.stdout = "1";
|
||||
}
|
||||
if (streamConfig.stderr) {
|
||||
params.stderr = "1";
|
||||
}
|
||||
const params = Object.entries(streamConfig)
|
||||
.filter(([, value]) => value)
|
||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), { from: from.toISOString(), to: to.toISOString() });
|
||||
|
||||
const logs = await (
|
||||
await fetch(
|
||||
`${config.base}/api/logs/${container.value.host}/${container.value.id}?${new URLSearchParams(
|
||||
params,
|
||||
).toString()}`,
|
||||
withBase(`/api/logs/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`),
|
||||
)
|
||||
).text();
|
||||
if (logs) {
|
||||
@@ -152,11 +142,7 @@ export function useLogStream(container: Ref<Container>, streamConfig: LogStreamC
|
||||
},
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (es) {
|
||||
es.close();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => close());
|
||||
|
||||
watch(
|
||||
() => container.value.id,
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
v-if="!isMobile"
|
||||
>
|
||||
<input type="checkbox" v-model="collapseNav" />
|
||||
<mdi:light-chevron-right class="swap-on text-secondary" />
|
||||
<mdi:light-chevron-left class="swap-off" />
|
||||
<mdi:chevron-right class="swap-on text-secondary" />
|
||||
<mdi:chevron-left class="swap-off" />
|
||||
</label>
|
||||
</div>
|
||||
<dialog ref="modal" class="modal items-start bg-white/20 backdrop:backdrop-blur-sm" @close="open = false">
|
||||
@@ -65,7 +65,7 @@
|
||||
<script lang="ts" setup>
|
||||
// @ts-ignore - splitpanes types are not available
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import { collapseNav } from "@/composables/settings";
|
||||
import { collapseNav } from "@/stores/settings";
|
||||
const { authorizationNeeded } = config;
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
|
||||
@@ -144,7 +144,15 @@ export class SkippedLogsEntry extends LogEntry<string> {
|
||||
}
|
||||
|
||||
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
|
||||
if (typeof event.m === "string") {
|
||||
if (isObject(event.m)) {
|
||||
return new ComplexLogEntry(
|
||||
event.m,
|
||||
event.id,
|
||||
new Date(event.ts),
|
||||
event.l,
|
||||
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
|
||||
);
|
||||
} else {
|
||||
return new SimpleLogEntry(
|
||||
event.m,
|
||||
event.id,
|
||||
@@ -153,13 +161,5 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
|
||||
event.p,
|
||||
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
|
||||
);
|
||||
} else {
|
||||
return new ComplexLogEntry(
|
||||
event.m,
|
||||
event.id,
|
||||
new Date(event.ts),
|
||||
event.l,
|
||||
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { type App } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
|
||||
const messages = Object.fromEntries(
|
||||
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(([key, value]) => {
|
||||
const yaml = key.endsWith(".yaml");
|
||||
return [key.slice(14, yaml ? -5 : -4), value.default];
|
||||
}),
|
||||
);
|
||||
import messages from "@intlify/unplugin-vue-i18n/messages";
|
||||
|
||||
const locale = messages?.hasOwnProperty(navigator.language) ? navigator.language : navigator.language.slice(0, 2);
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: navigator.language.slice(0, 2),
|
||||
locale: locale,
|
||||
fallbackLocale: "en",
|
||||
messages,
|
||||
});
|
||||
|
||||
export const install = (app: App) => {
|
||||
app.use(i18n);
|
||||
};
|
||||
|
||||
@@ -2,13 +2,12 @@ import { type App } from "vue";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import pages from "~pages";
|
||||
import { setupLayouts } from "virtual:generated-layouts";
|
||||
import config from "@/stores/config";
|
||||
|
||||
export const install = (app: App) => {
|
||||
const routes = setupLayouts(pages);
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(`${config.base}/`),
|
||||
history: createWebHistory(withBase("/")),
|
||||
routes,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-16 px-4 pt-8 md:px-8">
|
||||
<section v-if="config.user">
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<div v-if="config.authProvider === 'simple'">
|
||||
<button @click.prevent="logout()" class="link-primary">{{ $t("button.logout") }}</button>
|
||||
</div>
|
||||
<div>
|
||||
{{ config.user.name ? config.user.name : config.user.email }}
|
||||
</div>
|
||||
<img class="h-10 w-10 rounded-full p-1 ring-2 ring-base-content/50" :src="config.user.avatar" />
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="stats grid bg-base-lighter shadow">
|
||||
<div class="stat">
|
||||
@@ -65,6 +76,14 @@ watchEffect(() => {
|
||||
setTitle(t("title.dashboard", { count: runningContainers.length }));
|
||||
}
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
await fetch(withBase("/api/token"), {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
<style lang="postcss" scoped>
|
||||
:deep(tr td) {
|
||||
|
||||
@@ -49,14 +49,15 @@ let password = $ref("");
|
||||
let form: HTMLFormElement | undefined = $ref();
|
||||
|
||||
async function onLogin() {
|
||||
const response = await fetch(`${config.base}/api/validateCredentials`, {
|
||||
const url = config.authProvider === "simple" ? "/api/token" : "/api/validateCredentials";
|
||||
const response = await fetch(withBase(url), {
|
||||
body: new FormData(form),
|
||||
method: "post",
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (response.status == 200) {
|
||||
error = false;
|
||||
window.location.href = `${config.base}/`;
|
||||
window.location.href = withBase("/");
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ import {
|
||||
size,
|
||||
softWrap,
|
||||
automaticRedirect,
|
||||
} from "@/composables/settings";
|
||||
} from "@/stores/settings";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type Settings } from "@/stores/settings";
|
||||
|
||||
const text = document.querySelector("script#config__json")?.textContent || "{}";
|
||||
|
||||
interface Config {
|
||||
@@ -8,6 +10,14 @@ interface Config {
|
||||
maxLogs: number;
|
||||
hostname: string;
|
||||
hosts: { name: string; id: string }[];
|
||||
authProvider: "simple" | "none" | "forward-proxy";
|
||||
user?: {
|
||||
username: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
serverSettings?: Settings;
|
||||
}
|
||||
|
||||
const pageConfig = JSON.parse(text);
|
||||
@@ -19,4 +29,6 @@ const config: Config = {
|
||||
|
||||
config.version = config.version.replace(/^v/, "");
|
||||
|
||||
export default config;
|
||||
export default Object.freeze(config);
|
||||
|
||||
export const withBase = (path: string) => `${config.base}${path}`;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const useContainerStore = defineStore("container", () => {
|
||||
function connect() {
|
||||
es?.close();
|
||||
ready.value = false;
|
||||
es = new EventSource(`${config.base}/api/events/stream`);
|
||||
es = new EventSource(withBase("/api/events/stream"));
|
||||
es.addEventListener("error", (e) => {
|
||||
if (es?.readyState === EventSource.CLOSED) {
|
||||
showToast(
|
||||
@@ -76,6 +76,12 @@ export const useContainerStore = defineStore("container", () => {
|
||||
}
|
||||
});
|
||||
|
||||
es.onopen = () => {
|
||||
if (containers.value.length > 0) {
|
||||
containers.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
watchOnce(containers, () => (ready.value = true));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toRefs } from "@vueuse/core";
|
||||
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
|
||||
|
||||
export const DEFAULT_SETTINGS: {
|
||||
export type Settings = {
|
||||
search: boolean;
|
||||
size: "small" | "medium" | "large";
|
||||
menuWidth: number;
|
||||
@@ -14,7 +14,8 @@ export const DEFAULT_SETTINGS: {
|
||||
softWrap: boolean;
|
||||
collapseNav: boolean;
|
||||
automaticRedirect: boolean;
|
||||
} = {
|
||||
};
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
search: true,
|
||||
size: "medium",
|
||||
menuWidth: 15,
|
||||
@@ -30,7 +31,16 @@ export const DEFAULT_SETTINGS: {
|
||||
};
|
||||
|
||||
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
|
||||
settings.value = { ...DEFAULT_SETTINGS, ...settings.value };
|
||||
settings.value = { ...DEFAULT_SETTINGS, ...settings.value, ...config.serverSettings };
|
||||
|
||||
if (config.user) {
|
||||
watch(settings, (value) => {
|
||||
fetch(withBase("/api/profile/settings"), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(value),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const {
|
||||
collapseNav,
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
|
||||
playwright:
|
||||
container_name: playwright
|
||||
image: mcr.microsoft.com/playwright:v1.38.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.39.0-jammy
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
@@ -64,7 +64,9 @@ export default defineConfig({
|
||||
text: "Advanced Configuration",
|
||||
items: [
|
||||
{ text: "Authentication", link: "/guide/authentication" },
|
||||
{ text: "Changing Base", link: "/guide/changing-base" },
|
||||
{ text: "Healthcheck", link: "/guide/healthcheck" },
|
||||
{ text: "Hostname", link: "/guide/hostname" },
|
||||
{ text: "Remote Hosts", link: "/guide/remote-hosts" },
|
||||
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
|
||||
{ text: "Logging Files on Disk", link: "/guide/log-files-on-disk" },
|
||||
|
||||
@@ -144,12 +144,14 @@
|
||||
}
|
||||
|
||||
.VPHomeHero .name {
|
||||
@apply font-playfair font-light text-7xl;
|
||||
@apply text-7xl font-light;
|
||||
font-family: "Playfair Display", serif;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
@apply font-playfair font-light text-3xl;
|
||||
@apply text-3xl font-light;
|
||||
color: var(--bar-title-color);
|
||||
font-family: "Playfair Display", serif;
|
||||
}
|
||||
|
||||
.VPHero .container .image {
|
||||
|
||||
@@ -4,6 +4,229 @@ title: Authentication
|
||||
|
||||
# Setting Up Authentication
|
||||
|
||||
Dozzle support two configurations for authentication. In the first configuration, you bring your own authentication method by protecting Dozzle through a proxy. Dozzle can read appropriate headers out of the box.
|
||||
|
||||
If you do not have an authentication solution then Dozzle has a simple file based user management solution. Authentication providers are setup using `--auth-provider` flag. In both of these configurations, Dozzle will try to save user settings to disk. This data is written to `/data`.
|
||||
|
||||
## Forward Proxy
|
||||
|
||||
Dozzle can be configured to read proxy headers by setting `--auth-provider` to `forward-proxy`.
|
||||
|
||||
::: code-group
|
||||
|
||||
```sh [cli]
|
||||
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --auth-provider forward-proxy
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_AUTH_PROVIDER: forward-proxy
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
In this mode, Dozzle expects the following headers:
|
||||
|
||||
- `Remote-User` to map to the username eg. `johndoe`
|
||||
- `Remote-Email` to map to the user's email address. This email is also used to find the right [Gravatar](https://gravatar.com/) for the user.
|
||||
- `Remote-Name` to be a display name like `John Doe`
|
||||
|
||||
### Setting up Dozzle with Authelia
|
||||
|
||||
[Authelia](https://www.authelia.com/) is an open-source authentication and authorization server and portal fulfilling the identity and access management. While setting up Authelia is out of scope for this section, the configuration can be shared as an example for setting up Dozzle with Authelia.
|
||||
|
||||
::: code-group
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
version: "3.3"
|
||||
|
||||
networks:
|
||||
net:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
authelia:
|
||||
image: authelia/authelia
|
||||
container_name: authelia
|
||||
volumes:
|
||||
- ./authelia:/config
|
||||
networks:
|
||||
- net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.authelia.rule=Host(`authelia.example.com`)"
|
||||
- "traefik.http.routers.authelia.entrypoints=https"
|
||||
- "traefik.http.routers.authelia.tls=true"
|
||||
- "traefik.http.routers.authelia.tls.options=default"
|
||||
- "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://authelia.example.com"
|
||||
- "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
|
||||
expose:
|
||||
- 9091
|
||||
restart: unless-stopped
|
||||
|
||||
traefik:
|
||||
image: traefik:2.10.5
|
||||
container_name: traefik
|
||||
volumes:
|
||||
- ./traefik:/etc/traefik
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.api.rule=Host(`traefik.example.com`)"
|
||||
- "traefik.http.routers.api.entrypoints=https"
|
||||
- "traefik.http.routers.api.service=api@internal"
|
||||
- "traefik.http.routers.api.tls=true"
|
||||
- "traefik.http.routers.api.tls.options=default"
|
||||
- "traefik.http.routers.api.middlewares=authelia@docker"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
command:
|
||||
- "--api"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedByDefault=false"
|
||||
- "--providers.file.filename=/etc/traefik/certificates.yml"
|
||||
- "--entrypoints.http=true"
|
||||
- "--entrypoints.http.address=:80"
|
||||
- "--entrypoints.http.http.redirections.entrypoint.to=https"
|
||||
- "--entrypoints.http.http.redirections.entrypoint.scheme=https"
|
||||
- "--entrypoints.https=true"
|
||||
- "--entrypoints.https.address=:443"
|
||||
- "--log=true"
|
||||
- "--log.level=DEBUG"
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
networks:
|
||||
- net
|
||||
environment:
|
||||
DOZZLE_AUTH_PROVIDER: forward-proxy
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.dozzle.rule=Host(`dozzle.example.com`)"
|
||||
- "traefik.http.routers.dozzle.entrypoints=https"
|
||||
- "traefik.http.routers.dozzle.tls=true"
|
||||
- "traefik.http.routers.dozzle.tls.options=default"
|
||||
- "traefik.http.routers.dozzle.middlewares=authelia@docker"
|
||||
expose:
|
||||
- 8080
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
```yaml [configuration.yml]
|
||||
###############################################################
|
||||
# Authelia configuration #
|
||||
###############################################################
|
||||
|
||||
jwt_secret: a_very_important_secret
|
||||
default_redirection_url: https://public.example.com
|
||||
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: 9091
|
||||
|
||||
log:
|
||||
level: info
|
||||
|
||||
totp:
|
||||
issuer: authelia.com
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /config/users_database.yml
|
||||
|
||||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
- domain: traefik.example.com
|
||||
policy: one_factor
|
||||
- domain: dozzle.example.com
|
||||
policy: one_factor
|
||||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
domain: example.com # Should match whatever your root protected domain is
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
find_time: 120
|
||||
ban_time: 300
|
||||
|
||||
storage:
|
||||
encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
|
||||
local:
|
||||
path: /config/db.sqlite3
|
||||
|
||||
notifier:
|
||||
filesystem:
|
||||
filename: /config/notification.txt
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Valid SSL keys are required because Authelia only supports SSL.
|
||||
|
||||
## File Based User Management
|
||||
|
||||
Dozzle supports multi-user authentication by setting `--auth-provider` to `simple`. In this mode, Dozzle will try to read `/data/users.yml`. The content of the file looks like
|
||||
|
||||
```yml
|
||||
users:
|
||||
admin:
|
||||
name: "Admin"
|
||||
# Just sha-256 which can be computed with echo -n password | shasum -a 256
|
||||
password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
|
||||
email: me@email.net
|
||||
```
|
||||
|
||||
Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional.
|
||||
|
||||
The password is hashed using `sha256` which can be generated with `echo -n "secret-password" | shasum -a 256` or `echo -n "secret-password" | sha256sum` on linux.
|
||||
|
||||
You will need to mount this file for Dozzle to find it. Here is an example:
|
||||
|
||||
::: code-group
|
||||
|
||||
```sh [cli]
|
||||
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/dozzle/data:/data -p 8080:8080 amir20/dozzle --auth-provider simple
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /path/to/dozzle/data:/data
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_AUTH_PROVIDER: simple
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Dozzle uses [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token) to generate tokens for authentication. This token is saved in a cookie.
|
||||
|
||||
## Single Username/Password
|
||||
|
||||
::: danger
|
||||
`--username` and `--password` flags will be removed in v6.x in favor of `--auth-provider`.
|
||||
:::
|
||||
|
||||
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use docker secrets `--usernamefile` and `--passwordfile`.
|
||||
|
||||
::: code-group
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Changing Application Base
|
||||
---
|
||||
|
||||
# Changing Dozzle Base
|
||||
|
||||
Dozzle by default mounts to "/". This can be changed with the `--base` flag. For example, if you want to mount to "/foobar" then you can use `--base foobar` or the env variable `DOZZLE_BASE`.
|
||||
|
||||
::: code-group
|
||||
|
||||
```sh
|
||||
docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --base foobar
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_BASE: foobar
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Dozzle will be available at `http://localhost:8080/foobar/`. This option rewrites all assets to `/foobar/{file.path}` and automatically redirects `/foobar` to `/foobar/`.
|
||||
|
||||
## Example with proxy
|
||||
|
||||
Here is an example with Nginx and proxy Dozzle with a different base:
|
||||
|
||||
```conf
|
||||
location ^~ /foobar/ {
|
||||
include /config/nginx/proxy.conf;
|
||||
include /config/nginx/resolver.conf;
|
||||
set $upstream_app dozzle;
|
||||
set $upstream_port 8080;
|
||||
set $upstream_proto http;
|
||||
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
||||
|
||||
chunked_transfer_encoding off;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Hostname
|
||||
---
|
||||
|
||||
# Changing Dozzle's Hostname
|
||||
|
||||
Dozzle's default connection is called localhost. Using `--hostname` flag Dozzle's name can be changed to anything. This value be will shown on the page title and under the Dozzle logo.
|
||||
|
||||
Changing the label for localhost also changes the label for the `localhost` connection which is displayed under the multi-host menu. Below is an example of using `--hostname` to change the name of Dozzle's subheader to `mywebsite.xyz`.
|
||||
|
||||
::: code-group
|
||||
|
||||
```sh
|
||||
docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --hostname mywebsite.xyz
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_HOSTNAME: mywebsite.xyz
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -79,3 +79,7 @@ services:
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Changing localhost label
|
||||
|
||||
`localhost` is a special connection and uses different configuration than `--remote-host`. Changing the label for localhost can be done using the `--hostname` or `DOZZLE_HOSTNAME` env variable. See [hostname](/guide/hostname) page for examples on how to use this flag.
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
"type": "module",
|
||||
"name": "docs",
|
||||
"devDependencies": {
|
||||
"@netlify/functions": "^2.1.0",
|
||||
"@unocss/preset-typography": "^0.56.4",
|
||||
"@unocss/reset": "^0.56.4",
|
||||
"@unocss/transformer-directives": "^0.56.4",
|
||||
"@netlify/functions": "^2.3.0",
|
||||
"@unocss/preset-typography": "^0.57.2",
|
||||
"@unocss/reset": "^0.57.2",
|
||||
"@unocss/transformer-directives": "^0.57.2",
|
||||
"dozzle": "workspace:*",
|
||||
"sitemap": "^7.1.1",
|
||||
"unocss": "^0.56.4"
|
||||
"unocss": "^0.57.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,17 @@ settings:
|
||||
|
||||
devDependencies:
|
||||
'@netlify/functions':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
'@unocss/preset-typography':
|
||||
specifier: ^0.56.4
|
||||
version: 0.56.4
|
||||
specifier: ^0.57.2
|
||||
version: 0.57.2
|
||||
'@unocss/reset':
|
||||
specifier: ^0.56.4
|
||||
version: 0.56.4
|
||||
specifier: ^0.57.2
|
||||
version: 0.57.2
|
||||
'@unocss/transformer-directives':
|
||||
specifier: ^0.56.4
|
||||
version: 0.56.4
|
||||
specifier: ^0.57.2
|
||||
version: 0.57.2
|
||||
dozzle:
|
||||
specifier: workspace:*
|
||||
version: link:..
|
||||
@@ -24,8 +24,8 @@ devDependencies:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.1
|
||||
unocss:
|
||||
specifier: ^0.56.4
|
||||
version: 0.56.4(postcss@8.4.30)(vite@4.4.9)
|
||||
specifier: ^0.57.2
|
||||
version: 0.57.2(postcss@8.4.31)(vite@4.5.0)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -34,7 +34,7 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.3
|
||||
'@jridgewell/trace-mapping': 0.3.19
|
||||
'@jridgewell/trace-mapping': 0.3.20
|
||||
dev: true
|
||||
|
||||
/@antfu/install-pkg@0.1.1:
|
||||
@@ -250,8 +250,8 @@ packages:
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
dev: true
|
||||
|
||||
/@iconify/utils@2.1.10:
|
||||
resolution: {integrity: sha512-0/+5hxjzCZ9RoYpqxnOzbnpQyMdZRuHcMxPJeuX+x/aZkAAD/N4TajDjAPT7LpX+M0bfLExj/p0bbDkUfp0lrg==}
|
||||
/@iconify/utils@2.1.11:
|
||||
resolution: {integrity: sha512-M/w3PkN8zQYXi8N6qK/KhnYMfEbbb6Sk8RZVn8g+Pmmu5ybw177RpsaGwpziyHeUsu4etrexYSWq3rwnIqzYCg==}
|
||||
dependencies:
|
||||
'@antfu/install-pkg': 0.1.1
|
||||
'@antfu/utils': 0.7.6
|
||||
@@ -269,7 +269,7 @@ packages:
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
'@jridgewell/trace-mapping': 0.3.19
|
||||
'@jridgewell/trace-mapping': 0.3.20
|
||||
dev: true
|
||||
|
||||
/@jridgewell/resolve-uri@3.1.1:
|
||||
@@ -286,18 +286,18 @@ packages:
|
||||
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
||||
dev: true
|
||||
|
||||
/@jridgewell/trace-mapping@0.3.19:
|
||||
resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==}
|
||||
/@jridgewell/trace-mapping@0.3.20:
|
||||
resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==}
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.1
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/@netlify/functions@2.1.0:
|
||||
resolution: {integrity: sha512-QWuyj1Q6z6Cqcq3qCcOB75nxjYOxlNmv/d9/nnX4asSRQcWhM/ehXl/KDL6Sl9aufMRe6uQY10s6i9zCDNMRjg==}
|
||||
/@netlify/functions@2.3.0:
|
||||
resolution: {integrity: sha512-E3kzXPWMP/r1rAWhjTaXcaOT47dhEvg/eQUJjRLhD9Zzp0WqkdynHr+bqff4rFNv6tuXrtFZrpbPJFKHH0c0zw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dependencies:
|
||||
'@netlify/serverless-functions-api': 1.7.3
|
||||
'@netlify/serverless-functions-api': 1.9.0
|
||||
is-promise: 4.0.0
|
||||
dev: true
|
||||
|
||||
@@ -306,8 +306,8 @@ packages:
|
||||
engines: {node: ^14.16.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@netlify/serverless-functions-api@1.7.3:
|
||||
resolution: {integrity: sha512-n6/7cJlSWvvbBlUOEAbkGyEld80S6KbG/ldQI9OhLfe1lTatgKmrTNIgqVNpaWpUdTgP2OHWFjmFBzkxxBWs5w==}
|
||||
/@netlify/serverless-functions-api@1.9.0:
|
||||
resolution: {integrity: sha512-Jq4uk1Mwa5vyxImupJYXPP+I5yYcp3PtguvXtJRutKdm9DPALXfZVtCQzBWMNdZiqVWCM3La9hvaBsPjSMfeug==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@netlify/node-cookies': 0.1.0
|
||||
@@ -339,241 +339,238 @@ packages:
|
||||
resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==}
|
||||
dev: true
|
||||
|
||||
/@rollup/pluginutils@5.0.4:
|
||||
resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==}
|
||||
/@rollup/pluginutils@5.0.5:
|
||||
resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/estree': 1.0.2
|
||||
'@types/estree': 1.0.3
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/@types/estree@1.0.2:
|
||||
resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==}
|
||||
/@types/estree@1.0.3:
|
||||
resolution: {integrity: sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==}
|
||||
dev: true
|
||||
|
||||
/@types/node@17.0.45:
|
||||
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
|
||||
dev: true
|
||||
|
||||
/@types/node@18.14.2:
|
||||
resolution: {integrity: sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==}
|
||||
dev: true
|
||||
|
||||
/@types/sax@1.2.4:
|
||||
resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==}
|
||||
/@types/sax@1.2.6:
|
||||
resolution: {integrity: sha512-A1mpYCYu1aHFayy8XKN57ebXeAbh9oQIZ1wXcno6b1ESUAfMBDMx7mf/QGlYwcMRaFryh9YBuH03i/3FlPGDkQ==}
|
||||
dependencies:
|
||||
'@types/node': 18.14.2
|
||||
'@types/node': 17.0.45
|
||||
dev: true
|
||||
|
||||
/@unocss/astro@0.56.4(vite@4.4.9):
|
||||
resolution: {integrity: sha512-2op/0y5UjiE0PRzKwHSEcOFtjDTkeIoCJR9FAYFC+xTx95Sp7+S4yClwaURZtohPnxQmGug0eGpOgsFzoJwmOQ==}
|
||||
/@unocss/astro@0.57.2(vite@4.5.0):
|
||||
resolution: {integrity: sha512-6R6xJVD1n+OrgRwLtiw6fi8/Mx9lECB9wiqRo/liWH27lEWrToXKpgX3oTGGihQQceGNukGe6O4lBThYUILgEQ==}
|
||||
peerDependencies:
|
||||
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/reset': 0.56.4
|
||||
'@unocss/vite': 0.56.4(vite@4.4.9)
|
||||
vite: 4.4.9
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/reset': 0.57.2
|
||||
'@unocss/vite': 0.57.2(vite@4.5.0)
|
||||
vite: 4.5.0
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/@unocss/cli@0.56.4:
|
||||
resolution: {integrity: sha512-WyqqJWfOVb7hvWU5nxtSFAGiDp/Peeohj1f/Xt0KlmgOPS8RA7kmLjRYruuuRuK5yR677IB+0C4kJDFkRpRlcA==}
|
||||
/@unocss/cli@0.57.2:
|
||||
resolution: {integrity: sha512-Poz20X4q7rCu9oBnF8/vNGzCKU9M32xlyeeWoExho0nwh8WJ9JaZ8E8ijcLWeS7YUt1kLOdrsQlppcq+I8o2nQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.2.1
|
||||
'@rollup/pluginutils': 5.0.4
|
||||
'@unocss/config': 0.56.4
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/preset-uno': 0.56.4
|
||||
'@rollup/pluginutils': 5.0.5
|
||||
'@unocss/config': 0.57.2
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/preset-uno': 0.57.2
|
||||
cac: 6.7.14
|
||||
chokidar: 3.5.3
|
||||
colorette: 2.0.20
|
||||
consola: 3.2.3
|
||||
fast-glob: 3.3.1
|
||||
magic-string: 0.30.3
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.1
|
||||
perfect-debounce: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/@unocss/config@0.56.4:
|
||||
resolution: {integrity: sha512-e8pdkoTkKxqkimAttZ5ELiDXn3oYteNDOxNANygWUhw3h6DA1sKh831dQkhTu3Kigsy0/gQXmFXbekrlywMkKw==}
|
||||
/@unocss/config@0.57.2:
|
||||
resolution: {integrity: sha512-C+uZPz4lYN8dft0dKRRiBBtDIyd+RdYlbaQhuQp5F7UEfJeEPH8HpdiF+FeKZVJxP4j7kaGfcGWEJj1ImC/g9A==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
unconfig: 0.3.10
|
||||
'@unocss/core': 0.57.2
|
||||
unconfig: 0.3.11
|
||||
dev: true
|
||||
|
||||
/@unocss/core@0.56.4:
|
||||
resolution: {integrity: sha512-9DL+2adfjWWnFWp2QOJTq2OcfKKo++agaXnCX7CzeD8KgfWCBqxs2RDoxP3HrDbTflhDHdbjz23B+vJoVykhZw==}
|
||||
/@unocss/core@0.57.2:
|
||||
resolution: {integrity: sha512-iTmowhObigxeqcxtEW4v+mAEQtFslifTG0Fiw8kXs3+t4L6fcnjj0i7/FtBbz+nOxrWyt2EzdkUyjpLGQa/yCw==}
|
||||
dev: true
|
||||
|
||||
/@unocss/extractor-arbitrary-variants@0.56.4:
|
||||
resolution: {integrity: sha512-Q93Bu3wZld+Vj5dqfJYH6phcmjm7fy6mEul64RexLWF00As9WrRzWEXbLVYmmVwUm9e5xcxZO6s98gKq1nTH6Q==}
|
||||
/@unocss/extractor-arbitrary-variants@0.57.2:
|
||||
resolution: {integrity: sha512-f6sc8pfgHbJua1VzdpFSPW92lNyIBRl93avRNk+HM4iWAhxBPD3LsCxSS7kOnQg2tFe6YsRkm8QkuF6SjFq1AA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/inspector@0.56.4:
|
||||
resolution: {integrity: sha512-DY3kmX5nGTb8QsbqcGYDvyXXTPhGw7PoVN48wA8o0c+KkcfjjfsoZRTFDpj6P5JcW8Z68gIBOLyTnql6tIMesg==}
|
||||
/@unocss/inspector@0.57.2:
|
||||
resolution: {integrity: sha512-W82xj5oOi7fGSGuV+GvgwZVWH0xCthIgqEscZEwtphiIconswwV8zZjrwzt/gMXmvYv8qx9+QDKmlhVQnJJj6w==}
|
||||
dependencies:
|
||||
'@unocss/rule-utils': 0.56.4
|
||||
'@unocss/rule-utils': 0.57.2
|
||||
gzip-size: 6.0.0
|
||||
sirv: 2.0.3
|
||||
dev: true
|
||||
|
||||
/@unocss/postcss@0.56.4(postcss@8.4.30):
|
||||
resolution: {integrity: sha512-lc0+BjW1gY+86tvtUFppHg++Tw8I0V7hlldloUy7W4Fg2Mu8VAi3I/KaSSRvqMr5SNlfaOPv0XuB/BNkDE65Gw==}
|
||||
/@unocss/postcss@0.57.2(postcss@8.4.31):
|
||||
resolution: {integrity: sha512-OygjXgHkBPCQ6rc7Zo5vd2KDn4XUikTA1knz67oSiPFZLjUmmhzMc/XtckBITzougMNktv1qH1vECGAbKx+FNA==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
postcss: ^8.4.21
|
||||
dependencies:
|
||||
'@unocss/config': 0.56.4
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/rule-utils': 0.56.4
|
||||
'@unocss/config': 0.57.2
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/rule-utils': 0.57.2
|
||||
css-tree: 2.3.1
|
||||
fast-glob: 3.3.1
|
||||
magic-string: 0.30.3
|
||||
postcss: 8.4.30
|
||||
magic-string: 0.30.5
|
||||
postcss: 8.4.31
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-attributify@0.56.4:
|
||||
resolution: {integrity: sha512-mjs9NDniycAdipiLCHpGS75mItfFq3ruX6w4ypcvGxVAa8cGOhotWAAxqgHKSJobdDcoFTjnCaK1SA0svBplUQ==}
|
||||
/@unocss/preset-attributify@0.57.2:
|
||||
resolution: {integrity: sha512-OQh/vqR5PbPjOFJLizw4wrvSIrkHKyTTGaFObi0exeREW2XYslTs44Y+uEQc+GTriYDX9A2cKJDKu7vT7VVEIg==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-icons@0.56.4:
|
||||
resolution: {integrity: sha512-N02I6ZYt2cAThE4pINTRY5tUeHNhcUQYeoqtQhQUk7YFfwRMosWA5JbtH7LTzQpYbE2GsbjXkKdII4bkTWNwhA==}
|
||||
/@unocss/preset-icons@0.57.2:
|
||||
resolution: {integrity: sha512-LlKt83+QLVvbeCCOetq20t5iUnFA+8nIL4mc1bscdK0nyVqspkZkcM8F6uqRwHcMitEbReq1K5kS99qzwUieVQ==}
|
||||
dependencies:
|
||||
'@iconify/utils': 2.1.10
|
||||
'@unocss/core': 0.56.4
|
||||
'@iconify/utils': 2.1.11
|
||||
'@unocss/core': 0.57.2
|
||||
ofetch: 1.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-mini@0.56.4:
|
||||
resolution: {integrity: sha512-FCKs3sUUiHLXQ/ON/ZCQc2JZQox4tv0W8Du+8Y1mRZ6w1yazqqBhR2yDPaEZ51yKcHmDf6ndiTvcJ2tPIz/o8g==}
|
||||
/@unocss/preset-mini@0.57.2:
|
||||
resolution: {integrity: sha512-ZahZ8TWa40SVnJw6fYFhYzLFMCZU3obMBNl0otallevoooKLXIDZL2VmHxFDqCvwaAk9DYsAzPrqUpNWr6bPgA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/extractor-arbitrary-variants': 0.56.4
|
||||
'@unocss/rule-utils': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/extractor-arbitrary-variants': 0.57.2
|
||||
'@unocss/rule-utils': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-tagify@0.56.4:
|
||||
resolution: {integrity: sha512-CURDiJ+wzjzIARS2vF413hZ94u0EVK0+miY6l1eN6RrlW/YfU8LOaakbC/b0Cxl7hOhLrZuXFTgymcS9Ep4TMw==}
|
||||
/@unocss/preset-tagify@0.57.2:
|
||||
resolution: {integrity: sha512-ISebsMHcbUoZG9CoUBA1qDxAGVZ4izXLtQwWde6tb50xIrVSHt1bbfNSgZ/c2RJR1c3fBjn0dzfXVzOHDnFejA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-typography@0.56.4:
|
||||
resolution: {integrity: sha512-/rwCZ0SHa8ht/Chb2rCoENL/S/CWspnTtvK4pPxALEcx/Mqi8Amk45YipU7gXmLsQ3eKwY/s0V1EgHF0DOccHg==}
|
||||
/@unocss/preset-typography@0.57.2:
|
||||
resolution: {integrity: sha512-THCHKzt1Jc8rdt8Ft1DhKxm191QvtiW3+mfkbYvGUUOklIztVspNTSs6iS0Vr6EZWjhLzmUYevq8va+Zk4P91A==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/preset-mini': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/preset-mini': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-uno@0.56.4:
|
||||
resolution: {integrity: sha512-E4VbUe1nIeJ7D1en84osGzwP79IBJ1P9v37Pcx2PvaRAQTKf6WRF7EXzmzEm6HPF+skyGOX9RWuXs9QFODsW8A==}
|
||||
/@unocss/preset-uno@0.57.2:
|
||||
resolution: {integrity: sha512-A5fZmz8i1fSwKMUN8olRAUskkTAPjFsdw19Iem5yOHtK/9NYM3eQPaHDdHldhfZ7/51oF27poavdPfe8KKugQQ==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/preset-mini': 0.56.4
|
||||
'@unocss/preset-wind': 0.56.4
|
||||
'@unocss/rule-utils': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/preset-mini': 0.57.2
|
||||
'@unocss/preset-wind': 0.57.2
|
||||
'@unocss/rule-utils': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-web-fonts@0.56.4:
|
||||
resolution: {integrity: sha512-H9Vy4nYpx1jpQvSBHBTSiB83zU85CtehoK03EbzUrz/i69EjBWiw52+mP/vrtMK0M/5QghcpNf64i8pObFId+w==}
|
||||
/@unocss/preset-web-fonts@0.57.2:
|
||||
resolution: {integrity: sha512-Ymy1N/X7lRzsb551V/SE6EtVdWmBNjW9dFz8viuHuchgjBrq9wF6IBhCG+nrBoUqlz0Jj5piGd/M/OHHQ0Qseg==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
ofetch: 1.3.3
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-wind@0.56.4:
|
||||
resolution: {integrity: sha512-llQYQxrA531kl1juYMyaDEEl/IVdHoIPXaaJTdNHbGkSbMqrpZSRzLPsGE+CpaOBKTNqa93UyU1qlUGSr9ZB+A==}
|
||||
/@unocss/preset-wind@0.57.2:
|
||||
resolution: {integrity: sha512-d8s4PFcIakzcmAoECTY3Ft2Wtb5nn+AvVGj5j52YpVt5ShTuGVlk5UbF9kpfEfzLigtkHcNivM24D1UTfR/MBg==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/preset-mini': 0.56.4
|
||||
'@unocss/rule-utils': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/preset-mini': 0.57.2
|
||||
'@unocss/rule-utils': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/reset@0.56.4:
|
||||
resolution: {integrity: sha512-7o3Jpog49ORWW+Rpz7bzYY+CStfLMyBXAeDOzhM2PQjftuQl4ZQOvIdXi6zrXDEN2SJQsi279s2t50ERZN8YcA==}
|
||||
/@unocss/reset@0.57.2:
|
||||
resolution: {integrity: sha512-e9N5R9ZqbBhePa5ehK63LhU57nlgP3MSG4zblXv61SzBm1xIoTuhj7HX3OVJaMeDsrgazRlndSYNhSfD/ziPxg==}
|
||||
dev: true
|
||||
|
||||
/@unocss/rule-utils@0.56.4:
|
||||
resolution: {integrity: sha512-pZmTPqi/Tb+vbwjNXu9+PwxEh7PVe+FwP+pzUbfNSUpE5Hg9CCbl8hpsXRE/r8kdaoKhmw2ryxvjocOgKXG/ag==}
|
||||
/@unocss/rule-utils@0.57.2:
|
||||
resolution: {integrity: sha512-fgAc5gkZo8JLDe9vMisofSck3k1nN05+kblKhrEaq/+gS5bxHzL9VNExtiTbT00wyMKGjak/uC7qDMbdVmUhYg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
magic-string: 0.30.5
|
||||
dev: true
|
||||
|
||||
/@unocss/scope@0.56.4:
|
||||
resolution: {integrity: sha512-fS8HEWFUeuNhE3fSBwgSfcgC8JS80sYY692uvpwWUN29gFawQDyEmAkGbqDZoBuvfFtKN2IA+evVJ5iED1vtuA==}
|
||||
/@unocss/scope@0.57.2:
|
||||
resolution: {integrity: sha512-UPDCOSvkCDCvQXCAlpRDXLQDYnAYFjGerXtjHigaB+uzJ7Ds4p1yZxcOu6ds6jhcr2kt3Y56Lp3nTqf7tncM0g==}
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-attributify-jsx-babel@0.56.4:
|
||||
resolution: {integrity: sha512-ftkW+HX8tgtSG8bL3Ek9NeSHJfLLdIhokyP/31Kx+DZoOXcwGFN2oBfOs/tkPRdLXUDeEh/9+xLZyXSfrq2Kjw==}
|
||||
/@unocss/transformer-attributify-jsx-babel@0.57.2:
|
||||
resolution: {integrity: sha512-SKx4B0oIv1+F2lzmUyxbMlJ6xqoycPQUazaI1XD29pqkRaqEFYE5RBEZwUwLhLp0ksSyIy2lTtzZYBWQHUs5mw==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-attributify-jsx@0.56.4:
|
||||
resolution: {integrity: sha512-GWiiCMI4W5MaF5y7GejnvIjRzgpTj6xouQYXurMVyq8GaXX34oSzin09Vh7F2/9lMD582UV5/ISu54A78RtswA==}
|
||||
/@unocss/transformer-attributify-jsx@0.57.2:
|
||||
resolution: {integrity: sha512-6KFOp5ldBoWEA0DPAw+uh7FUglyqBU60IeRmyLASLbaz9BPT7ut1rAP7LyIXW78NIi+biwzxQq9FQSbDEFQ4QQ==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-compile-class@0.56.4:
|
||||
resolution: {integrity: sha512-/sPAamamhYBwg29ITrS9FOPF4Mt5r6H0ST0XdAaDVqt0JM3QmMHYUAn5YqvMS1z7c+A6FwsuZ5L0Sn55auXa+g==}
|
||||
/@unocss/transformer-compile-class@0.57.2:
|
||||
resolution: {integrity: sha512-le2H/kYSMobRSo9XzBv6E6jorrCpxdCS7N3hQ+GpRocJasriebffDgRG/m9hVRJvsTgjpYTxbNZkm0KUjGXsmw==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-directives@0.56.4:
|
||||
resolution: {integrity: sha512-L+h0pPgukk12TuqGaZjvGI0y6i3tFfl0dXJnJ2W6eeK2Y2CMirfhCo9Cb2uF81gfIdFKJqXGY6qujK+AS0ji/Q==}
|
||||
/@unocss/transformer-directives@0.57.2:
|
||||
resolution: {integrity: sha512-7pQROj/GPsqMig+t7ntzKi4rY/lvSrE/A0PiBJsFJt328PAa+wnzgCDWkL/FNnhhbXi4BIYSq0V2v+YACwPBVQ==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/rule-utils': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/rule-utils': 0.57.2
|
||||
css-tree: 2.3.1
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-variant-group@0.56.4:
|
||||
resolution: {integrity: sha512-4ebNLWgspeoaHDLQ6qW8ZEWl7rzXURxBJLaw1hUqMwfbM/HEc5gPwUy7k7W+4TJyB426UpBb6kWLgO2WRrJXWw==}
|
||||
/@unocss/transformer-variant-group@0.57.2:
|
||||
resolution: {integrity: sha512-Xik8auIUfVr5xQ6M/CggnrIu5wD9h1tqdbxy5ci9+RQvtmUfrh9m09MfRXxHZWTRQOruGjV63U6rfVzo9X74eA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/core': 0.57.2
|
||||
dev: true
|
||||
|
||||
/@unocss/vite@0.56.4(vite@4.4.9):
|
||||
resolution: {integrity: sha512-hswOJk2C7scwQioLIGDnT+H/bbC1EEkq+MhN+VDiVlRUYwaIgLITmSp0q6y6JK2RKD7VhDVxLNMYw4LxxcJuzA==}
|
||||
/@unocss/vite@0.57.2(vite@4.5.0):
|
||||
resolution: {integrity: sha512-PoyqYsgTSzRE7umfp5Qpdt29ZmYD24M4WIQhATIDkJQm26f/KME6SMcQO1ybLWgkJgrHoenO9QJo+/W8tId/uA==}
|
||||
peerDependencies:
|
||||
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.2.1
|
||||
'@rollup/pluginutils': 5.0.4
|
||||
'@unocss/config': 0.56.4
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/inspector': 0.56.4
|
||||
'@unocss/scope': 0.56.4
|
||||
'@unocss/transformer-directives': 0.56.4
|
||||
'@rollup/pluginutils': 5.0.5
|
||||
'@unocss/config': 0.57.2
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/inspector': 0.57.2
|
||||
'@unocss/scope': 0.57.2
|
||||
'@unocss/transformer-directives': 0.57.2
|
||||
chokidar: 3.5.3
|
||||
fast-glob: 3.3.1
|
||||
magic-string: 0.30.3
|
||||
vite: 4.4.9
|
||||
magic-string: 0.30.5
|
||||
vite: 4.5.0
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
@@ -666,12 +663,12 @@ packages:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/defu@6.1.2:
|
||||
resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==}
|
||||
/defu@6.1.3:
|
||||
resolution: {integrity: sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ==}
|
||||
dev: true
|
||||
|
||||
/destr@2.0.1:
|
||||
resolution: {integrity: sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA==}
|
||||
/destr@2.0.2:
|
||||
resolution: {integrity: sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg==}
|
||||
dev: true
|
||||
|
||||
/duplexer@0.1.2:
|
||||
@@ -853,8 +850,8 @@ packages:
|
||||
p-locate: 5.0.0
|
||||
dev: true
|
||||
|
||||
/magic-string@0.30.3:
|
||||
resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==}
|
||||
/magic-string@0.30.5:
|
||||
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
@@ -892,7 +889,7 @@ packages:
|
||||
acorn: 8.10.0
|
||||
pathe: 1.1.1
|
||||
pkg-types: 1.0.3
|
||||
ufo: 1.3.0
|
||||
ufo: 1.3.1
|
||||
dev: true
|
||||
|
||||
/mrmime@1.0.1:
|
||||
@@ -929,9 +926,9 @@ packages:
|
||||
/ofetch@1.3.3:
|
||||
resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==}
|
||||
dependencies:
|
||||
destr: 2.0.1
|
||||
destr: 2.0.2
|
||||
node-fetch-native: 1.4.0
|
||||
ufo: 1.3.0
|
||||
ufo: 1.3.1
|
||||
dev: true
|
||||
|
||||
/onetime@5.1.2:
|
||||
@@ -990,8 +987,8 @@ packages:
|
||||
pathe: 1.1.1
|
||||
dev: true
|
||||
|
||||
/postcss@8.4.30:
|
||||
resolution: {integrity: sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==}
|
||||
/postcss@8.4.31:
|
||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.6
|
||||
@@ -1015,8 +1012,8 @@ packages:
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/rollup@3.29.3:
|
||||
resolution: {integrity: sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==}
|
||||
/rollup@3.29.4:
|
||||
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
@@ -1029,8 +1026,8 @@ packages:
|
||||
queue-microtask: 1.2.3
|
||||
dev: true
|
||||
|
||||
/sax@1.2.4:
|
||||
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
|
||||
/sax@1.3.0:
|
||||
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
|
||||
dev: true
|
||||
|
||||
/shebang-command@2.0.0:
|
||||
@@ -1064,9 +1061,9 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@types/node': 17.0.45
|
||||
'@types/sax': 1.2.4
|
||||
'@types/sax': 1.2.6
|
||||
arg: 5.0.2
|
||||
sax: 1.2.4
|
||||
sax: 1.3.0
|
||||
dev: true
|
||||
|
||||
/source-map-js@1.0.2:
|
||||
@@ -1091,24 +1088,24 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/ufo@1.3.0:
|
||||
resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==}
|
||||
/ufo@1.3.1:
|
||||
resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==}
|
||||
dev: true
|
||||
|
||||
/unconfig@0.3.10:
|
||||
resolution: {integrity: sha512-tj317lhIq2iZF/NXrJnU1t2UaGUKKz1eL1sK2t63Oq66V9BxqvZV12m55fp/fpQJ+DDmVlLgo7cnLVOZkhlO/A==}
|
||||
/unconfig@0.3.11:
|
||||
resolution: {integrity: sha512-bV/nqePAKv71v3HdVUn6UefbsDKQWRX+bJIkiSm0+twIds6WiD2bJLWWT3i214+J/B4edufZpG2w7Y63Vbwxow==}
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.6
|
||||
defu: 6.1.2
|
||||
defu: 6.1.3
|
||||
jiti: 1.20.0
|
||||
mlly: 1.4.2
|
||||
dev: true
|
||||
|
||||
/unocss@0.56.4(postcss@8.4.30)(vite@4.4.9):
|
||||
resolution: {integrity: sha512-RcaZHLSlj7BAupx6Y9jJwOSRJ+Lu1pnLwRZuppMfD4GgWWNymDRJ7HF1uNqCCYhxJa1LgU1irOEfO8pzK8NYkw==}
|
||||
/unocss@0.57.2(postcss@8.4.31)(vite@4.5.0):
|
||||
resolution: {integrity: sha512-Wmh/a+iXSaDnf6KCf0JtzZ9AoY//fxXLadiXvuTuM4aGEcItHzgt0IeZPs4+ab60usebQBsDyWV5Yr/lgiQ4bQ==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@unocss/webpack': 0.56.4
|
||||
'@unocss/webpack': 0.57.2
|
||||
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
'@unocss/webpack':
|
||||
@@ -1116,27 +1113,27 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@unocss/astro': 0.56.4(vite@4.4.9)
|
||||
'@unocss/cli': 0.56.4
|
||||
'@unocss/core': 0.56.4
|
||||
'@unocss/extractor-arbitrary-variants': 0.56.4
|
||||
'@unocss/postcss': 0.56.4(postcss@8.4.30)
|
||||
'@unocss/preset-attributify': 0.56.4
|
||||
'@unocss/preset-icons': 0.56.4
|
||||
'@unocss/preset-mini': 0.56.4
|
||||
'@unocss/preset-tagify': 0.56.4
|
||||
'@unocss/preset-typography': 0.56.4
|
||||
'@unocss/preset-uno': 0.56.4
|
||||
'@unocss/preset-web-fonts': 0.56.4
|
||||
'@unocss/preset-wind': 0.56.4
|
||||
'@unocss/reset': 0.56.4
|
||||
'@unocss/transformer-attributify-jsx': 0.56.4
|
||||
'@unocss/transformer-attributify-jsx-babel': 0.56.4
|
||||
'@unocss/transformer-compile-class': 0.56.4
|
||||
'@unocss/transformer-directives': 0.56.4
|
||||
'@unocss/transformer-variant-group': 0.56.4
|
||||
'@unocss/vite': 0.56.4(vite@4.4.9)
|
||||
vite: 4.4.9
|
||||
'@unocss/astro': 0.57.2(vite@4.5.0)
|
||||
'@unocss/cli': 0.57.2
|
||||
'@unocss/core': 0.57.2
|
||||
'@unocss/extractor-arbitrary-variants': 0.57.2
|
||||
'@unocss/postcss': 0.57.2(postcss@8.4.31)
|
||||
'@unocss/preset-attributify': 0.57.2
|
||||
'@unocss/preset-icons': 0.57.2
|
||||
'@unocss/preset-mini': 0.57.2
|
||||
'@unocss/preset-tagify': 0.57.2
|
||||
'@unocss/preset-typography': 0.57.2
|
||||
'@unocss/preset-uno': 0.57.2
|
||||
'@unocss/preset-web-fonts': 0.57.2
|
||||
'@unocss/preset-wind': 0.57.2
|
||||
'@unocss/reset': 0.57.2
|
||||
'@unocss/transformer-attributify-jsx': 0.57.2
|
||||
'@unocss/transformer-attributify-jsx-babel': 0.57.2
|
||||
'@unocss/transformer-compile-class': 0.57.2
|
||||
'@unocss/transformer-directives': 0.57.2
|
||||
'@unocss/transformer-variant-group': 0.57.2
|
||||
'@unocss/vite': 0.57.2(vite@4.5.0)
|
||||
vite: 4.5.0
|
||||
transitivePeerDependencies:
|
||||
- postcss
|
||||
- rollup
|
||||
@@ -1147,8 +1144,8 @@ packages:
|
||||
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
|
||||
dev: true
|
||||
|
||||
/vite@4.4.9:
|
||||
resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==}
|
||||
/vite@4.5.0:
|
||||
resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -1176,8 +1173,8 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
esbuild: 0.18.20
|
||||
postcss: 8.4.30
|
||||
rollup: 3.29.3
|
||||
postcss: 8.4.31
|
||||
rollup: 3.29.4
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -5,7 +5,7 @@ require (
|
||||
github.com/alexflint/go-arg v1.4.3
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker v24.0.6+incompatible
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
@@ -18,24 +18,37 @@ require (
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/afero v1.9.5
|
||||
github.com/spf13/afero v1.10.0
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require github.com/go-chi/chi/v5 v5.0.10
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/jwtauth/v5 v5.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.11 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.12.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
|
||||
|
||||
@@ -61,12 +61,13 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ
|
||||
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/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
|
||||
github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE=
|
||||
github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -81,9 +82,13 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/jwtauth/v5 v5.1.1 h1:Pjixqu5YkjE9sCLpzE01L0Q4sQzJIPdo7uz9r8ftp/c=
|
||||
github.com/go-chi/jwtauth/v5 v5.1.1/go.mod h1:CYP1WSbzD4MPuKCr537EM3kfFhSQgpUEtMJFuYJjqWU=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -161,6 +166,19 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
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/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
|
||||
@@ -179,12 +197,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
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.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
|
||||
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
@@ -193,6 +213,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -202,6 +223,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/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=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -214,7 +236,11 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -248,6 +274,8 @@ 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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -282,6 +310,9 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -303,6 +334,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -341,10 +374,18 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -353,6 +394,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -410,6 +453,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
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.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const remoteUser contextKey = "remoteUser"
|
||||
|
||||
type proxyAuthAuth struct {
|
||||
}
|
||||
|
||||
func hashEmail(email string) string {
|
||||
email = strings.TrimSpace(email)
|
||||
email = strings.ToLower(email)
|
||||
hash := md5.Sum([]byte(email))
|
||||
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func NewForwardProxyAuth() *proxyAuthAuth {
|
||||
return &proxyAuthAuth{}
|
||||
}
|
||||
|
||||
func (p *proxyAuthAuth) AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Remote-Email") != "" {
|
||||
user := newUser(r.Header.Get("Remote-User"), r.Header.Get("Remote-Email"), r.Header.Get("Remote-Name"))
|
||||
ctx := context.WithValue(r.Context(), remoteUser, user)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *proxyAuthAuth) CreateToken(username, password string) (string, error) {
|
||||
log.Fatalf("CreateToken not implemented for proxy auth")
|
||||
return "", nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
type simpleAuthContext struct {
|
||||
UserDatabase *UserDatabase
|
||||
tokenAuth *jwtauth.JWTAuth
|
||||
}
|
||||
|
||||
var ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
|
||||
func NewSimpleAuth(userDatabase *UserDatabase) *simpleAuthContext {
|
||||
h := sha256.New()
|
||||
for _, user := range userDatabase.Users {
|
||||
h.Write([]byte(user.Password))
|
||||
}
|
||||
|
||||
tokenAuth := jwtauth.New("HS256", h.Sum(nil), nil)
|
||||
|
||||
return &simpleAuthContext{
|
||||
UserDatabase: userDatabase,
|
||||
tokenAuth: tokenAuth,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *simpleAuthContext) CreateToken(username, password string) (string, error) {
|
||||
user := a.UserDatabase.FindByPassword(username, password)
|
||||
if user == nil {
|
||||
return "", ErrInvalidCredentials
|
||||
}
|
||||
|
||||
_, tokenString, err := a.tokenAuth.Encode(map[string]interface{}{"username": user.Username, "email": user.Email, "name": user.Name, "timestamp": time.Now()})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func (a *simpleAuthContext) AuthMiddleware(next http.Handler) http.Handler {
|
||||
return jwtauth.Verifier(a.tokenAuth)(next)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email" yaml:"email"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Password string `json:"-" yaml:"password"`
|
||||
}
|
||||
|
||||
func newUser(username, email, name string) *User {
|
||||
avatar := ""
|
||||
if email != "" {
|
||||
avatar = "https://gravatar.com/avatar/" + hashEmail(email)
|
||||
}
|
||||
return &User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Name: name,
|
||||
Avatar: avatar,
|
||||
}
|
||||
}
|
||||
|
||||
type UserDatabase struct {
|
||||
Users map[string]*User `yaml:"users"`
|
||||
}
|
||||
|
||||
func ReadUsersFromFile(path string) (*UserDatabase, error) {
|
||||
users := UserDatabase{}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return &users, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := yaml.NewDecoder(file).Decode(&users); err != nil {
|
||||
return &users, err
|
||||
}
|
||||
|
||||
for username, user := range users.Users {
|
||||
user.Username = username
|
||||
}
|
||||
|
||||
return &users, nil
|
||||
}
|
||||
|
||||
func (u *UserDatabase) Find(username string) *User {
|
||||
user, ok := u.Users[username]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (u *UserDatabase) FindByPassword(username, password string) *User {
|
||||
user := u.Find(username)
|
||||
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if user.Password != sha256sum(password) {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func sha256sum(s string) string {
|
||||
bytes := sha256.Sum256([]byte(s))
|
||||
return hex.EncodeToString(bytes[:])
|
||||
}
|
||||
|
||||
func UserFromContext(ctx context.Context) *User {
|
||||
if user, ok := ctx.Value(remoteUser).(*User); ok {
|
||||
return user
|
||||
} else {
|
||||
if _, claims, err := jwtauth.FromContext(ctx); err == nil {
|
||||
username, ok := claims["username"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if username == "" {
|
||||
return nil
|
||||
}
|
||||
email := claims["email"].(string)
|
||||
name := claims["name"].(string)
|
||||
return newUser(username, email, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func RequireAuthentication(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := UserFromContext(r.Context())
|
||||
if user != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -31,7 +31,7 @@ var bufPool = sync.Pool{
|
||||
},
|
||||
}
|
||||
|
||||
var BadHeaderErr = fmt.Errorf("dozzle/docker: unable to read header")
|
||||
var ErrBadHeader = fmt.Errorf("dozzle/docker: unable to read header")
|
||||
|
||||
func NewEventGenerator(reader io.Reader, tty bool) *EventGenerator {
|
||||
generator := &EventGenerator{
|
||||
@@ -83,11 +83,12 @@ func (g *EventGenerator) consumeReader() {
|
||||
}
|
||||
|
||||
if readerError != nil {
|
||||
if readerError != BadHeaderErr {
|
||||
if readerError != ErrBadHeader {
|
||||
log.Debugf("reader error: %v", readerError)
|
||||
g.Errors <- readerError
|
||||
close(g.buffer)
|
||||
break
|
||||
}
|
||||
close(g.buffer)
|
||||
break
|
||||
}
|
||||
}
|
||||
g.wg.Done()
|
||||
@@ -119,12 +120,14 @@ func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
|
||||
}
|
||||
return message, streamType, nil
|
||||
} else {
|
||||
n, err := reader.Read(header)
|
||||
n, err := io.ReadFull(reader, header)
|
||||
if err != nil {
|
||||
return "", streamType, err
|
||||
}
|
||||
if n != 8 {
|
||||
return "", streamType, BadHeaderErr
|
||||
log.Warnf("unable to read header: %v", header)
|
||||
message, _ := reader.ReadString('\n')
|
||||
return message, streamType, ErrBadHeader
|
||||
}
|
||||
|
||||
switch header[0] {
|
||||
@@ -133,7 +136,7 @@ func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
|
||||
case 2:
|
||||
streamType = STDERR
|
||||
default:
|
||||
log.Warnf("unknown stream type %d", header[0])
|
||||
log.Warnf("unknown stream type: %v", header[0])
|
||||
}
|
||||
|
||||
count := binary.BigEndian.Uint32(header[4:])
|
||||
@@ -7,18 +7,30 @@ import (
|
||||
|
||||
var KEY_VALUE_REGEX = regexp.MustCompile(`level=(\w+)`)
|
||||
var ANSI_COLOR_REGEX = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
var LOG_LEVELS = []string{"error", "warn", "warning", "info", "debug", "trace", "fatal"}
|
||||
var LOG_LEVELS_PLAIN = map[string]*regexp.Regexp{}
|
||||
var LOG_LEVEL_BRACKET = map[string]*regexp.Regexp{}
|
||||
|
||||
func init() {
|
||||
for _, level := range LOG_LEVELS {
|
||||
LOG_LEVELS_PLAIN[level] = regexp.MustCompile("(?i)^" + level + "[^a-z]")
|
||||
}
|
||||
|
||||
for _, level := range LOG_LEVELS {
|
||||
LOG_LEVEL_BRACKET[level] = regexp.MustCompile("(?i)\\[ ?" + level + " ?\\]")
|
||||
}
|
||||
}
|
||||
|
||||
func guessLogLevel(logEvent *LogEvent) string {
|
||||
switch value := logEvent.Message.(type) {
|
||||
case string:
|
||||
levels := []string{"error", "warn", "warning", "info", "debug", "trace", "fatal"}
|
||||
stripped := ANSI_COLOR_REGEX.ReplaceAllString(value, "") // remove ansi color codes
|
||||
for _, level := range levels {
|
||||
if match, _ := regexp.MatchString("(?i)^"+level+"[^a-z]", stripped); match {
|
||||
for _, level := range LOG_LEVELS {
|
||||
if LOG_LEVELS_PLAIN[level].MatchString(stripped) {
|
||||
return level
|
||||
}
|
||||
|
||||
if strings.Contains(value, "["+strings.ToUpper(level)+"]") {
|
||||
if LOG_LEVEL_BRACKET[level].MatchString(stripped) {
|
||||
return level
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGuessLogLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"ERROR: Something went wrong", "error"},
|
||||
{"WARN: Something might be wrong", "warn"},
|
||||
{"INFO: Something happened", "info"},
|
||||
{"debug: Something happened", "debug"},
|
||||
{"debug Something happened", "debug"},
|
||||
{"TRACE: Something happened", "trace"},
|
||||
{"FATAL: Something happened", "fatal"},
|
||||
{"level=error Something went wrong", "error"},
|
||||
{"[ERROR] Something went wrong", "error"},
|
||||
{"[error] Something went wrong", "error"},
|
||||
{"[ ERROR ] Something went wrong", "error"},
|
||||
{"[error] Something went wrong", "error"},
|
||||
{"[test] [error] Something went wrong", "error"},
|
||||
{"[foo] [ ERROR] Something went wrong", "error"},
|
||||
{"123 ERROR Something went wrong", "error"},
|
||||
{"123 Something went wrong", ""},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
logEvent := &LogEvent{
|
||||
Message: test.input,
|
||||
}
|
||||
if level := guessLogLevel(logEvent); level != test.expected {
|
||||
t.Errorf("guessLogLevel(%s) = %s, want %s", test.input, level, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Search bool `json:"search"`
|
||||
MenuWidth float32 `json:"menuWidth"`
|
||||
SmallerScrollbars bool `json:"smallerScrollbars"`
|
||||
ShowTimestamp bool `json:"showTimestamp"`
|
||||
ShowStd bool `json:"showStd"`
|
||||
ShowAllContainers bool `json:"showAllContainers"`
|
||||
SoftWrap bool `json:"softWrap"`
|
||||
CollapseNav bool `json:"collapseNav"`
|
||||
AutomaticRedirect bool `json:"automaticRedirect"`
|
||||
Size string `json:"size,omitempty"`
|
||||
LightTheme string `json:"lightTheme,omitempty"`
|
||||
HourStyle string `json:"hourStyle,omitempty"`
|
||||
}
|
||||
|
||||
var data_path string
|
||||
|
||||
func init() {
|
||||
path, err := filepath.Abs("./data")
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get absolute path for data directory: %s", err)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(path, 0755); err != nil {
|
||||
log.Fatalf("Unable to create data directory: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
data_path = path
|
||||
}
|
||||
|
||||
func SaveUserSettings(user *auth.User, settings *Settings) error {
|
||||
path := filepath.Join(data_path, user.Username)
|
||||
|
||||
// Create user directory if it doesn't exist
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
settings_path := filepath.Join(path, "settings.json")
|
||||
|
||||
data, err := json.MarshalIndent(settings, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(settings_path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Saved settings for user %s", user.Username)
|
||||
|
||||
return f.Sync()
|
||||
}
|
||||
|
||||
func LoadUserSettings(user *auth.User) (*Settings, error) {
|
||||
path := filepath.Join(data_path, user.Username)
|
||||
settings_path := filepath.Join(path, "settings.json")
|
||||
|
||||
if _, err := os.Stat(settings_path); os.IsNotExist(err) {
|
||||
return &Settings{}, errors.New("Settings file does not exist")
|
||||
}
|
||||
|
||||
f, err := os.Open(settings_path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var settings Settings
|
||||
if err := json.NewDecoder(f).Decode(&settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/* snapshot: Test_createRoutes_foobar */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
foo page
|
||||
@@ -9,7 +9,7 @@ foo page
|
||||
/* snapshot: Test_createRoutes_index */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
index page
|
||||
@@ -17,7 +17,7 @@ index page
|
||||
/* snapshot: Test_createRoutes_redirect */
|
||||
HTTP/1.1 301 Moved Permanently
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Location: /foobar/
|
||||
|
||||
@@ -26,7 +26,7 @@ Location: /foobar/
|
||||
/* snapshot: Test_createRoutes_redirect_with_auth */
|
||||
HTTP/1.1 307 Temporary Redirect
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Location: /foobar/login
|
||||
|
||||
@@ -35,7 +35,7 @@ Location: /foobar/login
|
||||
/* snapshot: Test_createRoutes_username_password */
|
||||
HTTP/1.1 307 Temporary Redirect
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Location: /login
|
||||
|
||||
@@ -44,7 +44,7 @@ Location: /login
|
||||
/* snapshot: Test_createRoutes_username_password_invalid */
|
||||
HTTP/1.1 401 Unauthorized
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
@@ -56,7 +56,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -66,7 +66,7 @@ data: end of stream
|
||||
/* snapshot: Test_createRoutes_version */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/html
|
||||
|
||||
<pre>dev</pre>
|
||||
@@ -74,8 +74,8 @@ Content-Type: text/html
|
||||
/* snapshot: Test_handler_between_dates */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: application/ld+json; charset=UTF-8
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: application/x-jsonl; charset=UTF-8
|
||||
|
||||
{"m":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout"}
|
||||
{"m":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr"}
|
||||
@@ -101,7 +101,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -114,7 +114,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -132,7 +132,7 @@ data: {"actorId":"1234","name":"start","host":"localhost"}
|
||||
/* snapshot: Test_handler_streamLogs_error_finding_container */
|
||||
HTTP/1.1 404 Not Found
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
@@ -144,7 +144,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Accel-Buffering: no
|
||||
X-Content-Type-Options: nosniff
|
||||
@@ -154,7 +154,7 @@ test error
|
||||
/* snapshot: Test_handler_streamLogs_error_std */
|
||||
HTTP/1.1 400 Bad Request
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
@@ -166,7 +166,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -181,7 +181,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -194,7 +194,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -101,6 +101,40 @@ func (h *handler) validateCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (h *handler) createToken(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.PostFormValue("username")
|
||||
pass := r.PostFormValue("password")
|
||||
|
||||
if token, err := h.config.Authorizer.CreateToken(user, pass); err == nil {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "jwt",
|
||||
Value: token,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
log.Infof("Token created for user %s", user)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(http.StatusText(http.StatusOK)))
|
||||
} else {
|
||||
log.Errorf("Error while creating token: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) deleteToken(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "jwt",
|
||||
Value: "",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Unix(0, 0),
|
||||
})
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(http.StatusText(http.StatusOK)))
|
||||
}
|
||||
|
||||
func (h *handler) clearSession(w http.ResponseWriter, r *http.Request) {
|
||||
if !secured {
|
||||
log.Panic("Validating credentials with secured=false should not happen")
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
func cspHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' gravatar.com data:; manifest-src 'self'; connect-src 'self' api.github.com;")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
|
||||
log.Trace("Executing healthcheck request")
|
||||
var client DockerClient
|
||||
for _, v := range h.clients {
|
||||
client = v
|
||||
break
|
||||
}
|
||||
|
||||
if ping, err := client.Ping(r.Context()); err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
fmt.Fprintf(w, "OK API Version %v", ping.APIVersion)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/profile"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
_, err := h.content.Open(req.URL.Path)
|
||||
if err == nil && req.URL.Path != "" && req.URL.Path != "/" {
|
||||
fileServer.ServeHTTP(w, req)
|
||||
} else {
|
||||
if !isAuthorized(req) && req.URL.Path != "login" {
|
||||
http.Redirect(w, req, path.Clean(h.config.Base+"/login"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
h.executeTemplate(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
base := ""
|
||||
if h.config.Base != "/" {
|
||||
base = h.config.Base
|
||||
}
|
||||
hosts := make([]*docker.Host, 0, len(h.clients))
|
||||
for _, v := range h.clients {
|
||||
hosts = append(hosts, v.Host())
|
||||
}
|
||||
sort.Slice(hosts, func(i, j int) bool {
|
||||
return hosts[i].Name < hosts[j].Name
|
||||
})
|
||||
|
||||
config := map[string]interface{}{
|
||||
"base": base,
|
||||
"version": h.config.Version,
|
||||
"authorizationNeeded": h.isAuthorizationNeeded(req),
|
||||
"secured": secured,
|
||||
"hostname": h.config.Hostname,
|
||||
"hosts": hosts,
|
||||
"authProvider": h.config.AuthProvider,
|
||||
}
|
||||
|
||||
user := auth.UserFromContext(req.Context())
|
||||
if user != nil {
|
||||
if settings, err := profile.LoadUserSettings(user); err == nil {
|
||||
config["serverSettings"] = settings
|
||||
} else {
|
||||
config["serverSettings"] = struct{}{}
|
||||
}
|
||||
config["user"] = user
|
||||
} else if h.config.AuthProvider == FORWARD_PROXY {
|
||||
log.Error("Unable to find remote user. Please check your proxy configuration. Expecting headers Remote-Email, Remote-User, Remote-Name.")
|
||||
log.Debugf("Dumping all headers for url /%s", req.URL.String())
|
||||
for k, v := range req.Header {
|
||||
log.Debugf("%s: %s", k, v)
|
||||
}
|
||||
http.Error(w, "Unauthorized user", http.StatusUnauthorized)
|
||||
return
|
||||
} else if h.config.AuthProvider == SIMPLE && req.URL.Path != "login" {
|
||||
log.Debugf("Redirecting to login page for url /%s", req.URL.String())
|
||||
http.Redirect(w, req, path.Clean(h.config.Base+"/login"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Config": config,
|
||||
"Dev": h.config.Dev,
|
||||
"Manifest": h.readManifest(),
|
||||
"Base": base,
|
||||
}
|
||||
file, err := h.content.Open("index.html")
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
bytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
tmpl, err := template.New("index.html").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) template.JS {
|
||||
a, _ := json.Marshal(v)
|
||||
return template.JS(a)
|
||||
},
|
||||
}).Parse(string(bytes))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) readManifest() map[string]interface{} {
|
||||
if h.config.Dev {
|
||||
return map[string]interface{}{}
|
||||
} else {
|
||||
file, err := h.content.Open("manifest.json")
|
||||
if err != nil {
|
||||
// this should only happen during test. In production, the file is embedded in the binary and checked in main.go
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
bytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not read manifest.json: %v", err)
|
||||
}
|
||||
var manifest map[string]interface{}
|
||||
err = json.Unmarshal(bytes, &manifest)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse manifest.json: %v", err)
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -51,7 +51,7 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/ld+json; charset=UTF-8")
|
||||
w.Header().Set("Content-Type", "application/x-jsonl; charset=UTF-8")
|
||||
|
||||
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
|
||||
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
|
||||
@@ -0,0 +1,32 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/profile"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *handler) saveSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var settings profile.Settings
|
||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user == nil {
|
||||
http.Error(w, "Unable to find user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := profile.SaveUserSettings(user, &settings); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Errorf("Unable to save user settings: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/go-chi/chi/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AuthProvider string
|
||||
|
||||
const (
|
||||
NONE AuthProvider = "none"
|
||||
SIMPLE AuthProvider = "simple"
|
||||
FORWARD_PROXY AuthProvider = "forward-proxy"
|
||||
)
|
||||
|
||||
// Config is a struct for configuring the web service
|
||||
type Config struct {
|
||||
Base string
|
||||
Addr string
|
||||
Version string
|
||||
Username string
|
||||
Password string
|
||||
Hostname string
|
||||
NoAnalytics bool
|
||||
Dev bool
|
||||
AuthProvider AuthProvider
|
||||
Authorizer Authorizer
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
clients map[string]DockerClient
|
||||
content fs.FS
|
||||
config *Config
|
||||
}
|
||||
|
||||
// Client is a proxy around the docker client
|
||||
type DockerClient interface {
|
||||
ListContainers() ([]docker.Container, error)
|
||||
FindContainer(string) (docker.Container, error)
|
||||
ContainerLogs(context.Context, string, string, docker.StdType) (io.ReadCloser, error)
|
||||
Events(context.Context, chan<- docker.ContainerEvent) <-chan error
|
||||
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, docker.StdType) (io.ReadCloser, error)
|
||||
ContainerStats(context.Context, string, chan<- docker.ContainerStat) error
|
||||
Ping(context.Context) (types.Ping, error)
|
||||
Host() *docker.Host
|
||||
}
|
||||
|
||||
type Authorizer interface {
|
||||
AuthMiddleware(http.Handler) http.Handler
|
||||
CreateToken(string, string) (string, error)
|
||||
}
|
||||
|
||||
func CreateServer(clients map[string]DockerClient, content fs.FS, config Config) *http.Server {
|
||||
handler := &handler{
|
||||
clients: clients,
|
||||
content: content,
|
||||
config: &config,
|
||||
}
|
||||
return &http.Server{Addr: config.Addr, Handler: createRouter(handler)}
|
||||
}
|
||||
|
||||
var fileServer http.Handler
|
||||
|
||||
func createRouter(h *handler) *chi.Mux {
|
||||
initializeAuth(h)
|
||||
|
||||
base := h.config.Base
|
||||
r := chi.NewRouter()
|
||||
|
||||
if !h.config.Dev {
|
||||
r.Use(cspHeaders)
|
||||
}
|
||||
|
||||
r.Route(base, func(r chi.Router) {
|
||||
if h.config.Authorizer != nil {
|
||||
r.Use(h.config.Authorizer.AuthMiddleware)
|
||||
}
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
if h.config.AuthProvider != NONE {
|
||||
r.Use(auth.RequireAuthentication)
|
||||
}
|
||||
r.Use(authorizationRequired) // TODO remove this
|
||||
r.Get("/api/logs/stream/{host}/{id}", h.streamLogs)
|
||||
r.Get("/api/logs/download/{host}/{id}", h.downloadLogs)
|
||||
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
|
||||
r.Get("/api/events/stream", h.streamEvents)
|
||||
r.Put("/api/profile/settings", h.saveSettings)
|
||||
r.Get("/logout", h.clearSession) // TODO remove this
|
||||
r.Get("/version", h.version)
|
||||
})
|
||||
|
||||
defaultHandler := http.StripPrefix(strings.Replace(base+"/", "//", "/", 1), http.HandlerFunc(h.index))
|
||||
r.Get("/*", func(w http.ResponseWriter, req *http.Request) {
|
||||
defaultHandler.ServeHTTP(w, req)
|
||||
})
|
||||
})
|
||||
|
||||
if h.config.AuthProvider == SIMPLE {
|
||||
r.Post("/api/token", h.createToken)
|
||||
r.Delete("/api/token", h.deleteToken)
|
||||
}
|
||||
|
||||
r.Post("/api/validateCredentials", h.validateCredentials) // TODO remove this
|
||||
r.Get("/healthcheck", h.healthcheck)
|
||||
})
|
||||
|
||||
if base != "/" {
|
||||
r.Get(base, func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
|
||||
})
|
||||
}
|
||||
|
||||
fileServer = http.FileServer(http.FS(h.content))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *handler) clientFromRequest(r *http.Request) DockerClient {
|
||||
host := chi.URLParam(r, "host")
|
||||
|
||||
if host == "" {
|
||||
log.Fatalf("No host found for url %v", r.URL)
|
||||
}
|
||||
|
||||
if client, ok := h.clients[host]; ok {
|
||||
return client
|
||||
}
|
||||
|
||||
log.Fatalf("No client found for host %v and url %v", host, r.URL)
|
||||
return nil
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/beme/abide"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -92,7 +92,7 @@ func Test_createRoutes_foobar_file(t *testing.T) {
|
||||
func Test_createRoutes_version(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
|
||||
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/", Version: "dev"})
|
||||
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/", Version: "dev", AuthProvider: NONE})
|
||||
req, err := http.NewRequest("GET", "/version", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -181,7 +181,7 @@ func Test_createRoutes_username_password_valid_session(t *testing.T) {
|
||||
mockedClient := new(MockedClient)
|
||||
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, "123", "", docker.STDALL).Return(io.NopCloser(strings.NewReader("test data")), io.EOF)
|
||||
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
|
||||
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password", AuthProvider: NONE})
|
||||
|
||||
// Get cookie first
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/beme/abide"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/beme/abide"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/beme/abide"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -80,5 +80,5 @@ func createHandler(client DockerClient, content fs.FS, config Config) *chi.Mux {
|
||||
}
|
||||
|
||||
func createDefaultHandler(client DockerClient) *chi.Mux {
|
||||
return createHandler(client, nil, Config{Base: "/"})
|
||||
return createHandler(client, nil, Config{Base: "/", AuthProvider: NONE})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *handler) version(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, "<pre>%v</pre>", h.config.Version)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
toolbar:
|
||||
clear: 清除
|
||||
download: 下載
|
||||
search: 查詢
|
||||
show: 僅顯示 {std}
|
||||
show-all: 顯示全部資料流
|
||||
label:
|
||||
containers: 容器
|
||||
total-containers: 總容器數
|
||||
running: 運行中
|
||||
total-cpu-usage: 總 CPU 使用率
|
||||
total-mem-usage: 總記憶體使用率
|
||||
dozzle-version: Dozzle 版本
|
||||
all: 全部
|
||||
host: 主機
|
||||
password: 密碼
|
||||
username: 使用者名稱
|
||||
container-name: 容器名稱
|
||||
status: 狀態
|
||||
created: 建立時間
|
||||
avg-cpu: 平均 CPU (%)
|
||||
avg-mem: 平均記憶體 (%)
|
||||
tooltip:
|
||||
search: 查詢容器 (⌘ + k, ⌃k)
|
||||
pin-column: 固定此列
|
||||
error:
|
||||
page-not-found: 此頁面不存在。
|
||||
invalid-auth: 使用者名稱和密碼不正確。
|
||||
logs-skipped: 略過 {total} 筆記錄
|
||||
container-not-found: 找不到容器。
|
||||
title:
|
||||
page-not-found: 頁面不存在
|
||||
login: 需要身份驗證
|
||||
dashboard: 1 個容器 | {count} 個容器
|
||||
settings: 設定
|
||||
button:
|
||||
logout: 登出
|
||||
login: 登入
|
||||
settings: 設定
|
||||
placeholder:
|
||||
search-containers: 查詢容器 (⌘ + k, ⌃k)
|
||||
settings:
|
||||
display: 顯示
|
||||
small-scrollbars: 使用較小的捲軸
|
||||
show-timesamps: 顯示時間戳記
|
||||
soft-wrap: 自動換行
|
||||
12-24-format: >-
|
||||
預設情況下,Dozzle 會使用您瀏覽器的區域設定來格式化時間。您可以強制使用 12 或 24 小時制。
|
||||
font-size: 日誌的字型大小
|
||||
color-scheme: 顏色配置
|
||||
options: 選項
|
||||
show-stopped-containers: 顯示已停止的容器
|
||||
about: 關於
|
||||
search: 以 Dozzle 進行查詢
|
||||
using-version: 您正在使用 Dozzle {version}。
|
||||
update-available: >-
|
||||
有新版本可用!請更新至 <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>。
|
||||
show-std: 顯示 stdout 和 stderr 標籤
|
||||
@@ -7,16 +7,18 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/alexflint/go-arg"
|
||||
"github.com/amir20/dozzle/analytics"
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/healthcheck"
|
||||
"github.com/amir20/dozzle/web"
|
||||
"github.com/amir20/dozzle/internal/analytics"
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/healthcheck"
|
||||
"github.com/amir20/dozzle/internal/web"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -50,6 +52,7 @@ type args struct {
|
||||
Filter map[string][]string `arg:"-"`
|
||||
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running."`
|
||||
RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"`
|
||||
AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."`
|
||||
}
|
||||
|
||||
type HealthcheckCmd struct {
|
||||
@@ -71,9 +74,13 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if args.AuthProvider != "none" && args.AuthProvider != "forward-proxy" && args.AuthProvider != "simple" {
|
||||
log.Fatalf("Invalid auth provider %s", args.AuthProvider)
|
||||
}
|
||||
|
||||
log.Infof("Dozzle version %s", version)
|
||||
|
||||
clients := createClients(args, docker.NewClientWithFilters, docker.NewClientWithTlsAndFilter)
|
||||
clients := createClients(args, docker.NewClientWithFilters, docker.NewClientWithTlsAndFilter, args.Hostname)
|
||||
|
||||
if len(clients) == 0 {
|
||||
log.Fatal("Could not connect to any Docker Engines")
|
||||
@@ -132,10 +139,14 @@ func doStartEvent(arg args) {
|
||||
|
||||
func createClients(args args,
|
||||
localClientFactory func(map[string][]string) (*docker.Client, error),
|
||||
remoteClientFactory func(map[string][]string, docker.Host) (*docker.Client, error)) map[string]web.DockerClient {
|
||||
remoteClientFactory func(map[string][]string, docker.Host) (*docker.Client, error),
|
||||
hostname string) map[string]web.DockerClient {
|
||||
clients := make(map[string]web.DockerClient)
|
||||
|
||||
if localClient := createLocalClient(args, localClientFactory); localClient != nil {
|
||||
if hostname != "" {
|
||||
localClient.Host().Name = hostname
|
||||
}
|
||||
clients[localClient.Host().ID] = localClient
|
||||
}
|
||||
|
||||
@@ -163,15 +174,41 @@ func createClients(args args,
|
||||
|
||||
func createServer(args args, clients map[string]web.DockerClient) *http.Server {
|
||||
_, dev := os.LookupEnv("DEV")
|
||||
|
||||
var provider web.AuthProvider = web.NONE
|
||||
var authorizer web.Authorizer
|
||||
if args.AuthProvider == "forward-proxy" {
|
||||
provider = web.FORWARD_PROXY
|
||||
authorizer = auth.NewForwardProxyAuth()
|
||||
} else if args.AuthProvider == "simple" {
|
||||
provider = web.SIMPLE
|
||||
|
||||
path, err := filepath.Abs("./data/users.yml")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not find absolute path to users.yml file: %s", err)
|
||||
}
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
log.Fatalf("Could not find users.yml file at %s", path)
|
||||
}
|
||||
|
||||
users, err := auth.ReadUsersFromFile(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not read users.yml file at %s: %s", path, err)
|
||||
}
|
||||
authorizer = auth.NewSimpleAuth(users)
|
||||
}
|
||||
|
||||
config := web.Config{
|
||||
Addr: args.Addr,
|
||||
Base: args.Base,
|
||||
Version: version,
|
||||
Username: args.Username,
|
||||
Password: args.Password,
|
||||
Hostname: args.Hostname,
|
||||
NoAnalytics: args.NoAnalytics,
|
||||
Dev: dev,
|
||||
Addr: args.Addr,
|
||||
Base: args.Base,
|
||||
Version: version,
|
||||
Username: args.Username,
|
||||
Password: args.Password,
|
||||
Hostname: args.Hostname,
|
||||
NoAnalytics: args.NoAnalytics,
|
||||
Dev: dev,
|
||||
AuthProvider: provider,
|
||||
Authorizer: authorizer,
|
||||
}
|
||||
|
||||
assets, err := fs.Sub(content, "dist")
|
||||
@@ -206,8 +243,9 @@ func createLocalClient(args args, localClientFactory func(map[string][]string) (
|
||||
dockerClient, err := localClientFactory(args.Filter)
|
||||
if err == nil {
|
||||
_, err := dockerClient.ListContainers()
|
||||
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
log.Warnf("Could not connect to local Docker Engine: %s", err)
|
||||
} else {
|
||||
log.Debugf("Connected to local Docker Engine")
|
||||
return dockerClient
|
||||
}
|
||||
@@ -251,6 +289,7 @@ func parseArgs() args {
|
||||
}
|
||||
|
||||
if args.Username != "" || args.Password != "" {
|
||||
log.Warn("Using --username and --password is being deprecated and removed in v6.x. Use --auth-provider instead. See https://dozzle.dev/guide/authentication#file-based-user-management for more information.")
|
||||
if args.Username == "" || args.Password == "" {
|
||||
log.Fatalf("Username AND password are required for authentication")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -77,7 +77,7 @@ func Test_valid_remote(t *testing.T) {
|
||||
RemoteHost: []string{"tcp://test:2375"},
|
||||
}
|
||||
|
||||
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory)
|
||||
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory, "")
|
||||
|
||||
assert.Equal(t, 1, len(clients))
|
||||
assert.Contains(t, clients, "test")
|
||||
@@ -106,7 +106,7 @@ func Test_valid_remote_and_local(t *testing.T) {
|
||||
RemoteHost: []string{"tcp://test:2375"},
|
||||
}
|
||||
|
||||
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory)
|
||||
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory, "")
|
||||
|
||||
assert.Equal(t, 2, len(clients))
|
||||
assert.Contains(t, clients, "test")
|
||||
@@ -133,7 +133,7 @@ func Test_no_clients(t *testing.T) {
|
||||
|
||||
args := args{}
|
||||
|
||||
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory)
|
||||
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory, "")
|
||||
|
||||
assert.Equal(t, 0, len(clients))
|
||||
local.AssertExpectations(t)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "5.0.2",
|
||||
"version": "5.2.3",
|
||||
"description": "Realtime log viewer for docker containers. ",
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/amir20/dozzle/issues"
|
||||
},
|
||||
"packageManager": "pnpm@8.7.6",
|
||||
"packageManager": "pnpm@8.10.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/amir20/dozzle.git"
|
||||
@@ -29,16 +29,16 @@
|
||||
"dependencies": {
|
||||
"@iconify-json/carbon": "^1.1.21",
|
||||
"@iconify-json/cil": "^1.1.5",
|
||||
"@iconify-json/mdi": "^1.1.54",
|
||||
"@iconify-json/mdi": "^1.1.55",
|
||||
"@iconify-json/mdi-light": "^1.1.7",
|
||||
"@iconify-json/octicon": "^1.1.49",
|
||||
"@iconify-json/ph": "^1.1.6",
|
||||
"@intlify/unplugin-vue-i18n": "^1.2.0",
|
||||
"@vueuse/components": "^10.4.1",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"@vueuse/integrations": "^10.4.1",
|
||||
"@vueuse/math": "^10.4.1",
|
||||
"@vueuse/router": "^10.4.1",
|
||||
"@intlify/unplugin-vue-i18n": "^1.5.0",
|
||||
"@vueuse/components": "^10.5.0",
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@vueuse/integrations": "^10.5.0",
|
||||
"@vueuse/math": "^10.5.0",
|
||||
"@vueuse/router": "^10.5.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"d3-array": "^3.2.4",
|
||||
@@ -47,61 +47,58 @@
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"daisyui": "^3.8.1",
|
||||
"daisyui": "^3.9.4",
|
||||
"date-fns": "^2.30.0",
|
||||
"entities": "^4.5.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"pinia": "^2.1.6",
|
||||
"postcss": "^8.4.30",
|
||||
"pinia": "^2.1.7",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-mixins": "^9.0.4",
|
||||
"splitpanes": "^3.1.5",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"vite": "4.4.9",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"unplugin-auto-import": "^0.16.7",
|
||||
"unplugin-icons": "^0.17.3",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"unplugin-vue-macros": "^2.6.2",
|
||||
"vite": "4.5.0",
|
||||
"vite-plugin-pages": "^0.31.0",
|
||||
"vite-plugin-vue-layouts": "^0.8.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.4.1",
|
||||
"vue": "^3.3.7",
|
||||
"vue-i18n": "^9.6.4",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/ic": "^1.1.14",
|
||||
"@pinia/testing": "^0.1.3",
|
||||
"@playwright/test": "^1.38.1",
|
||||
"@types/d3-array": "^3.0.8",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-scale": "^4.0.5",
|
||||
"@types/d3-selection": "^3.0.7",
|
||||
"@types/d3-shape": "^3.1.3",
|
||||
"@types/d3-transition": "^3.0.5",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^20.7.1",
|
||||
"@types/semver": "^7.5.3",
|
||||
"@vitejs/plugin-vue": "4.3.4",
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@types/d3-array": "^3.2.0",
|
||||
"@types/d3-ease": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.6",
|
||||
"@types/d3-selection": "^3.0.8",
|
||||
"@types/d3-shape": "^3.1.4",
|
||||
"@types/d3-transition": "^3.0.6",
|
||||
"@types/lodash.debounce": "^4.0.8",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/semver": "^7.5.4",
|
||||
"@vitejs/plugin-vue": "4.4.0",
|
||||
"@vue/compiler-sfc": "^3.3.7",
|
||||
"@vue/test-utils": "^2.4.1",
|
||||
"bumpp": "^9.2.0",
|
||||
"c8": "^8.0.1",
|
||||
"concurrently": "^8.2.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"jest-serializer-vue": "^3.1.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lint-staged": "^14.0.1",
|
||||
"lint-staged": "^15.0.2",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-icons": "^0.17.0",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"unplugin-vue-macros": "^2.6.0",
|
||||
"vite": "4.4.9",
|
||||
"vite-plugin-pages": "^0.31.0",
|
||||
"vite-plugin-vue-layouts": "^0.8.0",
|
||||
"vitepress": "1.0.0-rc.20",
|
||||
"vitest": "^0.34.5",
|
||||
"vue-tsc": "^1.8.15"
|
||||
"vitepress": "1.0.0-rc.24",
|
||||
"vitest": "^0.34.6",
|
||||
"vue-tsc": "^1.8.22"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css,ts,html,md}": [
|
||||
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
@@ -19,8 +19,9 @@
|
||||
}}
|
||||
<link rel="stylesheet" href="{{ .Base }}/{{ $css }}" />
|
||||
<script type="module" src="{{ .Base }}/{{ $js }}"></script>
|
||||
|
||||
{{- end }}
|
||||
<meta name="description" content="Dozzle is a log viewer for Docker" />
|
||||
<link rel="apple-touch-icon" href="{{ .Base }}/apple-touch-icon.png" sizes="512x512" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -65,6 +65,9 @@ export default defineConfig(() => ({
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ["**/data/**"],
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: {
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/amir20/dozzle/analytics"
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Config is a struct for configuring the web service
|
||||
type Config struct {
|
||||
Base string
|
||||
Addr string
|
||||
Version string
|
||||
Username string
|
||||
Password string
|
||||
Hostname string
|
||||
NoAnalytics bool
|
||||
Dev bool
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
clients map[string]DockerClient
|
||||
content fs.FS
|
||||
config *Config
|
||||
}
|
||||
|
||||
// Client is a proxy around the docker client
|
||||
type DockerClient interface {
|
||||
ListContainers() ([]docker.Container, error)
|
||||
FindContainer(string) (docker.Container, error)
|
||||
ContainerLogs(context.Context, string, string, docker.StdType) (io.ReadCloser, error)
|
||||
Events(context.Context, chan<- docker.ContainerEvent) <-chan error
|
||||
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, docker.StdType) (io.ReadCloser, error)
|
||||
ContainerStats(context.Context, string, chan<- docker.ContainerStat) error
|
||||
Ping(context.Context) (types.Ping, error)
|
||||
Host() *docker.Host
|
||||
}
|
||||
|
||||
func CreateServer(clients map[string]DockerClient, content fs.FS, config Config) *http.Server {
|
||||
handler := &handler{
|
||||
clients: clients,
|
||||
content: content,
|
||||
config: &config,
|
||||
}
|
||||
return &http.Server{Addr: config.Addr, Handler: createRouter(handler)}
|
||||
}
|
||||
|
||||
var fileServer http.Handler
|
||||
|
||||
func createRouter(h *handler) *chi.Mux {
|
||||
initializeAuth(h)
|
||||
|
||||
base := h.config.Base
|
||||
r := chi.NewRouter()
|
||||
|
||||
if !h.config.Dev {
|
||||
r.Use(cspHeaders)
|
||||
}
|
||||
|
||||
r.Route(base, func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(authorizationRequired)
|
||||
r.Get("/api/logs/stream/{host}/{id}", h.streamLogs)
|
||||
r.Get("/api/logs/download/{host}/{id}", h.downloadLogs)
|
||||
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
|
||||
r.Get("/api/events/stream", h.streamEvents)
|
||||
r.Get("/logout", h.clearSession)
|
||||
r.Get("/version", h.version)
|
||||
})
|
||||
|
||||
r.Post("/api/validateCredentials", h.validateCredentials)
|
||||
r.Get("/healthcheck", h.healthcheck)
|
||||
defaultHandler := http.StripPrefix(strings.Replace(base+"/", "//", "/", 1), http.HandlerFunc(h.index))
|
||||
r.Get("/*", func(w http.ResponseWriter, req *http.Request) {
|
||||
defaultHandler.ServeHTTP(w, req)
|
||||
})
|
||||
})
|
||||
|
||||
if base != "/" {
|
||||
r.Get(base, func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
|
||||
})
|
||||
}
|
||||
|
||||
fileServer = http.FileServer(http.FS(h.content))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
_, err := h.content.Open(req.URL.Path)
|
||||
if err == nil && req.URL.Path != "" && req.URL.Path != "/" {
|
||||
fileServer.ServeHTTP(w, req)
|
||||
if !h.config.NoAnalytics {
|
||||
go func() {
|
||||
host, _ := os.Hostname()
|
||||
|
||||
var client DockerClient
|
||||
for _, v := range h.clients {
|
||||
client = v
|
||||
break
|
||||
}
|
||||
|
||||
if containers, err := client.ListContainers(); err == nil {
|
||||
totalContainers := len(containers)
|
||||
runningContainers := 0
|
||||
for _, container := range containers {
|
||||
if container.State == "running" {
|
||||
runningContainers++
|
||||
}
|
||||
}
|
||||
|
||||
re := analytics.RequestEvent{
|
||||
ClientId: host,
|
||||
TotalContainers: totalContainers,
|
||||
RunningContainers: runningContainers,
|
||||
}
|
||||
analytics.SendRequestEvent(re)
|
||||
}
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
if !isAuthorized(req) && req.URL.Path != "login" {
|
||||
http.Redirect(w, req, path.Clean(h.config.Base+"/login"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
h.executeTemplate(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
file, err := h.content.Open("index.html")
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
bytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
tmpl, err := template.New("index.html").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) template.JS {
|
||||
a, _ := json.Marshal(v)
|
||||
return template.JS(a)
|
||||
},
|
||||
}).Parse(string(bytes))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
path := ""
|
||||
if h.config.Base != "/" {
|
||||
path = h.config.Base
|
||||
}
|
||||
|
||||
hosts := make([]*docker.Host, 0, len(h.clients))
|
||||
for _, v := range h.clients {
|
||||
hosts = append(hosts, v.Host())
|
||||
}
|
||||
sort.Slice(hosts, func(i, j int) bool {
|
||||
return hosts[i].Name < hosts[j].Name
|
||||
})
|
||||
|
||||
config := map[string]interface{}{
|
||||
"base": path,
|
||||
"version": h.config.Version,
|
||||
"authorizationNeeded": h.isAuthorizationNeeded(req),
|
||||
"secured": secured,
|
||||
"hostname": h.config.Hostname,
|
||||
"hosts": hosts,
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Config": config,
|
||||
"Dev": h.config.Dev,
|
||||
"Manifest": h.readManifest(),
|
||||
"Base": path,
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) readManifest() map[string]interface{} {
|
||||
if h.config.Dev {
|
||||
return map[string]interface{}{}
|
||||
} else {
|
||||
file, err := h.content.Open("manifest.json")
|
||||
if err != nil {
|
||||
// this should only happen during test. In production, the file is embedded in the binary and checked in main.go
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
bytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not read manifest.json: %v", err)
|
||||
}
|
||||
var manifest map[string]interface{}
|
||||
err = json.Unmarshal(bytes, &manifest)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse manifest.json: %v", err)
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *handler) version(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, "<pre>%v</pre>", h.config.Version)
|
||||
}
|
||||
|
||||
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
|
||||
log.Trace("Executing healthcheck request")
|
||||
var client DockerClient
|
||||
for _, v := range h.clients {
|
||||
client = v
|
||||
break
|
||||
}
|
||||
|
||||
if ping, err := client.Ping(r.Context()); err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
fmt.Fprintf(w, "OK API Version %v", ping.APIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) clientFromRequest(r *http.Request) DockerClient {
|
||||
host := chi.URLParam(r, "host")
|
||||
|
||||
if host == "" {
|
||||
log.Fatalf("No host found for url %v", r.URL)
|
||||
}
|
||||
|
||||
if client, ok := h.clients[host]; ok {
|
||||
return client
|
||||
}
|
||||
|
||||
log.Fatalf("No client found for host %v and url %v", host, r.URL)
|
||||
return nil
|
||||
}
|
||||