mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-08 01:41:56 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 480fdea272 | |||
| 573ee5ed03 | |||
| cb6267f9ae | |||
| 3aa3b879de | |||
| 79bf0f01f9 | |||
| 6b442d26a9 | |||
| 67348ab514 | |||
| cb9ab3f944 | |||
| 0a722bd2da | |||
| 3195bc0d80 | |||
| 7bf455ae6b | |||
| 285a8bf251 | |||
| 65d690074c | |||
| 264af62bff | |||
| c3ac804e7a | |||
| e8e1c3ea6b | |||
| 29f06eb00d | |||
| 020342a1bd | |||
| 3b0b3ed5fc | |||
| 6b121633e9 |
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.1
|
||||
go-version: 1.25.2
|
||||
check-latest: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
@@ -96,12 +96,12 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.5.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3.5.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -15,12 +15,12 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.5.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3.5.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.1"
|
||||
go-version: "1.25.2"
|
||||
check-latest: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.1"
|
||||
go-version: "1.25.2"
|
||||
check-latest: true
|
||||
- name: Generate dependencies
|
||||
run: make fake_assets shared_key.pem shared_cert.pem
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{{ config.user.email }}
|
||||
</div>
|
||||
</div>
|
||||
<ul v-if="config.authProvider === 'simple'" class="menu mt-4 p-0">
|
||||
<ul v-if="config.authProvider === 'simple' || config.logoutUrl" class="menu mt-4 p-0">
|
||||
<li>
|
||||
<button @click.prevent="logout()" class="text-primary p-2">
|
||||
<material-symbols:logout />
|
||||
@@ -46,11 +46,17 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
async function logout() {
|
||||
await fetch(withBase("/api/token"), {
|
||||
method: "DELETE",
|
||||
});
|
||||
const { logoutUrl } = config;
|
||||
|
||||
location.reload();
|
||||
async function logout() {
|
||||
if (logoutUrl) {
|
||||
location.href = logoutUrl;
|
||||
} else {
|
||||
await fetch(withBase("/api/token"), {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="hover:text-secondary relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
|
||||
<div class="hover:text-primary relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
|
||||
<div class="border-primary overflow-hidden rounded-xs border px-px pt-1 pb-px max-md:hidden">
|
||||
<StatSparkline :data="data" @selected-point="onSelectedPoint" />
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Config {
|
||||
hostname: string;
|
||||
hosts: Host[];
|
||||
authProvider: "simple" | "none" | "forward-proxy";
|
||||
logoutUrl?: string;
|
||||
enableActions: boolean;
|
||||
enableShell: boolean;
|
||||
enableDownload: boolean;
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ services:
|
||||
|
||||
playwright:
|
||||
container_name: playwright
|
||||
image: mcr.microsoft.com/playwright:v1.55.1-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
@@ -211,10 +211,19 @@ In this mode, Dozzle expects the following headers:
|
||||
- `Remote-Filter` to be a comma-separated list of filters allowed for user.
|
||||
- `Remote-Roles` to be a comma-separated list of roles allowed for user.
|
||||
|
||||
Additionally, you can configure a logout URL with:
|
||||
|
||||
```yaml
|
||||
DOZZLE_AUTH_LOGOUT_URL: http://oauth2.example.ru/oauth2/sign_out
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
<details>
|
||||
<summary>➡️ Click to expand Authelia example</summary>
|
||||
|
||||
::: code-group
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
@@ -349,6 +358,8 @@ notifier:
|
||||
|
||||
Valid SSL keys are required because Authelia only supports SSL.
|
||||
|
||||
</details>
|
||||
|
||||
### Setting up Dozzle with Cloudflare Zero Trust
|
||||
|
||||
Cloudflare Zero Trust is a service for authenticated access to self-hosted software. This section defines how Dozzle can be set up to use Cloudflare Zero Trust for authentication.
|
||||
@@ -372,3 +383,86 @@ services:
|
||||
```
|
||||
|
||||
After running the Dozzle container, configure the Application in Cloudflare Zero Trust dashboard by following the [guide](https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/self-hosted-apps/).
|
||||
|
||||
### Setting up Dozzle with Pocket ID
|
||||
|
||||
You must first setup a container to pass OpenID Connect authentication through your reverse proxy.
|
||||
|
||||
Below is an example using [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy).
|
||||
|
||||
<details>
|
||||
<summary>➡️ Click to expand oauth2-proxy example</summary>
|
||||
|
||||
1. Create a new OIDC client in Pocket ID for Dozzle:
|
||||
- **Name:** `Dozzle`
|
||||
- **Callback URLs:** `https://dozzle.example.com/oauth2/callback`
|
||||
- **PKCE:** `Enabled`
|
||||
|
||||
Copy the **Client ID** and **Client Secret** values for use later.
|
||||
|
||||
2. Add the following to your existing Dozzle compose:
|
||||
|
||||
```yml
|
||||
environment:
|
||||
DOZZLE_AUTH_PROVIDER: forward-proxy
|
||||
DOZZLE_AUTH_HEADER_USER: X-Forwarded-User
|
||||
DOZZLE_AUTH_HEADER_EMAIL: X-Forwarded-Email
|
||||
DOZZLE_AUTH_HEADER_NAME: X-Forwarded-Preferred-Username
|
||||
```
|
||||
|
||||
Comment out the Dozzle ports, as we will redirect these through the new authentication container.
|
||||
|
||||
This method should not require any changes to your reverse proxy configuration.
|
||||
|
||||
```yml
|
||||
# ports:
|
||||
# - 8080:8080
|
||||
```
|
||||
|
||||
3. Add a new oauth2-proxy container service to your existing Dozzle compose:
|
||||
|
||||
```yml
|
||||
services:
|
||||
# ...
|
||||
oauth2-proxy:
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:latest
|
||||
restart: unless-stopped
|
||||
container_name: dozzle-oidc
|
||||
command: --config /oauth2-proxy.cfg
|
||||
volumes:
|
||||
- "./oauth2-proxy.cfg:/oauth2-proxy.cfg"
|
||||
ports:
|
||||
- 8080:4180
|
||||
```
|
||||
|
||||
4. Create the oauth2-proxy config file.
|
||||
|
||||
In the directory beside your compose file, create `oauth2-proxy.cfg` :
|
||||
|
||||
```toml
|
||||
client_id = "xxx" # from Pocket ID
|
||||
client_secret = "xxx" # from Pocket ID
|
||||
cookie_secret = "xxx" # generate with openssl rand -base64 32 | tr -- '+/' '-_'
|
||||
upstreams = "http://dozzle:8080" # upstream to Dozzle containers internal port
|
||||
code_challenge_method = "S256" # PKCE challenges plain or S256
|
||||
cookie_expire = "0" # seconds, 0 for session
|
||||
cookie_name = "__Host-oauth2-proxy" # or __Secure-oauth2-proxy (less secure)
|
||||
cookie_secure = true # uses the secure HTTPS cookie
|
||||
email_domains = ["*"] # allows any email domain to authenticate
|
||||
http_address = "0.0.0.0:4180" # port oauth2-proxy listens on
|
||||
oidc_issuer_url = "https://id.example.com" # your Pocket base URL
|
||||
provider_display_name = "Pocket ID" # display name for OIDC login
|
||||
provider = "oidc" # use OpenID connect
|
||||
reverse_proxy = true # reverse proxy the traffic
|
||||
scope = "openid email profile groups" # passthru these OIDC scopes
|
||||
```
|
||||
|
||||
Fill in the variables per the comments.
|
||||
|
||||
5. Finally - restart your Docker compose stack.
|
||||
|
||||
Your reverse proxy should now authenticate you to Dozzle via oauth2-proxy.
|
||||
|
||||
Check logs for troubleshooting.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -8,7 +8,7 @@ Dozzle supports connecting to remote Docker hosts. This is useful when running D
|
||||
|
||||
However, with Dozzle agents, you can connect to remote hosts without exposing the Docker socket. See the [agent](/guide/agent) page for more information.
|
||||
|
||||
Dozzle agents remove the need to remotely expose the Docker socket but cannot be uses with a Docker Socket proxy inside the Dozzle agent stack. If you wish to use a Socket Proxy on it's own without an agent see the [connecting with a socket proxy](#connecting-with-a-socket-proxy) section.
|
||||
Dozzle agents remove the need to remotely expose the Docker socket but cannot be used with a Docker Socket proxy inside the Dozzle agent stack. If you wish to use a Socket Proxy on it's own without an agent see the [connecting with a socket proxy](#connecting-with-a-socket-proxy) section.
|
||||
|
||||
> [!WARNING]
|
||||
> Remote hosts have been replaced with agents. Agents provide a more secure way to connect to remote hosts. Although remote hosts are still supported, it is recommended to use agents. See the [agent](/guide/agent) page for more information and examples. For comparison, see the [comparing agents with remote connections](/guide/agent#comparing-agents-with-remote-connection) section. I won't be able to investigate user's issues with remote hosts as it is very time consuming.
|
||||
|
||||
@@ -18,6 +18,7 @@ Configurations can be done with flags or environment variables. The table below
|
||||
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
|
||||
| `--auth-header-filter` | `DOZZLE_AUTH_HEADER_FILTER` | `Remote-Filter` |
|
||||
| `--auth-header-roles` | `DOZZLE_AUTH_HEADER_ROLES` | `Remote-Roles` |
|
||||
| `--auth-logout-url` | `DOZZLE_AUTH_LOGOUT_URL` | `""` |
|
||||
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | `false` |
|
||||
| `--enable-shell` | `DOZZLE_ENABLE_SHELL` | `false` |
|
||||
| `--disable-avatars` | `DOZZLE_DISABLE_AVATARS` | `false` |
|
||||
|
||||
@@ -4,7 +4,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/alexflint/go-arg v1.6.0
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/docker/docker v28.4.0+incompatible
|
||||
github.com/docker/docker v28.5.1+incompatible
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
@@ -17,8 +17,8 @@ require (
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -27,17 +27,17 @@ require (
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/jwtauth/v5 v5.3.3
|
||||
github.com/go-faker/faker/v4 v4.6.2
|
||||
github.com/go-faker/faker/v4 v4.7.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/samber/lo v1.51.0
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/sync v0.17.0
|
||||
google.golang.org/grpc v1.75.1
|
||||
google.golang.org/protobuf v1.36.9
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/client-go v0.34.1
|
||||
@@ -102,10 +102,10 @@ require (
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/term v0.36.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
@@ -119,4 +119,4 @@ require (
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
go 1.25.1
|
||||
go 1.25.2
|
||||
|
||||
@@ -58,6 +58,10 @@ github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjY
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
|
||||
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E=
|
||||
github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -88,6 +92,8 @@ github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt
|
||||
github.com/go-faker/faker/v4 v4.6.1/go.mod h1:arSdxNCSt7mOhdk8tEolvHeIJ7eX4OX80wXjKKvkKBY=
|
||||
github.com/go-faker/faker/v4 v4.6.2 h1:IR1uQUYotFZnuTL7Iuy0FDGtHM5Rt1Q+2nipH9gnqKs=
|
||||
github.com/go-faker/faker/v4 v4.6.2/go.mod h1:u1dIRP5neLB6kTzgyVjdBOV5R1uP7BdxkcWk7tiKQXk=
|
||||
github.com/go-faker/faker/v4 v4.7.0 h1:VboC02cXHl/NuQh5lM2W8b87yp4iFXIu59x4w0RZi4E=
|
||||
github.com/go-faker/faker/v4 v4.7.0/go.mod h1:u1dIRP5neLB6kTzgyVjdBOV5R1uP7BdxkcWk7tiKQXk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -222,6 +228,8 @@ github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
|
||||
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
|
||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
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.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
@@ -328,6 +336,8 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -356,6 +366,8 @@ golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
@@ -409,6 +421,8 @@ golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -428,6 +442,8 @@ golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -449,6 +465,8 @@ golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -476,6 +494,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||
@@ -492,6 +512,8 @@ google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
@@ -500,6 +522,8 @@ google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyM
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/encoding/gzip"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
@@ -45,7 +46,10 @@ func NewClient(endpoint string, certificates tls.Certificate, opts ...grpc.DialO
|
||||
// Create the gRPC transport credentials
|
||||
creds := credentials.NewTLS(tlsConfig)
|
||||
|
||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||
opts = append(opts,
|
||||
grpc.WithTransportCredentials(creds),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(10*1024*1024), grpc.UseCompressor(gzip.Name)),
|
||||
)
|
||||
conn, err := grpc.NewClient(endpoint, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to %s: %w", endpoint, err)
|
||||
@@ -88,7 +92,7 @@ func rpcErrToErr(err error) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since time.Time, until time.Time, std container.StdType) (<-chan *container.LogEvent, error) {
|
||||
func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since time.Time, until time.Time, std container.StdType) (<-chan container.LogEvent, error) {
|
||||
stream, err := c.client.LogsBetweenDates(ctx, &pb.LogsBetweenDatesRequest{
|
||||
ContainerId: containerID,
|
||||
Since: timestamppb.New(since),
|
||||
@@ -100,7 +104,7 @@ func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := make(chan *container.LogEvent)
|
||||
events := make(chan container.LogEvent)
|
||||
|
||||
go func() {
|
||||
sendLogs(stream, events)
|
||||
@@ -110,7 +114,7 @@ func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, since time.Time, std container.StdType, events chan<- *container.LogEvent) error {
|
||||
func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, since time.Time, std container.StdType, events chan<- container.LogEvent) error {
|
||||
stream, err := c.client.StreamLogs(ctx, &pb.StreamLogsRequest{
|
||||
ContainerId: containerID,
|
||||
Since: timestamppb.New(since),
|
||||
@@ -124,7 +128,7 @@ func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, si
|
||||
return sendLogs(stream, events)
|
||||
}
|
||||
|
||||
func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *container.LogEvent) error {
|
||||
func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- container.LogEvent) error {
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
@@ -150,7 +154,7 @@ func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *container.
|
||||
continue
|
||||
}
|
||||
|
||||
events <- &container.LogEvent{
|
||||
events <- container.LogEvent{
|
||||
Id: resp.Event.Id,
|
||||
ContainerID: resp.Event.ContainerId,
|
||||
Message: message,
|
||||
|
||||
@@ -65,7 +65,7 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
|
||||
for event := range g.Events {
|
||||
out.Send(&pb.StreamLogsResponse{
|
||||
Event: logEventToPb(event),
|
||||
Event: logEventToPb(&event),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
|
||||
select {
|
||||
case event := <-g.Events:
|
||||
out.Send(&pb.StreamLogsResponse{
|
||||
Event: logEventToPb(event),
|
||||
Event: logEventToPb(&event),
|
||||
})
|
||||
case e := <-g.Errors:
|
||||
return e
|
||||
|
||||
+28
-8
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -18,23 +19,42 @@ const (
|
||||
const All = Shell | Actions | Download
|
||||
|
||||
// ParseRole parses a comma-separated string of roles and returns the corresponding Role.
|
||||
func ParseRole(commaValues string) Role {
|
||||
func ParseRole(input string) Role {
|
||||
var roles Role
|
||||
for r := range strings.SplitSeq(commaValues, ",") {
|
||||
var parts []string
|
||||
|
||||
// Check if input is valid JSON
|
||||
trimmed := strings.TrimSpace(input)
|
||||
if json.Valid([]byte(trimmed)) {
|
||||
var jsonRoles []string
|
||||
if err := json.Unmarshal([]byte(trimmed), &jsonRoles); err == nil {
|
||||
parts = jsonRoles
|
||||
} else {
|
||||
log.Warn().Str("input", input).Msg("failed to parse JSON roles")
|
||||
return None
|
||||
}
|
||||
} else {
|
||||
// Split by both commas and pipes
|
||||
parts = strings.FieldsFunc(input, func(c rune) bool {
|
||||
return c == ',' || c == '|'
|
||||
})
|
||||
}
|
||||
|
||||
for _, r := range parts {
|
||||
role := strings.TrimSpace(strings.ToLower(r))
|
||||
switch role {
|
||||
case "shell":
|
||||
case "shell", "dozzle_shell":
|
||||
roles |= Shell
|
||||
case "actions":
|
||||
case "actions", "dozzle_actions":
|
||||
roles |= Actions
|
||||
case "download":
|
||||
case "download", "dozzle_download":
|
||||
roles |= Download
|
||||
case "none":
|
||||
case "none", "dozzle_none":
|
||||
return None
|
||||
case "all":
|
||||
case "all", "dozzle_all":
|
||||
return All
|
||||
default:
|
||||
log.Warn().Str("role", role).Msg("invalid role")
|
||||
log.Debug().Str("role", role).Msg("invalid role")
|
||||
}
|
||||
}
|
||||
return roles
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseRole(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected Role
|
||||
}{
|
||||
// Single role tests
|
||||
{"Single shell role", "shell", Shell},
|
||||
{"Single actions role", "actions", Actions},
|
||||
{"Single download role", "download", Download},
|
||||
{"None role", "none", None},
|
||||
{"All role", "all", All},
|
||||
|
||||
// Single dozzle_ prefixed role tests
|
||||
{"Single dozzle_shell role", "dozzle_shell", Shell},
|
||||
{"Single dozzle_actions role", "dozzle_actions", Actions},
|
||||
{"Single dozzle_download role", "dozzle_download", Download},
|
||||
{"Dozzle_none role", "dozzle_none", None},
|
||||
{"Dozzle_all role", "dozzle_all", All},
|
||||
|
||||
// Case insensitive tests
|
||||
{"Shell uppercase", "SHELL", Shell},
|
||||
{"Actions mixed case", "AcTiOnS", Actions},
|
||||
{"Download with spaces", " download ", Download},
|
||||
{"Dozzle_shell uppercase", "DOZZLE_SHELL", Shell},
|
||||
{"Dozzle_actions mixed case", "DoZzLe_AcTiOnS", Actions},
|
||||
{"Dozzle_download with spaces", " dozzle_download ", Download},
|
||||
|
||||
// Multiple roles with comma separator
|
||||
{"Shell and actions", "shell,actions", Shell | Actions},
|
||||
{"All three roles", "shell,actions,download", Shell | Actions | Download},
|
||||
{"Roles with spaces", "shell , actions , download", Shell | Actions | Download},
|
||||
{"Dozzle roles with comma", "dozzle_shell,dozzle_actions", Shell | Actions},
|
||||
{"Mixed dozzle and regular", "shell,dozzle_actions,download", Shell | Actions | Download},
|
||||
|
||||
// Multiple roles with pipe separator
|
||||
{"Shell and actions with pipe", "shell|actions", Shell | Actions},
|
||||
{"All three with pipe", "shell|actions|download", Shell | Actions | Download},
|
||||
{"Mixed separators", "shell,actions|download", Shell | Actions | Download},
|
||||
{"Dozzle roles with pipe", "dozzle_shell|dozzle_actions", Shell | Actions},
|
||||
|
||||
// JSON format tests
|
||||
{"JSON single role", `["shell"]`, Shell},
|
||||
{"JSON multiple roles", `["shell", "actions"]`, Shell | Actions},
|
||||
{"JSON all roles", `["shell", "actions", "download"]`, Shell | Actions | Download},
|
||||
{"JSON with spaces", ` ["shell", "actions"] `, Shell | Actions},
|
||||
{"JSON single dozzle role", `["dozzle_shell"]`, Shell},
|
||||
{"JSON multiple dozzle roles", `["dozzle_shell", "dozzle_actions"]`, Shell | Actions},
|
||||
{"JSON mixed dozzle and regular", `["shell", "dozzle_actions"]`, Shell | Actions},
|
||||
|
||||
// Edge cases
|
||||
{"Empty string", "", None},
|
||||
{"Whitespace only", " ", None},
|
||||
{"Invalid role", "invalid", None},
|
||||
{"Mixed valid and invalid", "shell,invalid,actions", Shell | Actions},
|
||||
{"None overrides others", "shell,none,actions", None},
|
||||
{"All overrides others", "shell,all,actions", All},
|
||||
{"Dozzle_none overrides others", "dozzle_shell,dozzle_none,dozzle_actions", None},
|
||||
{"Dozzle_all overrides others", "dozzle_shell,dozzle_all,dozzle_actions", All},
|
||||
|
||||
// Invalid JSON
|
||||
{"Invalid JSON format", `["shell"`, None},
|
||||
{"Malformed JSON", `{shell: "test"}`, None},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ParseRole(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("ParseRole(%q) = %d, expected %d", tc.input, int(result), int(tc.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,11 @@ import (
|
||||
)
|
||||
|
||||
type EventGenerator struct {
|
||||
Events chan *LogEvent
|
||||
Events chan LogEvent
|
||||
Errors chan error
|
||||
reader LogReader
|
||||
next *LogEvent
|
||||
buffer chan *LogEvent
|
||||
buffer chan LogEvent
|
||||
wg sync.WaitGroup
|
||||
containerID string
|
||||
ctx context.Context
|
||||
@@ -36,9 +36,9 @@ type LogReader interface {
|
||||
func NewEventGenerator(ctx context.Context, reader LogReader, container Container) *EventGenerator {
|
||||
generator := &EventGenerator{
|
||||
reader: reader,
|
||||
buffer: make(chan *LogEvent, 100),
|
||||
buffer: make(chan LogEvent, 100),
|
||||
Errors: make(chan error, 1),
|
||||
Events: make(chan *LogEvent),
|
||||
Events: make(chan LogEvent),
|
||||
containerID: container.ID,
|
||||
ctx: ctx,
|
||||
}
|
||||
@@ -49,24 +49,29 @@ func NewEventGenerator(ctx context.Context, reader LogReader, container Containe
|
||||
}
|
||||
|
||||
func (g *EventGenerator) processBuffer() {
|
||||
var current, next *LogEvent
|
||||
var current, next LogEvent
|
||||
var hasNext bool
|
||||
|
||||
loop:
|
||||
for {
|
||||
if g.next != nil {
|
||||
current = g.next
|
||||
current = *g.next
|
||||
g.next = nil
|
||||
next = g.peek()
|
||||
next, hasNext = g.peek()
|
||||
} else {
|
||||
event, ok := <-g.buffer
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
current = event
|
||||
next = g.peek()
|
||||
next, hasNext = g.peek()
|
||||
}
|
||||
|
||||
checkPosition(current, next)
|
||||
if hasNext {
|
||||
checkPosition(¤t, &next)
|
||||
} else {
|
||||
checkPosition(¤t, nil)
|
||||
}
|
||||
|
||||
select {
|
||||
case g.Events <- current:
|
||||
@@ -101,23 +106,23 @@ func (g *EventGenerator) consumeReader() {
|
||||
g.wg.Done()
|
||||
}
|
||||
|
||||
func (g *EventGenerator) peek() *LogEvent {
|
||||
func (g *EventGenerator) peek() (LogEvent, bool) {
|
||||
if g.next != nil {
|
||||
return g.next
|
||||
return *g.next, true
|
||||
}
|
||||
select {
|
||||
case event := <-g.buffer:
|
||||
g.next = event
|
||||
return g.next
|
||||
g.next = &event
|
||||
return event, true
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
return nil
|
||||
return LogEvent{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func createEvent(message string, streamType StdType) *LogEvent {
|
||||
func createEvent(message string, streamType StdType) LogEvent {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(message))
|
||||
logEvent := &LogEvent{Id: h.Sum32(), Message: message, Stream: streamType.String()}
|
||||
logEvent := LogEvent{Id: h.Sum32(), Message: message, Stream: streamType.String()}
|
||||
if index := strings.IndexAny(message, " "); index != -1 {
|
||||
logId := message[:index]
|
||||
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
|
||||
@@ -157,20 +162,20 @@ func createEvent(message string, streamType StdType) *LogEvent {
|
||||
}
|
||||
|
||||
func checkPosition(currentEvent *LogEvent, nextEvent *LogEvent) {
|
||||
currentLevel := guessLogLevel(currentEvent)
|
||||
currentLevel := guessLogLevel(*currentEvent)
|
||||
if nextEvent != nil {
|
||||
if currentEvent.IsCloseToTime(nextEvent) && currentLevel != "unknown" && !nextEvent.HasLevel() {
|
||||
if currentEvent.IsCloseToTime(*nextEvent) && currentLevel != "unknown" && !nextEvent.HasLevel() {
|
||||
currentEvent.Position = Beginning
|
||||
nextEvent.Position = Middle
|
||||
}
|
||||
|
||||
// If next item is not close to current item or has level, set current item position to end
|
||||
if currentEvent.Position == Middle && (nextEvent.HasLevel() || !currentEvent.IsCloseToTime(nextEvent)) {
|
||||
if currentEvent.Position == Middle && (nextEvent.HasLevel() || !currentEvent.IsCloseToTime(*nextEvent)) {
|
||||
currentEvent.Position = End
|
||||
}
|
||||
|
||||
// If next item is close to current item and has no level, set next item position to middle
|
||||
if currentEvent.Position == Middle && !nextEvent.HasLevel() && currentEvent.IsCloseToTime(nextEvent) {
|
||||
if currentEvent.Position == Middle && !nextEvent.HasLevel() && currentEvent.IsCloseToTime(*nextEvent) {
|
||||
nextEvent.Position = Middle
|
||||
}
|
||||
// Set next item level to current item level
|
||||
|
||||
@@ -96,14 +96,14 @@ func Test_createEvent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *LogEvent
|
||||
want LogEvent
|
||||
}{
|
||||
{
|
||||
name: "empty message",
|
||||
args: args{
|
||||
message: "",
|
||||
},
|
||||
want: &LogEvent{
|
||||
want: LogEvent{
|
||||
Message: "",
|
||||
},
|
||||
}, {
|
||||
@@ -111,7 +111,7 @@ func Test_createEvent(t *testing.T) {
|
||||
args: args{
|
||||
message: "2020-05-13T18:55:37.772853839Z {\"xyz\": \"value\", \"abc\": \"value2\"}",
|
||||
},
|
||||
want: &LogEvent{
|
||||
want: LogEvent{
|
||||
Message: data,
|
||||
},
|
||||
},
|
||||
@@ -120,7 +120,7 @@ func Test_createEvent(t *testing.T) {
|
||||
args: args{
|
||||
message: "2020-05-13T18:55:37.772853839Z {\"key\"}",
|
||||
},
|
||||
want: &LogEvent{
|
||||
want: LogEvent{
|
||||
Message: "{\"key\"}",
|
||||
},
|
||||
},
|
||||
@@ -129,7 +129,7 @@ func Test_createEvent(t *testing.T) {
|
||||
args: args{
|
||||
message: "2020-05-13T18:55:37.772853839Z 123",
|
||||
},
|
||||
want: &LogEvent{
|
||||
want: LogEvent{
|
||||
Message: "123",
|
||||
},
|
||||
},
|
||||
@@ -138,7 +138,7 @@ func Test_createEvent(t *testing.T) {
|
||||
args: args{
|
||||
message: "2020-05-13T18:55:37.772853839Z sample text with=equal sign",
|
||||
},
|
||||
want: &LogEvent{
|
||||
want: LogEvent{
|
||||
Message: "sample text with=equal sign",
|
||||
},
|
||||
},
|
||||
@@ -147,7 +147,7 @@ func Test_createEvent(t *testing.T) {
|
||||
args: args{
|
||||
message: "2020-05-13T18:55:37.772853839Z null",
|
||||
},
|
||||
want: &LogEvent{
|
||||
want: LogEvent{
|
||||
Message: "",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ func init() {
|
||||
SupportedLogLevels["unknown"] = struct{}{}
|
||||
}
|
||||
|
||||
func guessLogLevel(logEvent *LogEvent) string {
|
||||
func guessLogLevel(logEvent LogEvent) string {
|
||||
switch value := logEvent.Message.(type) {
|
||||
case string:
|
||||
value = stripANSI(value)
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestGuessLogLevel(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
name, _ := json.Marshal(test.input)
|
||||
t.Run(string(name), func(t *testing.T) {
|
||||
actual := guessLogLevel(&LogEvent{Message: test.input})
|
||||
actual := guessLogLevel(LogEvent{Message: test.input})
|
||||
if actual != test.expected {
|
||||
t.Errorf("Expected %s, got %s", test.expected, actual)
|
||||
}
|
||||
|
||||
@@ -176,14 +176,14 @@ type LogEvent struct {
|
||||
ContainerID string `json:"c,omitempty"`
|
||||
}
|
||||
|
||||
func (l *LogEvent) HasLevel() bool {
|
||||
func (l LogEvent) HasLevel() bool {
|
||||
return l.Level != "unknown"
|
||||
}
|
||||
|
||||
func (l *LogEvent) IsCloseToTime(other *LogEvent) bool {
|
||||
func (l LogEvent) IsCloseToTime(other LogEvent) bool {
|
||||
return math.Abs(float64(l.Timestamp-other.Timestamp)) < 10
|
||||
}
|
||||
|
||||
func (l *LogEvent) MessageId() int64 {
|
||||
func (l LogEvent) MessageId() int64 {
|
||||
return l.Timestamp
|
||||
}
|
||||
|
||||
@@ -75,10 +75,10 @@ func UpdateFromReader(user auth.User, reader io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return Save(user, existingProfile)
|
||||
return save(user, existingProfile)
|
||||
}
|
||||
|
||||
func Save(user auth.User, profile Profile) error {
|
||||
func save(user auth.User, profile Profile) error {
|
||||
path := filepath.Join(dataPath, user.Username)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(path, 0755); err != nil {
|
||||
|
||||
@@ -22,6 +22,7 @@ type Args struct {
|
||||
AuthHeaderName string `arg:"--auth-header-name,env:DOZZLE_AUTH_HEADER_NAME" default:"Remote-Name" help:"sets the HTTP Header to use for name in Forward Proxy configuration."`
|
||||
AuthHeaderFilter string `arg:"--auth-header-filter,env:DOZZLE_AUTH_HEADER_FILTER" default:"Remote-Filter" help:"sets the HTTP Header to use for filtering in Forward Proxy configuration."`
|
||||
AuthHeaderRoles string `arg:"--auth-header-roles,env:DOZZLE_AUTH_HEADER_ROLES" default:"Remote-Roles" help:"sets the HTTP Header to use for roles in Forward Proxy configuration."`
|
||||
AuthLogoutUrl string `arg:"--auth-logout-url,env:DOZZLE_AUTH_LOGOUT_URL" help:"sets the Logout URL used with Forward Proxy."`
|
||||
EnableActions bool `arg:"--enable-actions,env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."`
|
||||
EnableShell bool `arg:"--enable-shell,env:DOZZLE_ENABLE_SHELL" default:"false" help:"enables shell access to containers from the web interface."`
|
||||
DisableAvatars bool `arg:"--disable-avatars,env:DOZZLE_DISABLE_AVATARS" default:"false" help:"disables avatars for authenticated users."`
|
||||
|
||||
@@ -32,11 +32,11 @@ func (a *agentService) RawLogs(ctx context.Context, container container.Containe
|
||||
return a.client.StreamRawBytes(ctx, container.ID, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (a *agentService) LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
func (a *agentService) LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan container.LogEvent, error) {
|
||||
return a.client.LogsBetweenDates(ctx, container.ID, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (a *agentService) StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
func (a *agentService) StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- container.LogEvent) error {
|
||||
return a.client.StreamContainerLogs(ctx, container.ID, from, stdTypes, events)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type ClientService interface {
|
||||
ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error)
|
||||
Host(ctx context.Context) (container.Host, error)
|
||||
ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error
|
||||
LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error)
|
||||
LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan container.LogEvent, error)
|
||||
RawLogs(context.Context, container.Container, time.Time, time.Time, container.StdType) (io.ReadCloser, error)
|
||||
|
||||
// Subscriptions
|
||||
@@ -24,7 +24,7 @@ type ClientService interface {
|
||||
SubscribeContainersStarted(context.Context, chan<- container.Container)
|
||||
|
||||
// Blocking streaming functions that should be used in a goroutine
|
||||
StreamLogs(context.Context, container.Container, time.Time, container.StdType, chan<- *container.LogEvent) error
|
||||
StreamLogs(context.Context, container.Container, time.Time, container.StdType, chan<- container.LogEvent) error
|
||||
|
||||
// Terminal
|
||||
Attach(context.Context, container.Container, io.Reader, io.Writer) error
|
||||
|
||||
@@ -24,11 +24,11 @@ func (c *ContainerService) RawLogs(ctx context.Context, from time.Time, to time.
|
||||
return c.clientService.RawLogs(ctx, c.Container, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (c *ContainerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
func (c *ContainerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (<-chan container.LogEvent, error) {
|
||||
return c.clientService.LogsBetweenDates(ctx, c.Container, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (c *ContainerService) StreamLogs(ctx context.Context, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
func (c *ContainerService) StreamLogs(ctx context.Context, from time.Time, stdTypes container.StdType, events chan<- container.LogEvent) error {
|
||||
return c.clientService.StreamLogs(ctx, c.Container, from, stdTypes, events)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ func (d *DockerClientService) RawLogs(ctx context.Context, container container.C
|
||||
|
||||
}
|
||||
|
||||
func (d *DockerClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
func (d *DockerClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan container.LogEvent, error) {
|
||||
reader, err := d.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -63,7 +63,7 @@ func (d *DockerClientService) LogsBetweenDates(ctx context.Context, c container.
|
||||
return g.Events, nil
|
||||
}
|
||||
|
||||
func (d *DockerClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
func (d *DockerClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- container.LogEvent) error {
|
||||
reader, err := d.client.ContainerLogs(ctx, c.ID, from, stdTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -45,7 +45,7 @@ func (k *K8sClientService) ContainerAction(ctx context.Context, container contai
|
||||
return k.client.ContainerActions(ctx, action, container.ID)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
func (k *K8sClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan container.LogEvent, error) {
|
||||
reader, err := k.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -60,7 +60,7 @@ func (k *K8sClientService) RawLogs(ctx context.Context, container container.Cont
|
||||
return k.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
func (k *K8sClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- container.LogEvent) error {
|
||||
reader, err := k.client.ContainerLogs(ctx, c.ID, from, stdTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -16,7 +16,7 @@ func ParseRegex(search string) (*regexp.Regexp, error) {
|
||||
return CreateRegex(search, search == strings.ToLower(search))
|
||||
}
|
||||
|
||||
func Search(re *regexp.Regexp, logEvent *container.LogEvent) bool {
|
||||
func Search(re *regexp.Regexp, logEvent container.LogEvent) bool {
|
||||
matcher := NewPatternMatcher(re, MarkerStart, MarkerEnd)
|
||||
return matcher.MarkInLogEvent(logEvent)
|
||||
return matcher.MarkInLogEvent(&logEvent)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
@@ -54,6 +55,10 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
config["enableDownload"] = true
|
||||
}
|
||||
|
||||
if h.config.Authorization.Provider == FORWARD_PROXY && strings.TrimSpace(h.config.Authorization.LogoutUrl) != "" {
|
||||
config["logoutUrl"] = strings.TrimSpace(h.config.Authorization.LogoutUrl)
|
||||
}
|
||||
|
||||
config["authProvider"] = h.config.Authorization.Provider
|
||||
config["version"] = h.config.Version
|
||||
config["hostname"] = h.config.Hostname
|
||||
|
||||
@@ -82,7 +82,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
minimum := 0
|
||||
buffer := utils.NewRingBuffer[*container.LogEvent](500)
|
||||
buffer := utils.NewRingBuffer[container.LogEvent](500)
|
||||
if r.URL.Query().Has("min") {
|
||||
minimum, err = strconv.Atoi(r.URL.Query().Get("min"))
|
||||
if err != nil {
|
||||
@@ -94,7 +94,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, errors.New("minimum must be between 0 and buffer size").Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
buffer = utils.NewRingBuffer[*container.LogEvent](minimum)
|
||||
buffer = utils.NewRingBuffer[container.LogEvent](minimum)
|
||||
}
|
||||
|
||||
maxStart := math.MaxInt
|
||||
@@ -177,7 +177,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
break
|
||||
}
|
||||
|
||||
support_web.EscapeHTMLValues(event)
|
||||
support_web.EscapeHTMLValues(&event)
|
||||
buffer.Push(event)
|
||||
}
|
||||
}
|
||||
@@ -294,9 +294,9 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
||||
|
||||
absoluteTime := time.Time{}
|
||||
var regex *regexp.Regexp
|
||||
liveLogs := make(chan *container.LogEvent)
|
||||
liveLogs := make(chan container.LogEvent)
|
||||
events := make(chan *container.ContainerEvent, 1)
|
||||
backfill := make(chan []*container.LogEvent)
|
||||
backfill := make(chan []container.LogEvent)
|
||||
|
||||
levels := make(map[string]struct{})
|
||||
for _, level := range r.URL.Query()["levels"] {
|
||||
@@ -318,7 +318,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
||||
delta := -10 * time.Second
|
||||
to := absoluteTime
|
||||
for minimum > 0 {
|
||||
events := make([]*container.LogEvent, 0)
|
||||
events := make([]container.LogEvent, 0)
|
||||
stillRunning := false
|
||||
for _, container := range existingContainers {
|
||||
containerService, err := h.hostService.FindContainer(container.Host, container.ID, userLabels)
|
||||
@@ -418,7 +418,7 @@ loop:
|
||||
continue
|
||||
}
|
||||
|
||||
support_web.EscapeHTMLValues(logEvent)
|
||||
support_web.EscapeHTMLValues(&logEvent)
|
||||
sseWriter.Message(logEvent)
|
||||
case c := <-newContainers:
|
||||
if _, err := h.hostService.FindContainer(c.Host, c.ID, userLabels); err == nil {
|
||||
@@ -433,8 +433,8 @@ loop:
|
||||
}
|
||||
|
||||
case backfillEvents := <-backfill:
|
||||
for _, event := range backfillEvents {
|
||||
support_web.EscapeHTMLValues(event)
|
||||
for i := range backfillEvents {
|
||||
support_web.EscapeHTMLValues(&backfillEvents[i])
|
||||
}
|
||||
if err := sseWriter.Event("logs-backfill", backfillEvents); err != nil {
|
||||
log.Error().Err(err).Msg("error encoding container event")
|
||||
|
||||
@@ -52,6 +52,7 @@ type Authorization struct {
|
||||
Provider AuthProvider
|
||||
Authorizer Authorizer
|
||||
TTL time.Duration
|
||||
LogoutUrl string
|
||||
}
|
||||
|
||||
type Authorizer interface {
|
||||
|
||||
@@ -208,6 +208,7 @@ func createServer(args cli.Args, hostService web.HostService) *http.Server {
|
||||
Provider: provider,
|
||||
Authorizer: authorizer,
|
||||
TTL: authTTL,
|
||||
LogoutUrl: args.AuthLogoutUrl,
|
||||
},
|
||||
EnableActions: args.EnableActions,
|
||||
EnableShell: args.EnableShell,
|
||||
|
||||
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "8.14.2",
|
||||
"version": "8.14.4",
|
||||
"description": "Realtime log viewer for docker containers.",
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/amir20/dozzle/issues"
|
||||
},
|
||||
"packageManager": "pnpm@10.17.1",
|
||||
"packageManager": "pnpm@10.18.2",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -30,17 +30,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@duckdb/duckdb-wasm": "1.30.0",
|
||||
"@iconify-json/carbon": "^1.2.13",
|
||||
"@iconify-json/carbon": "^1.2.14",
|
||||
"@iconify-json/cil": "^1.2.3",
|
||||
"@iconify-json/ic": "^1.2.4",
|
||||
"@iconify-json/material-symbols": "^1.2.39",
|
||||
"@iconify-json/material-symbols": "^1.2.41",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/mdi-light": "^1.2.2",
|
||||
"@iconify-json/octicon": "^1.2.14",
|
||||
"@iconify-json/octicon": "^1.2.16",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "4.1.13",
|
||||
"@tailwindcss/vite": "4.1.14",
|
||||
"@vueuse/components": "^13.9.0",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"@vueuse/integrations": "^13.9.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"daisyui": "5.1.23",
|
||||
"daisyui": "5.2.3",
|
||||
"entities": "^7.0.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
@@ -62,13 +62,13 @@
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "^4.0.4",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tailwindcss": "4.1.13",
|
||||
"tailwindcss": "4.1.14",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-icons": "^22.3.0",
|
||||
"unplugin-icons": "^22.4.2",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"unplugin-vue-macros": "^2.14.5",
|
||||
"unplugin-vue-router": "^0.15.0",
|
||||
"vite": "7.1.7",
|
||||
"vite": "7.1.9",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitepress": "1.6.4",
|
||||
@@ -77,12 +77,12 @@
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apache-arrow/esnext-esm": "^21.0.0",
|
||||
"@apache-arrow/esnext-esm": "^21.1.0",
|
||||
"@iconify-json/ion": "^1.2.6",
|
||||
"@iconify-json/material-symbols-light": "^1.2.39",
|
||||
"@iconify-json/ri": "^1.2.5",
|
||||
"@iconify-json/material-symbols-light": "^1.2.41",
|
||||
"@iconify-json/ri": "^1.2.6",
|
||||
"@pinia/testing": "^1.0.2",
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@playwright/test": "^1.56.0",
|
||||
"@types/d3-array": "^3.2.2",
|
||||
"@types/d3-ease": "^3.0.2",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
@@ -90,24 +90,24 @@
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/d3-transition": "^3.0.9",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/node": "^24.7.2",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vue/compiler-sfc": "^3.5.22",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"bumpp": "^10.2.3",
|
||||
"bumpp": "^10.3.1",
|
||||
"c8": "^10.1.3",
|
||||
"concurrently": "^9.2.1",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"lint-staged": "^16.2.1",
|
||||
"lint-staged": "^16.2.4",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-component-type-helpers": "3.0.8",
|
||||
"vue-tsc": "3.0.8"
|
||||
"vue-component-type-helpers": "3.1.1",
|
||||
"vue-tsc": "3.1.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css,ts,html,md}": [
|
||||
|
||||
Generated
+499
-516
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user