mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 18:11:57 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f393d0d3e6 |
@@ -1,30 +0,0 @@
|
||||
name: "\u2601\uFE0F Dozzle Cloud Bug Report"
|
||||
labels: ["bug", "cloud"]
|
||||
description: |
|
||||
Use this template for issues related to Dozzle Cloud.
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Dozzle version
|
||||
description: The version of Dozzle connected to Dozzle Cloud.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots or logs
|
||||
description: Drag screenshots or paste logs into the text input below.
|
||||
validations:
|
||||
required: false
|
||||
@@ -3,10 +3,6 @@ on:
|
||||
tags:
|
||||
- "v*"
|
||||
name: Test and Release
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
npm-test:
|
||||
name: JavaScript Tests
|
||||
@@ -18,12 +14,12 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -37,7 +33,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.26.0
|
||||
go-version: 1.25.5
|
||||
check-latest: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
@@ -60,12 +56,12 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -96,19 +92,16 @@ jobs:
|
||||
needs: [go-test, npm-test, int-test]
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.7.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.7.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -132,7 +125,7 @@ jobs:
|
||||
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
|
||||
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.19.0
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
@@ -146,8 +139,6 @@ jobs:
|
||||
needs: [buildx]
|
||||
name: Github Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -6,11 +6,6 @@ on:
|
||||
branches:
|
||||
- master
|
||||
name: Push container
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
buildx:
|
||||
name: Push branches and PRs
|
||||
@@ -20,12 +15,12 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.7.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.7.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -44,7 +39,7 @@ jobs:
|
||||
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
|
||||
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.19.0
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
cache: pnpm # or pnpm / yarn
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
@@ -6,10 +6,6 @@ on:
|
||||
branches:
|
||||
- master
|
||||
name: Test
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
@@ -21,12 +17,12 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -44,7 +40,7 @@ jobs:
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -58,7 +54,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26.0"
|
||||
go-version: "1.25.5"
|
||||
check-latest: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
@@ -81,7 +77,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26.0"
|
||||
go-version: "1.25.5"
|
||||
check-latest: true
|
||||
- name: Generate dependencies
|
||||
run: make fake_assets shared_key.pem shared_cert.pem
|
||||
@@ -100,12 +96,12 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.13.1
|
||||
node-version: 24.12.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -4,4 +4,3 @@ typed-router.d.ts
|
||||
docs/.vitepress/cache
|
||||
docs/.vitepress/dist
|
||||
dist
|
||||
assets/types/graphql.ts
|
||||
|
||||
@@ -31,6 +31,9 @@ The application supports multiple deployment modes: standalone server, Docker Sw
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Install Go tools (protobuf, air hot-reloader)
|
||||
make tools
|
||||
|
||||
# Generate certificates and protobuf files
|
||||
make generate
|
||||
```
|
||||
@@ -127,18 +130,8 @@ The Go backend is organized into these key packages:
|
||||
- Role-based authorization (`roles.go`)
|
||||
|
||||
- **`internal/container/`** - Container domain models and interfaces
|
||||
- `event_generator.go`: Log parsing and grouping logic (multi-line, JSON detection)
|
||||
|
||||
- **`internal/notification/`** - Alert and notification system
|
||||
- `manager.go`: Notification rule evaluation and dispatching
|
||||
- `log_listener.go`: Log pattern matching for alerts
|
||||
- `dispatcher/`: Notification channel implementations (email, webhook, etc.)
|
||||
|
||||
- **`graph/`** - GraphQL API layer
|
||||
- `schema.graphqls`: GraphQL schema definitions
|
||||
- `*.resolvers.go`: GraphQL resolver implementations
|
||||
|
||||
- **`main.go`** - Application entry point with mode switching (server/swarm/k8s/agent)
|
||||
- **`main.go`** - Application entry point with mode switching (server/swarm/k8s)
|
||||
|
||||
### Frontend (Vue 3)
|
||||
|
||||
@@ -168,23 +161,17 @@ The frontend uses file-based routing with these conventions:
|
||||
- `ContainerTable.vue`: Container table with historical stat visualization
|
||||
|
||||
- **`assets/stores/`** - Pinia stores (auto-imported)
|
||||
- `config.ts`: App configuration and feature flags (injected from backend HTML, frozen immutable)
|
||||
- `container.ts`: Container state management with EventSource streaming (`/api/events/stream`)
|
||||
- `config.ts`: App configuration and feature flags
|
||||
- `container.ts`: Container state management
|
||||
- `hosts.ts`: Multi-host state
|
||||
- `settings.ts`: User preferences (localStorage-backed via profileStorage)
|
||||
- `pinned.ts`: Pinned container logs for side-by-side viewing
|
||||
- `swarm.ts`, `k8s.ts`: Deployment mode-specific state
|
||||
- `announcements.ts`: Feature announcements
|
||||
- `settings.ts`: User preferences
|
||||
|
||||
- **`assets/composable/`** - Vue composables (auto-imported)
|
||||
- `eventStreams.ts`: SSE connection management with buffer-based flushing (250ms debounce)
|
||||
- `eventStreams.ts`: SSE connection management
|
||||
- `historicalLogs.ts`: Historical log fetching
|
||||
- `logContext.ts`: Log filtering and search context (provide/inject pattern)
|
||||
- `scrollContext.ts`: Scroll state management (paused, progress, currentDate)
|
||||
- `storage.ts`: LocalStorage abstractions with reactivity
|
||||
- `logContext.ts`: Log filtering and search context
|
||||
- `storage.ts`: LocalStorage abstractions
|
||||
- `visible.ts`: Log filtering by visible keys for complex logs
|
||||
- `containerActions.ts`: Container control operations
|
||||
- `duckdb.ts`: DuckDB WASM for SQL queries on logs
|
||||
|
||||
- **`assets/modules/`** - Vue plugins
|
||||
- `router.ts`: Vue Router configuration
|
||||
@@ -198,7 +185,6 @@ The frontend uses file-based routing with these conventions:
|
||||
3. **Stats**: Real-time CPU/memory stats streamed via SSE alongside events
|
||||
4. **Actions**: POST to `/api/hosts/{host}/containers/{id}/actions/{action}` (start/stop/restart)
|
||||
5. **Terminal**: WebSocket connections for container attach/exec at `/api/hosts/{host}/containers/{id}/attach`
|
||||
6. **GraphQL**: POST to `/api/graphql` for queries and mutations (container metadata, historical logs, notifications)
|
||||
|
||||
### Build System
|
||||
|
||||
@@ -220,8 +206,6 @@ The frontend uses file-based routing with these conventions:
|
||||
- `ComplexLogEntry`: Structured JSON logs (`JSONObject`)
|
||||
- `GroupedLogEntry`: Multi-line grouped logs (`string[]`)
|
||||
- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages
|
||||
- **Log Entry Factory Pattern**: Use `LogEntry.create(logEvent)` to instantiate the correct entry type based on `logEvent.t` field
|
||||
- **EventSource Buffering**: Log streams use buffer-based flushing (250ms debounce, 1000ms max) to batch UI updates
|
||||
- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)
|
||||
- `BarChart.vue`: Self-contained bar chart with responsive downsampling
|
||||
- Downsampling algorithm: Averages data into buckets based on available screen width
|
||||
@@ -233,13 +217,6 @@ The frontend uses file-based routing with these conventions:
|
||||
- Certificate generation is required (`make generate` creates shared_key.pem and shared_cert.pem)
|
||||
- Protocol buffer generation happens via `go generate` directive in `main.go`
|
||||
- Docker client uses API version negotiation for compatibility
|
||||
- **GraphQL API**: Uses gqlgen with schema in `graph/schema.graphqls`, generated code in `graph/generated.go`
|
||||
- Run `pnpm codegen` to regenerate GraphQL types
|
||||
- Resolvers follow-schema layout in `graph/*.resolvers.go`
|
||||
- **Service Layer Architecture**:
|
||||
- `ClientService` interface abstracts Docker/K8s/Agent backends
|
||||
- `MultiHostService` orchestrates multi-host operations
|
||||
- `ClientManager` implementations: `RetriableClientManager` (server mode), `SwarmClientManager` (swarm mode)
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -270,168 +247,7 @@ The frontend uses file-based routing with these conventions:
|
||||
|
||||
### Deployment Modes
|
||||
|
||||
- **Server mode** (default): Single or multi-host Docker monitoring
|
||||
- Uses `RetriableClientManager` with local + remote agent clients
|
||||
- **Server mode**: Single or multi-host Docker monitoring
|
||||
- **Swarm mode**: Automatic discovery of Swarm nodes via Docker API
|
||||
- Creates gRPC agent server on each node (port 7007)
|
||||
- Uses `SwarmClientManager` for node discovery
|
||||
- **K8s mode**: Pod log monitoring in Kubernetes cluster
|
||||
- Implements `container.Client` interface via Kubernetes API
|
||||
- **Agent mode**: Lightweight gRPC agent for remote log collection
|
||||
- Run with `dozzle agent` or `pnpm run agent:dev`
|
||||
- Listens on port 7007 with TLS certificate authentication
|
||||
|
||||
## Key Architectural Patterns
|
||||
|
||||
### Backend Abstraction Layers
|
||||
|
||||
The backend follows a clean layered architecture:
|
||||
|
||||
```
|
||||
HTTP Handlers (internal/web)
|
||||
↓
|
||||
HostService Interface (MultiHostService)
|
||||
↓
|
||||
ClientService Interface (per host)
|
||||
↓
|
||||
container.Client Interface
|
||||
↓
|
||||
Implementation (DockerClient, K8sClient, AgentClient)
|
||||
```
|
||||
|
||||
**When adding new container operations:**
|
||||
|
||||
1. Define method in `container.Client` interface (`internal/container/client.go`)
|
||||
2. Implement in `internal/docker/client.go` (and `internal/k8s/client.go` if applicable)
|
||||
3. Add wrapper method in `ClientService` interface (`internal/support/container/service.go`)
|
||||
4. Add HTTP handler in `internal/web/` with appropriate route
|
||||
|
||||
### Frontend Data Flow
|
||||
|
||||
**Real-time Log Viewing:**
|
||||
|
||||
1. User navigates to `/container/{id}` route
|
||||
2. Page component calls `useContainerStream(container)` composable
|
||||
3. Composable creates EventSource connection to `/api/hosts/{host}/containers/{id}/logs/stream`
|
||||
4. Backend streams `LogEvent` objects via SSE
|
||||
5. Frontend buffers events (250ms debounce, max 1000ms)
|
||||
6. Batched buffer flushes update reactive `messages` array
|
||||
7. `LogViewer.vue` renders using appropriate component (`SimpleLogItem`, `ComplexLogItem`, `GroupedLogItem`)
|
||||
8. When messages exceed `maxLogs` (400), oldest entries replaced or marked as `SkippedLogsEntry`
|
||||
|
||||
**Stats Streaming:**
|
||||
|
||||
1. `container.ts` store connects to `/api/events/stream` on app init
|
||||
2. Backend multiplexes container events and stats into single SSE stream
|
||||
3. `container-stat` events update `Container._stat` and append to `_statsHistory`
|
||||
4. EMA calculation provides smoothed `movingAverageStat` (alpha=0.2)
|
||||
5. `ContainerTable.vue` displays mini bar charts using `statsHistory` with downsampling
|
||||
|
||||
### Protocol Buffer Flow (Agent Mode)
|
||||
|
||||
1. Main server creates `agent.NewClient(endpoint, certs)` for each remote host
|
||||
2. AgentClient implements `container.Client` interface
|
||||
3. Method calls translate to gRPC requests defined in `protos/rpc.proto`
|
||||
4. Remote agent receives gRPC call, delegates to local `DockerClient`
|
||||
5. Streaming RPCs (logs, stats, events) use bidirectional channels
|
||||
6. Responses converted back to domain models via `FromProto()` methods
|
||||
|
||||
### Log Parsing Pipeline
|
||||
|
||||
1. Docker API returns multiplexed stream (8-byte headers + payload)
|
||||
2. `log_reader.go` parses headers, extracts stdout/stderr type
|
||||
3. `event_generator.go` receives raw log lines
|
||||
4. Detection logic identifies:
|
||||
- JSON structure → `ComplexLogEntry`
|
||||
- Multi-line patterns (stack traces) → `GroupedLogEntry`
|
||||
- Single lines → `SimpleLogEntry`
|
||||
5. Log level extraction via regex patterns
|
||||
6. `LogEvent` serialized to JSON and sent via SSE
|
||||
7. Frontend deserializes and renders with appropriate component
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Adding a New HTTP Route
|
||||
|
||||
1. Define route in `internal/web/routes.go` using chi router:
|
||||
```go
|
||||
r.Get("/api/custom-endpoint", h.customHandler)
|
||||
```
|
||||
2. Implement handler method in appropriate file (e.g., `actions.go`, `logs.go`)
|
||||
3. Use `hostService` to find container/host via `FindContainer()` or `FindHost()`
|
||||
4. Return JSON response or establish SSE/WebSocket stream
|
||||
|
||||
### Adding a New Log View Type
|
||||
|
||||
1. Create route file in `assets/pages/` (e.g., `custom/[id].vue`)
|
||||
2. Create composable in `assets/composable/eventStreams.ts` (e.g., `useCustomStream()`)
|
||||
3. Composable should:
|
||||
- Build API URL with appropriate filters
|
||||
- Create EventSource connection
|
||||
- Handle buffering and message batching
|
||||
- Return reactive `messages` array and control methods
|
||||
4. Use `LogViewer.vue` component to render messages
|
||||
5. Add backend API endpoint if needed (see above)
|
||||
|
||||
### Adding a New GraphQL Query/Mutation
|
||||
|
||||
1. Define in `graph/schema.graphqls`
|
||||
2. Run `pnpm codegen` to regenerate types
|
||||
3. Implement resolver in `graph/schema.resolvers.go`
|
||||
4. Use `hostService` from resolver context to access backend services
|
||||
5. Frontend calls via urql client (auto-imported via `@urql/vue`)
|
||||
|
||||
### Adding Container Stats/Metrics
|
||||
|
||||
1. Add field to `Stat` type in `internal/container/types.go`
|
||||
2. Update `stats_collector.go` to extract metric from Docker API response
|
||||
3. Add calculation logic in `docker/calculation.go` if needed
|
||||
4. Ensure protobuf definition includes field in `protos/rpc.proto`
|
||||
5. Frontend automatically receives updates via existing SSE stream
|
||||
6. Update `Container` model in `assets/models/Container.ts` if UI needs access
|
||||
|
||||
### Working with Notifications/Alerts
|
||||
|
||||
**Backend** (`internal/notification/`):
|
||||
|
||||
- `manager.go`: Rule evaluation engine, manages alert state
|
||||
- `log_listener.go`: Subscribes to container log streams, evaluates rules against incoming logs
|
||||
- `types.go`: Alert rule definitions (log pattern matching, thresholds)
|
||||
- `dispatcher/`: Notification channel implementations
|
||||
|
||||
**Frontend** (`assets/pages/notifications.vue`, `assets/components/Notification/`):
|
||||
|
||||
- `AlertForm.vue`, `DestinationForm.vue`: UI for creating rules
|
||||
- Rules stored via GraphQL mutations
|
||||
- Alert state displayed in notification cards
|
||||
|
||||
**Adding a new notification channel:**
|
||||
|
||||
1. Implement dispatcher interface in `internal/notification/dispatcher/`
|
||||
2. Register in `manager.go` dispatcher factory
|
||||
3. Add UI form in `assets/components/Notification/DestinationForm.vue`
|
||||
4. Add GraphQL schema fields if needed
|
||||
|
||||
## Common Development Patterns
|
||||
|
||||
### Testing
|
||||
|
||||
- Always run Go tests with race detector: `go test -race`
|
||||
- Frontend tests require `TZ=UTC` for timestamp consistency
|
||||
- Integration tests use Playwright with `make int` (runs docker-compose setup)
|
||||
- Use `testify/assert` for Go test assertions
|
||||
|
||||
### Hot Reload Development
|
||||
|
||||
- `make dev` runs both backend (air) and frontend (vite) with hot reload
|
||||
- `DEV=true` disables embedded asset serving
|
||||
- `LIVE_FS=true` serves assets from filesystem instead of embedded
|
||||
- Backend changes trigger air restart automatically
|
||||
- Frontend changes trigger vite HMR
|
||||
|
||||
### Debugging
|
||||
|
||||
- Backend logs: Set `--level debug` flag or `DOZZLE_LEVEL=debug` env var
|
||||
- Frontend: Vue DevTools browser extension
|
||||
- GraphQL: Use GraphQL Playground at `/api/graphql` (when enabled)
|
||||
- SSE streams: Browser DevTools Network tab shows EventSource connections
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:25.6.0-alpine AS node
|
||||
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine AS node
|
||||
|
||||
RUN npm install -g --force corepack && corepack enable
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -22,10 +22,10 @@ COPY public ./public
|
||||
# Build assets
|
||||
RUN pnpm build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||
|
||||
# install gRPC dependencies
|
||||
RUN apk add --no-cache ca-certificates protoc protobuf-dev \
|
||||
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
|
||||
&& mkdir /dozzle \
|
||||
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
||||
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
@@ -50,8 +50,8 @@ COPY --from=node /build/dist ./dist
|
||||
ARG TAG=dev
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
# Generate protos and graphql
|
||||
RUN go generate ./...
|
||||
# Generate protos
|
||||
RUN go generate
|
||||
|
||||
# Build binary
|
||||
RUN GOEXPERIMENT=jsonv2 GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=$TAG" -o dozzle
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
PROTO_DIR := protos
|
||||
GEN_DIR := internal/agent/pb
|
||||
PROTO_FILES := $(wildcard $(PROTO_DIR)/*.proto)
|
||||
GEN_FILES := $(patsubst $(PROTO_DIR)/%.proto,$(GEN_DIR)/%.pb.go,$(PROTO_FILES))
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@@ -27,11 +30,9 @@ build: dist generate
|
||||
|
||||
.PHONY: docker
|
||||
docker: shared_key.pem shared_cert.pem
|
||||
@docker build --build-arg TAG=local -t amir20/dozzle:local .
|
||||
@docker build --build-arg TAG=local -t amir20/dozzle .
|
||||
|
||||
.PHONY: generate
|
||||
generate: shared_key.pem shared_cert.pem
|
||||
@go generate ./...
|
||||
generate: shared_key.pem shared_cert.pem $(GEN_FILES)
|
||||
|
||||
.PHONY: dev
|
||||
dev: generate fake_assets
|
||||
@@ -49,24 +50,21 @@ shared_cert.pem: shared_key.pem
|
||||
@openssl x509 -req -in shared_request.csr -signkey shared_key.pem -out shared_cert.pem -days 1825
|
||||
@rm shared_request.csr
|
||||
|
||||
$(GEN_DIR)/%.pb.go: $(PROTO_DIR)/%.proto
|
||||
@go generate
|
||||
|
||||
.PHONY: push
|
||||
push: docker
|
||||
@docker tag amir20/dozzle:latest amir20/dozzle:local-test
|
||||
@docker push amir20/dozzle:local-test
|
||||
|
||||
tools:
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
go install github.com/air-verse/air@latest
|
||||
|
||||
run: docker
|
||||
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:local
|
||||
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest
|
||||
|
||||
preview: build
|
||||
pnpm preview
|
||||
|
||||
.PHONY: agent-reload
|
||||
agent-reload: docker
|
||||
@VM_NAME=$${VM_NAME:-dozzle-agent}; \
|
||||
echo "📦 Loading image into VM $$VM_NAME..."; \
|
||||
docker save amir20/dozzle:local | orb exec -m $$VM_NAME docker load; \
|
||||
echo "🔄 Recreating agent..."; \
|
||||
orb exec -m $$VM_NAME docker stop dozzle-agent || true; \
|
||||
orb exec -m $$VM_NAME docker rm dozzle-agent || true; \
|
||||
orb exec -m $$VM_NAME docker run -d --name dozzle-agent -p 7007:7007 -v /var/run/docker.sock:/var/run/docker.sock -v ~/dozzle-certs:/certs -v ~/dozzle-data:/data -e DOZZLE_LEVEL=debug amir20/dozzle:local agent --cert /certs/shared_cert.pem --key /certs/shared_key.pem; \
|
||||
echo "✅ Agent reloaded"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# Dozzle - [dozzle.dev](https://dozzle.dev/)
|
||||
|
||||
Dozzle is a lightweight, web-based application for monitoring Docker logs in real time. It doesn't store any log files—it's designed purely for live log viewing.
|
||||
Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesn’t store any log files. It is for live monitoring of your container logs only.
|
||||
|
||||
https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
|
||||
|
||||
@@ -14,22 +14,22 @@ https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> If you like Dozzle, check out [`dtop`](https://github.com/amir20/dtop), a top-like application for monitoring Docker containers. It integrates with Dozzle to link directly to container logs.
|
||||
> If you like Dozzle, check out [`dtop`](https://github.com/amir20/dtop) which is a top like application for monitoring Docker containers. It integrates with Dozzle to allow for linking directly to container logs.
|
||||
|
||||
## Features
|
||||
|
||||
- Intelligent fuzzy search for container names
|
||||
- Search logs using regex
|
||||
- Search logs using [SQL queries](https://dozzle.dev/guide/sql-engine)
|
||||
- Small memory footprint
|
||||
- Intelligent fuzzy search for container names 🤖
|
||||
- Search logs using regex 🔦
|
||||
- Search logs using [SQL queries](https://dozzle.dev/guide/sql-engine) 📊
|
||||
- Small memory footprint 🏎
|
||||
- Split screen for viewing multiple logs
|
||||
- Live stats with memory and CPU usage
|
||||
- Multi-user [authentication](https://dozzle.dev/guide/authentication) with support for forward proxy authorization
|
||||
- [Swarm mode](https://dozzle.dev/guide/swarm-mode) support
|
||||
- [Agent mode](https://dozzle.dev/guide/agent) for monitoring multiple Docker hosts
|
||||
- Dark mode
|
||||
- Multi-user [authentication](https://dozzle.dev/guide/authentication) with support for proxy forward authorization 🚨
|
||||
- [Swarm](https://dozzle.dev/guide/swarm-mode) mode support 🐳
|
||||
- [Agent](https://dozzle.dev/guide/agent) mode for monitoring multiple Docker hosts 🕵️♂️
|
||||
- Dark mode 🌙
|
||||
|
||||
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 better 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.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -39,13 +39,13 @@ Dozzle is a small container (7 MB compressed). Pull the latest release with:
|
||||
|
||||
### Running Dozzle
|
||||
|
||||
The simplest way to use Dozzle is to run the Docker container. Mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
|
||||
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 8080:8080 amir20/dozzle:latest
|
||||
|
||||
Dozzle will be available at [http://localhost:8080/](http://localhost:8080/).
|
||||
|
||||
Here is a Docker Compose example:
|
||||
Here is the Docker Compose file:
|
||||
|
||||
services:
|
||||
dozzle:
|
||||
@@ -56,11 +56,11 @@ Here is a Docker Compose example:
|
||||
ports:
|
||||
- 8080:8080
|
||||
|
||||
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 the documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
|
||||
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).
|
||||
|
||||
## Swarm Mode
|
||||
|
||||
Dozzle works with Docker Swarm. You can run Dozzle as a global service:
|
||||
Dozzle works with Docker Swarm mode. You can run Dozzle as a global service with:
|
||||
|
||||
$ docker service create --name dozzle --env DOZZLE_MODE=swarm --mode global --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
|
||||
|
||||
@@ -68,7 +68,7 @@ See the [Swarm Mode](https://dozzle.dev/guide/swarm-mode) documentation for more
|
||||
|
||||
## Agent Mode
|
||||
|
||||
Dozzle can monitor multiple Docker hosts. Run Dozzle in agent mode with:
|
||||
Dozzle can be used to monitor multiple Docker hosts. You can run Dozzle in agent mode with:
|
||||
|
||||
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:latest agent
|
||||
|
||||
@@ -76,19 +76,19 @@ See the [Agent Mode](https://dozzle.dev/guide/agent) documentation for more deta
|
||||
|
||||
## Technical Details
|
||||
|
||||
Dozzle uses automatic API negotiation, which works with most Docker configurations. Dozzle also works with [Colima](https://github.com/abiosoft/colima) and [Podman](https://podman.io/).
|
||||
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/).
|
||||
|
||||
### Installation on Podman
|
||||
### Installation on podman
|
||||
|
||||
By default, Podman doesn't have a background process, but you can enable the remote socket for Dozzle to work.
|
||||
By default Podman doesn't have a background process but you can enable this for Dozzle to work.
|
||||
|
||||
First, verify if your Podman installation has the remote socket enabled:
|
||||
Verify first if your podman installation has enabled remote socket:
|
||||
|
||||
```
|
||||
podman info
|
||||
```
|
||||
|
||||
If you see output like this under the remote socket key, it's already enabled:
|
||||
When you get under the key remote socket output like this, its already enabled:
|
||||
|
||||
```
|
||||
remoteSocket:
|
||||
@@ -96,40 +96,40 @@ If you see output like this under the remote socket key, it's already enabled:
|
||||
path: /run/user/1000/podman/podman.sock
|
||||
```
|
||||
|
||||
If it's not enabled, follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
|
||||
If it's not enabled please follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
|
||||
|
||||
Once the Podman remote socket is enabled, you can run Dozzle:
|
||||
Once you have the podman remote socket you can run Dozzle on podman.
|
||||
|
||||
```
|
||||
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 docker.io/amir20/dozzle:latest
|
||||
```
|
||||
|
||||
Additionally, you need to create a fake engine-id to prevent `host not found` errors. Podman doesn't generate an engine-id like Docker does, due to its daemonless architecture.
|
||||
Additionally you have to create a fake engine-id to prevent `host not found` errors. Podman doesn't generate an engine-id like Docker by itself due to its daemonless architecture.
|
||||
|
||||
Create a file named `engine-id` under `/var/lib/docker`. On a system with Podman, you'll need to create the folder path as well. Place a UUID inside the file, for example using `uuidgen > engine-id`. The file should contain an identifier like: `b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9`.
|
||||
Under `/var/lib/docker` create a file named `engine-id`. On a system with Podman you will have to create the folder path as well. Inside the file place the UUID, for instance using `uuidgen > engine-id`. After that the file should have an identifier that looks like this: `b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9`.
|
||||
|
||||
For more details, see [Podman Info](docs/guide/podman.md) or the [FAQ](docs/guide/faq.md#i-am-seeing-host-not-found-error-in-the-logs-how-do-i-fix-it).
|
||||
For more details check [Podman Infos](docs/guide/podman.md) or the [FAQ](docs/guide/faq.md#i-am-seeing-host-not-found-error-in-the-logs-how-do-i-fix-it)
|
||||
|
||||
## Security
|
||||
|
||||
Dozzle supports file-based authentication and forward proxy authentication with tools like [Authelia](https://www.authelia.com/). See the documentation at https://dozzle.dev/guide/authentication.
|
||||
Dozzle supports file based authentication and forward proxy like [Authelia](https://www.authelia.com/). These are documented at https://dozzle.dev/guide/authentication.
|
||||
|
||||
## Analytics
|
||||
## Analytics collected
|
||||
|
||||
Dozzle collects anonymous user configurations using Google Analytics. Why? Dozzle is an open source project with no funding, so there's no time for formal user studies. Analytics help prioritize features and fixes based on how people use Dozzle. This data is completely public and can be viewed live on the [Data Studio dashboard](https://datastudio.google.com/s/naeIu0MiWsY).
|
||||
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).
|
||||
|
||||
To disable analytics, use the `--no-analytics` flag.
|
||||
If you do not want to be tracked at all, see the `--no-analytics` flag below.
|
||||
|
||||
## Environment Variables and Configuration
|
||||
## Environment variables and configuration
|
||||
|
||||
Dozzle follows the [12-factor](https://12factor.net/) model. Configuration can be done via CLI flags or environment variables. See the documentation at [dozzle.dev/guide/supported-env-vars](https://dozzle.dev/guide/supported-env-vars) for more details.
|
||||
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. See documentation at [https://dozzle.dev/guide/supported-env-vars](https://dozzle.dev/guide/supported-env-vars) for more details.
|
||||
|
||||
## Support
|
||||
|
||||
There are many ways to support Dozzle:
|
||||
There are many ways you can support Dozzle:
|
||||
|
||||
- Use it! Write about it! Star it! If you love Dozzle, drop me a line and tell me what you love.
|
||||
- Blog about Dozzle to spread the word. If you're good at writing, send PRs to improve the documentation at [dozzle.dev](https://dozzle.dev/).
|
||||
- Blog about Dozzle to spread the word. If you are good at writing send PRs to improve the documentation at [dozzle.dev](https://dozzle.dev/)
|
||||
- Sponsor my work at https://www.buymeacoffee.com/amirraminfar
|
||||
|
||||
<a href="https://www.buymeacoffee.com/amirraminfar" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
@@ -142,9 +142,8 @@ There are many ways to support Dozzle:
|
||||
|
||||
To build and test locally:
|
||||
|
||||
1. Install [Node.js](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
|
||||
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
|
||||
2. Install [Go](https://go.dev/doc/install).
|
||||
3. Install [protoc](https://grpc.io/docs/protoc-installation/).
|
||||
4. Install Go tools with `go install tool`.
|
||||
5. Install Node modules with `pnpm install`.
|
||||
6. Run `make dev` to start a development server with hot reload.
|
||||
3. Install tools with `make tools`.
|
||||
4. Install node modules `pnpm install`.
|
||||
5. Run `make dev` to start a development server with hot reload.
|
||||
|
||||
Vendored
+6
-22
@@ -27,19 +27,15 @@ declare global {
|
||||
const controlledComputed: typeof import('@vueuse/core').controlledComputed
|
||||
const controlledRef: typeof import('@vueuse/core').controlledRef
|
||||
const createApp: typeof import('vue').createApp
|
||||
const createContainerHints: typeof import('./composable/exprEditor').createContainerHints
|
||||
const createDrawer: typeof import('./composable/drawer').createDrawer
|
||||
const createEventHook: typeof import('@vueuse/core').createEventHook
|
||||
const createExprEditor: typeof import('./composable/exprEditor').createExprEditor
|
||||
const createGlobalState: typeof import('@vueuse/core').createGlobalState
|
||||
const createInjectionState: typeof import('@vueuse/core').createInjectionState
|
||||
const createLogHints: typeof import('./composable/exprEditor').createLogHints
|
||||
const createPinia: typeof import('pinia').createPinia
|
||||
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
|
||||
const createRef: typeof import('@vueuse/core').createRef
|
||||
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
|
||||
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
|
||||
const createTemplateEditor: typeof import('./composable/templateEditor').createTemplateEditor
|
||||
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
|
||||
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
|
||||
const customRef: typeof import('vue').customRef
|
||||
@@ -82,6 +78,7 @@ declare global {
|
||||
const locale: typeof import('./stores/settings').locale
|
||||
const loggingContextKey: typeof import('./composable/logContext').loggingContextKey
|
||||
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
|
||||
const manualResetRef: typeof import('@vueuse/core')['manualResetRef']
|
||||
const mapActions: typeof import('pinia').mapActions
|
||||
const mapGetters: typeof import('pinia').mapGetters
|
||||
const mapState: typeof import('pinia').mapState
|
||||
@@ -134,6 +131,7 @@ declare global {
|
||||
const refWithControl: typeof import('@vueuse/core').refWithControl
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const resolveRef: typeof import('@vueuse/core').resolveRef
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const scrollContextKey: typeof import('./composable/scrollContext').scrollContextKey
|
||||
const search: typeof import('./stores/settings').search
|
||||
const sessionHost: typeof import('./composable/storage').sessionHost
|
||||
@@ -208,7 +206,6 @@ declare global {
|
||||
const useCountdown: typeof import('@vueuse/core').useCountdown
|
||||
const useCounter: typeof import('@vueuse/core').useCounter
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssSupports: typeof import('@vueuse/core').useCssSupports
|
||||
const useCssVar: typeof import('@vueuse/core').useCssVar
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
|
||||
@@ -265,7 +262,6 @@ declare global {
|
||||
const useK8sStore: typeof import('./stores/k8s').useK8sStore
|
||||
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
|
||||
const useLastChanged: typeof import('@vueuse/core').useLastChanged
|
||||
const useLink: typeof import('vue-router/auto').useLink
|
||||
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
|
||||
const useLoggingContext: typeof import('./composable/logContext').useLoggingContext
|
||||
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
|
||||
@@ -309,8 +305,8 @@ declare global {
|
||||
const useRafFn: typeof import('@vueuse/core').useRafFn
|
||||
const useRefHistory: typeof import('@vueuse/core').useRefHistory
|
||||
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver
|
||||
const useRoute: typeof import('vue-router/auto').useRoute
|
||||
const useRouter: typeof import('vue-router/auto').useRouter
|
||||
const useRoute: typeof import('vue-router').useRoute
|
||||
const useRouter: typeof import('vue-router').useRouter
|
||||
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
|
||||
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
|
||||
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
|
||||
@@ -401,12 +397,6 @@ declare global {
|
||||
export type { LogStreamSource } from './composable/eventStreams'
|
||||
import('./composable/eventStreams')
|
||||
// @ts-ignore
|
||||
export type { ExprEditorOptions } from './composable/exprEditor'
|
||||
import('./composable/exprEditor')
|
||||
// @ts-ignore
|
||||
export type { TemplateEditorOptions } from './composable/templateEditor'
|
||||
import('./composable/templateEditor')
|
||||
// @ts-ignore
|
||||
export type { Config, Profile } from './stores/config'
|
||||
import('./stores/config')
|
||||
// @ts-ignore
|
||||
@@ -446,19 +436,15 @@ declare module 'vue' {
|
||||
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
|
||||
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createContainerHints: UnwrapRef<typeof import('./composable/exprEditor')['createContainerHints']>
|
||||
readonly createDrawer: UnwrapRef<typeof import('./composable/drawer')['createDrawer']>
|
||||
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
|
||||
readonly createExprEditor: UnwrapRef<typeof import('./composable/exprEditor')['createExprEditor']>
|
||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createLogHints: UnwrapRef<typeof import('./composable/exprEditor')['createLogHints']>
|
||||
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
|
||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createTemplateEditor: UnwrapRef<typeof import('./composable/templateEditor')['createTemplateEditor']>
|
||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
@@ -627,7 +613,6 @@ declare module 'vue' {
|
||||
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssSupports: UnwrapRef<typeof import('@vueuse/core')['useCssSupports']>
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
|
||||
@@ -684,7 +669,6 @@ declare module 'vue' {
|
||||
readonly useK8sStore: UnwrapRef<typeof import('./stores/k8s')['useK8sStore']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
@@ -728,8 +712,8 @@ declare module 'vue' {
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
|
||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||
|
||||
Vendored
-18
@@ -11,8 +11,6 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AlertCard: typeof import('./components/Notification/AlertCard.vue')['default']
|
||||
AlertForm: typeof import('./components/Notification/AlertForm.vue')['default']
|
||||
Announcements: typeof import('./components/Announcements.vue')['default']
|
||||
BarChart: typeof import('./components/BarChart.vue')['default']
|
||||
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
|
||||
@@ -32,7 +30,6 @@ declare module 'vue' {
|
||||
'Cil:circle': typeof import('~icons/cil/circle')['default']
|
||||
'Cil:columns': typeof import('~icons/cil/columns')['default']
|
||||
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
|
||||
CloudDestinationForm: typeof import('./components/Notification/CloudDestinationForm.vue')['default']
|
||||
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
|
||||
ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default']
|
||||
ContainerDropdown: typeof import('./components/ContainerDropdown.vue')['default']
|
||||
@@ -44,8 +41,6 @@ declare module 'vue' {
|
||||
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
|
||||
ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default']
|
||||
DateTime: typeof import('./components/common/DateTime.vue')['default']
|
||||
DestinationCard: typeof import('./components/Notification/DestinationCard.vue')['default']
|
||||
DestinationForm: typeof import('./components/Notification/DestinationForm.vue')['default']
|
||||
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
|
||||
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
|
||||
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
|
||||
@@ -85,22 +80,15 @@ declare module 'vue' {
|
||||
'MaterialSymbols:terminal': typeof import('~icons/material-symbols/terminal')['default']
|
||||
'MaterialSymbolsLight:collapseAll': typeof import('~icons/material-symbols-light/collapse-all')['default']
|
||||
'Mdi:account': typeof import('~icons/mdi/account')['default']
|
||||
'Mdi:alert': typeof import('~icons/mdi/alert')['default']
|
||||
'Mdi:alertCircle': typeof import('~icons/mdi/alert-circle')['default']
|
||||
'Mdi:alertOutline': typeof import('~icons/mdi/alert-outline')['default']
|
||||
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
|
||||
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
|
||||
'Mdi:beer': typeof import('~icons/mdi/beer')['default']
|
||||
'Mdi:bell': typeof import('~icons/mdi/bell')['default']
|
||||
'Mdi:check': typeof import('~icons/mdi/check')['default']
|
||||
'Mdi:checkCircle': typeof import('~icons/mdi/check-circle')['default']
|
||||
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
|
||||
'Mdi:chevronDown': typeof import('~icons/mdi/chevron-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:cloud': typeof import('~icons/mdi/cloud')['default']
|
||||
'Mdi:cloudOutline': typeof import('~icons/mdi/cloud-outline')['default']
|
||||
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
|
||||
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
|
||||
'Mdi:gauge': typeof import('~icons/mdi/gauge')['default']
|
||||
@@ -110,13 +98,8 @@ declare module 'vue' {
|
||||
'Mdi:key': typeof import('~icons/mdi/key')['default']
|
||||
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
'Mdi:lightningBolt': typeof import('~icons/mdi/lightning-bolt')['default']
|
||||
'Mdi:linkVariant': typeof import('~icons/mdi/link-variant')['default']
|
||||
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
|
||||
'Mdi:pencilOutline': typeof import('~icons/mdi/pencil-outline')['default']
|
||||
'Mdi:plus': typeof import('~icons/mdi/plus')['default']
|
||||
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
|
||||
'Mdi:trashCanOutline': typeof import('~icons/mdi/trash-can-outline')['default']
|
||||
'Mdi:webhook': typeof import('~icons/mdi/webhook')['default']
|
||||
MetricCard: typeof import('./components/MetricCard.vue')['default']
|
||||
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
|
||||
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
|
||||
@@ -167,7 +150,6 @@ declare module 'vue' {
|
||||
ToastModal: typeof import('./components/common/ToastModal.vue')['default']
|
||||
Toggle: typeof import('./components/common/Toggle.vue')['default']
|
||||
ViewerWithSource: typeof import('./components/LogViewer/ViewerWithSource.vue')['default']
|
||||
WebhookDestinationForm: typeof import('./components/Notification/WebhookDestinationForm.vue')['default']
|
||||
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,12 +203,7 @@ if (enableShell) {
|
||||
}
|
||||
|
||||
const containerRef = computed(() => [container]);
|
||||
const { downloadUrl, isFiltered } = useDownloadUrl(
|
||||
containerRef,
|
||||
streamConfig,
|
||||
levels,
|
||||
toRef(() => container.name),
|
||||
);
|
||||
const { downloadUrl, isFiltered } = useDownloadUrl(containerRef, streamConfig, levels);
|
||||
|
||||
const disableRestart = computed(() => actionStates.stop || actionStates.start || actionStates.restart);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="@container flex min-w-0 flex-1 items-center gap-1.5 md:gap-2">
|
||||
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
|
||||
<label class="swap swap-rotate size-4">
|
||||
<input type="checkbox" v-model="pinned" />
|
||||
<carbon:star-filled class="swap-on text-secondary" />
|
||||
<carbon:star class="swap-off" />
|
||||
</label>
|
||||
<div class="inline-flex min-w-0 items-center text-sm">
|
||||
<div class="breadcrumbs min-w-0 overflow-x-visible p-0 font-mono">
|
||||
<div class="inline-flex items-center text-sm">
|
||||
<div class="breadcrumbs overflow-x-visible p-0 font-mono">
|
||||
<ul>
|
||||
<li v-if="config.hosts.length > 1" class="font-thin max-md:hidden">
|
||||
{{ container.hostLabel }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{{ $t("label.container", group.containers.length) }}
|
||||
</ContainerDropdown>
|
||||
<MultiContainerStat class="ml-auto" :containers="group.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" :name="group.name" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</Tag>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" :name="host.name" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ContainerDropdown>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="namespace.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" :name="namespace.name" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ContainerDropdown>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="owner.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" :name="owner.name" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
|
||||
@@ -3,15 +3,6 @@
|
||||
<slot name="more-items"></slot>
|
||||
<Announcements />
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/notifications' }"
|
||||
:aria-label="$t('title.notifications')"
|
||||
data-testid="notifications"
|
||||
class="btn btn-circle btn-sm"
|
||||
>
|
||||
<mdi:bell class="size-6" />
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/settings' }"
|
||||
:aria-label="$t('title.settings')"
|
||||
|
||||
@@ -73,12 +73,6 @@
|
||||
{{ $t("action.show-details") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a @click="createAlert()">
|
||||
<mdi:bell />
|
||||
{{ $t("action.create-alert") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
@@ -88,7 +82,6 @@ import stripAnsi from "strip-ansi";
|
||||
import { Container } from "@/models/Container";
|
||||
import { LogEntry, SimpleLogEntry, ComplexLogEntry, GroupedLogEntry, JSONObject } from "@/models/LogEntry";
|
||||
import LogDetails from "./LogDetails.vue";
|
||||
import AlertForm from "@/components/Notification/AlertForm.vue";
|
||||
|
||||
const { logEntry, container } = defineProps<{
|
||||
logEntry: LogEntry<string | JSONObject>;
|
||||
@@ -154,22 +147,6 @@ async function copyPermalink() {
|
||||
}
|
||||
}
|
||||
|
||||
function createAlert() {
|
||||
const containerExpr = `name contains "${container.name}"`;
|
||||
let logExpr = "";
|
||||
if (logEntry.level && logEntry.level !== "unknown") {
|
||||
logExpr = `level == "${logEntry.level}"`;
|
||||
}
|
||||
|
||||
const nameParts = [container.name];
|
||||
if (logEntry.level && logEntry.level !== "unknown") {
|
||||
nameParts.push(logEntry.level);
|
||||
}
|
||||
const name = nameParts.join(" ");
|
||||
|
||||
showDrawer(AlertForm, { prefill: { name, containerExpression: containerExpr, logExpression: logExpr } }, "lg");
|
||||
}
|
||||
|
||||
function hideMenu(e: MouseEvent) {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LogEntry, LogMessage } from "@/models/LogEntry";
|
||||
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||
|
||||
const { progress, currentDate } = useScrollContext();
|
||||
|
||||
const { messages } = defineProps<{
|
||||
messages: LogEntry<LogMessage>[];
|
||||
messages: LogEntry<string | string[] | JSONObject>[];
|
||||
}>();
|
||||
|
||||
const { containers } = useLoggingContext();
|
||||
|
||||
@@ -94,11 +94,9 @@ const { showSearch } = useSearchFilter();
|
||||
const { enableDownload } = config;
|
||||
const clear = defineEmit();
|
||||
|
||||
const { name } = defineProps<{ name?: string }>();
|
||||
|
||||
const { streamConfig, showHostname, showContainerName, containers, levels } = useLoggingContext();
|
||||
|
||||
const { downloadUrl, isFiltered } = useDownloadUrl(containers, streamConfig, levels, name);
|
||||
const { downloadUrl, isFiltered } = useDownloadUrl(containers, streamConfig, levels);
|
||||
|
||||
const hideMenu = (e: MouseEvent) => {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div class="flex gap-1 md:gap-4">
|
||||
<div
|
||||
class="grid hidden min-w-15 grid-cols-[auto_1fr_auto_1fr] items-center gap-0.5 text-xs leading-none sm:grid md:grid-cols-[auto_1fr]"
|
||||
>
|
||||
<div class="grid min-w-15 grid-cols-[auto_1fr] items-center gap-0.5 text-xs leading-none max-md:hidden">
|
||||
<PhArrowUp class="text-primary" />
|
||||
<span class="tabular-nums">{{ formatBytes(networkRate.tx, { short: true, decimals: 1 }) }}/s</span>
|
||||
<PhArrowDown class="text-secondary" />
|
||||
@@ -117,7 +115,7 @@ const limits = computed(() => {
|
||||
totalCpu += hostTotalCpu;
|
||||
} else {
|
||||
// All containers have limits, sum them up (capped at host total)
|
||||
const sumCpu = hostContainers.reduce((sum, c) => sum + (c.cpuLimit || 0), 0);
|
||||
const sumCpu = hostContainers.reduce((sum, c) => sum + 1, 0);
|
||||
totalCpu += Math.min(sumCpu, hostTotalCpu);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
<li><a><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M21 19v1H3v-1l2-2v-6c0-3.1 2.03-5.83 5-6.71V4a2 2 0 0 1 2-2a2 2 0 0 1 2 2v.29c2.97.88 5 3.61 5 6.71v6zm-7 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"></path>
|
||||
</svg> action.create-alert</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
@@ -62,9 +59,6 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
<li><a><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M21 19v1H3v-1l2-2v-6c0-3.1 2.03-5.83 5-6.71V4a2 2 0 0 1 2-2a2 2 0 0 1 2 2v.29c2.97.88 5 3.61 5 6.71v6zm-7 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"></path>
|
||||
</svg> action.create-alert</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
@@ -103,9 +97,6 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
<li><a><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M21 19v1H3v-1l2-2v-6c0-3.1 2.03-5.83 5-6.71V4a2 2 0 0 1 2-2a2 2 0 0 1 2 2v.29c2.97.88 5 3.61 5 6.71v6zm-7 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"></path>
|
||||
</svg> action.create-alert</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<div class="card bg-base-100 shadow-sm" :class="{ 'opacity-60': !alert.enabled }">
|
||||
<div class="card-body gap-4 p-5">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="flex items-center gap-2 text-lg font-semibold">
|
||||
<span>{{ alert.name }}</span> <span class="text-sm font-light">→</span>
|
||||
<span class="flex gap-1 text-xs font-light" :class="{ 'text-warning': !alert.dispatcher }">
|
||||
<template v-if="alert.dispatcher">
|
||||
<mdi:webhook v-if="alert.dispatcher.type === 'webhook'" />
|
||||
<mdi:cloud v-else />
|
||||
{{ alert.dispatcher.name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<mdi:alert-outline />
|
||||
{{ $t("notifications.alert.dispatcher-deleted") }}
|
||||
</template>
|
||||
</span>
|
||||
</h4>
|
||||
<span v-if="!alert.enabled" class="badge badge-warning badge-sm">{{ $t("notifications.alert.paused") }}</span>
|
||||
</div>
|
||||
<input type="checkbox" class="toggle toggle-primary" :checked="alert.enabled" @change="toggleEnabled" />
|
||||
</div>
|
||||
|
||||
<!-- Expressions -->
|
||||
<div class="text-base-content/80 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
|
||||
<span>{{ $t("notifications.alert.containers") }}</span>
|
||||
<code class="bg-base-200 text-base-content rounded px-2 py-0.5 font-mono">{{ alert.containerExpression }}</code>
|
||||
<span>{{ $t("notifications.alert.log-filter") }}</span>
|
||||
<code class="bg-base-200 text-base-content rounded px-2 py-0.5 font-mono">{{ alert.logExpression }}</code>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="border-base-content/10 text-base-content/80 flex items-center justify-between border-t pt-3 text-xs">
|
||||
<div class="flex items-center gap-4">
|
||||
<span>
|
||||
{{ $t("notifications.alert.containers-count", { count: alert.triggeredContainers }) }}
|
||||
</span>
|
||||
<span>
|
||||
{{ $t("notifications.alert.triggered-count", { count: alert.triggerCount }) }}
|
||||
</span>
|
||||
<span v-if="alert.lastTriggeredAt">
|
||||
{{ $t("notifications.alert.last-triggered", { time: formatTimeAgo(alert.lastTriggeredAt) }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<button class="btn btn-ghost btn-square" @click="editAlert">
|
||||
<mdi:pencil-outline />
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-square" @click="deleteAlert" :disabled="isDeleting">
|
||||
<span v-if="isDeleting" class="loading loading-spinner loading-xs"></span>
|
||||
<mdi:trash-can-outline v-else />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationRule } from "@/types/notifications";
|
||||
import AlertForm from "./AlertForm.vue";
|
||||
|
||||
const { alert, onUpdated } = defineProps<{
|
||||
alert: NotificationRule;
|
||||
onUpdated?: () => void;
|
||||
}>();
|
||||
|
||||
const showDrawer = useDrawer();
|
||||
const isDeleting = ref(false);
|
||||
|
||||
function formatTimeAgo(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
if (date.getFullYear() === 0) return "-";
|
||||
return toRelativeTime(date, undefined);
|
||||
}
|
||||
|
||||
async function toggleEnabled() {
|
||||
await fetch(withBase(`/api/notifications/rules/${alert.id}`), {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled: !alert.enabled }),
|
||||
});
|
||||
onUpdated?.();
|
||||
}
|
||||
|
||||
function editAlert() {
|
||||
showDrawer(AlertForm, { alert, onCreated: onUpdated }, "lg");
|
||||
}
|
||||
|
||||
async function deleteAlert() {
|
||||
isDeleting.value = true;
|
||||
try {
|
||||
await fetch(withBase(`/api/notifications/rules/${alert.id}`), { method: "DELETE" });
|
||||
onUpdated?.();
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,343 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ isEditing ? $t("notifications.alert-form.edit-title") : $t("notifications.alert-form.create-title") }}
|
||||
</h2>
|
||||
<p class="text-base-content/60">{{ $t("notifications.alert-form.description") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Alert Name -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.alert-name") }}</legend>
|
||||
<input
|
||||
ref="alertNameInput"
|
||||
v-model="alertName"
|
||||
type="text"
|
||||
class="input focus:input-primary w-full text-base"
|
||||
:class="alertName.trim() ? 'input-primary' : ''"
|
||||
required
|
||||
:placeholder="$t('notifications.alert-form.alert-name-placeholder')"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<!-- Container Filter -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.container-filter") }}</legend>
|
||||
<div
|
||||
class="input focus-within:input-primary w-full focus-within:z-50"
|
||||
:class="
|
||||
containerExpression.trim() && !containerResult?.error
|
||||
? 'input-primary'
|
||||
: { 'input-error!': containerResult?.error }
|
||||
"
|
||||
>
|
||||
<div ref="containerEditorRef" class="w-full"></div>
|
||||
</div>
|
||||
<div v-if="containerResult" class="fieldset-label">
|
||||
<span v-if="containerResult.error" class="text-error">{{ containerResult.error }}</span>
|
||||
<span v-else-if="containerResult.containers?.length" class="text-success">
|
||||
<mdi:check class="inline" />
|
||||
{{
|
||||
$t("notifications.alert-form.containers-match", {
|
||||
count: containerResult.containers.length,
|
||||
names: containerResult.containers.map((c) => c.name).join(", "),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="text-warning">
|
||||
<mdi:alert class="inline" />
|
||||
{{ $t("notifications.alert-form.no-containers-match") }}
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Log Filter -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.log-filter") }}</legend>
|
||||
<div
|
||||
class="input focus-within:input-primary w-full focus-within:z-50"
|
||||
:class="logExpression.trim() && !logError ? 'input-primary' : { 'input-error!': logError }"
|
||||
>
|
||||
<div ref="logEditorRef" class="w-full"></div>
|
||||
</div>
|
||||
<div v-if="logError || logExpression" class="fieldset-label">
|
||||
<span v-if="logError" class="text-error">{{ logError }}</span>
|
||||
<span v-else-if="logMessages.length" class="text-success">
|
||||
<mdi:check class="inline" />
|
||||
{{ $t("notifications.alert-form.logs-match", { count: logTotalCount }) }}
|
||||
</span>
|
||||
<span v-else-if="!isLoading" class="text-warning">
|
||||
<mdi:alert class="inline" />
|
||||
{{ $t("notifications.alert-form.no-logs-match") }}
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Destination -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.destination") }}</legend>
|
||||
<details class="dropdown w-full" ref="destinationDropdown">
|
||||
<summary class="btn btn-outline w-full justify-between" :class="{ 'btn-primary': selectedDestination }">
|
||||
<span class="flex items-center gap-2">
|
||||
<template v-if="selectedDestination">
|
||||
<mdi:webhook v-if="selectedDestination.type === 'webhook'" />
|
||||
<mdi:cloud v-else />
|
||||
{{ selectedDestination.name }}
|
||||
</template>
|
||||
<span v-else class="text-base-content/60">{{ $t("notifications.alert-form.select-destination") }}</span>
|
||||
</span>
|
||||
<carbon:caret-down />
|
||||
</summary>
|
||||
<ul class="dropdown-content menu bg-base-200 rounded-box z-50 mt-1 w-full border p-2 shadow-sm">
|
||||
<li v-for="dest in destinations" :key="dest.id">
|
||||
<a
|
||||
@click="
|
||||
dispatcherId = dest.id;
|
||||
destinationDropdown?.removeAttribute('open');
|
||||
"
|
||||
:class="{ active: dispatcherId === dest.id }"
|
||||
>
|
||||
<mdi:webhook v-if="dest.type === 'webhook'" />
|
||||
<mdi:cloud v-else />
|
||||
{{ dest.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<div v-if="!destinations.length" class="fieldset-label">
|
||||
<span class="text-warning">
|
||||
<mdi:alert class="inline" />
|
||||
{{ $t("notifications.alert-form.no-destinations") }}
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Log Preview -->
|
||||
<div v-if="logMessages.length" class="mt-4">
|
||||
<div class="mb-2 text-lg">{{ $t("notifications.alert-form.preview") }}</div>
|
||||
<LogList
|
||||
:messages="logMessages"
|
||||
:last-selected-item="undefined"
|
||||
class="border-base-content/50 h-64 overflow-hidden rounded-lg border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div v-if="saveError" class="alert alert-error">
|
||||
<span>{{ saveError }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-2 pt-4">
|
||||
<button class="btn" @click="close?.()">{{ $t("notifications.alert-form.cancel") }}</button>
|
||||
<button class="btn btn-primary" :disabled="!canSave" @click="saveAlert">
|
||||
<span v-if="isSaving" class="loading loading-spinner loading-sm"></span>
|
||||
{{ isEditing ? $t("notifications.alert-form.save") : $t("notifications.alert-form.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type LogEvent, type LogEntry, type LogMessage, asLogEntry } from "@/models/LogEntry";
|
||||
import { Container } from "@/models/Container";
|
||||
import type { ContainerJson } from "@/types/Container";
|
||||
import { createExprEditor, createContainerHints, createLogHints } from "@/composable/exprEditor";
|
||||
|
||||
import type { Dispatcher, NotificationRule, PreviewResult } from "@/types/notifications";
|
||||
|
||||
const { close, onCreated, alert, prefill } = defineProps<{
|
||||
close?: () => void;
|
||||
onCreated?: () => void;
|
||||
alert?: NotificationRule;
|
||||
prefill?: { name?: string; containerExpression?: string; logExpression?: string };
|
||||
}>();
|
||||
|
||||
// Fetch dispatchers
|
||||
const destinations = ref<Dispatcher[]>([]);
|
||||
onMounted(async () => {
|
||||
const res = await fetch(withBase("/api/notifications/dispatchers"));
|
||||
destinations.value = await res.json();
|
||||
});
|
||||
|
||||
// Container store for autocomplete hints
|
||||
const containerStore = useContainerStore();
|
||||
const { containers } = storeToRefs(containerStore);
|
||||
const containerNames = computed(() => [
|
||||
...new Set(containers.value.filter((c) => c.state === "running").map((c) => c.name)),
|
||||
]);
|
||||
const imageNames = computed(() => [...new Set(containers.value.map((c) => c.image))]);
|
||||
const hostNames = computed(() => [...new Set(containers.value.map((c) => c.host))]);
|
||||
|
||||
// Template refs
|
||||
const alertNameInput = ref<HTMLInputElement>();
|
||||
const containerEditorRef = ref<HTMLElement>();
|
||||
const logEditorRef = ref<HTMLElement>();
|
||||
const destinationDropdown = ref<HTMLDetailsElement>();
|
||||
|
||||
// Form state
|
||||
const isEditing = computed(() => !!alert);
|
||||
const alertName = ref(alert?.name ?? prefill?.name ?? "");
|
||||
const containerExpression = ref(alert?.containerExpression ?? prefill?.containerExpression ?? "");
|
||||
const logExpression = ref(alert?.logExpression ?? prefill?.logExpression ?? "");
|
||||
const dispatcherId = ref(alert?.dispatcher?.id ?? 0);
|
||||
const selectedDestination = computed(() => destinations.value.find((d) => d.id === dispatcherId.value));
|
||||
useFocus(alertNameInput, { initialValue: true });
|
||||
|
||||
// Validation state
|
||||
interface ContainerResult {
|
||||
error?: string;
|
||||
containers?: Container[];
|
||||
}
|
||||
const containerResult = ref<ContainerResult | null>(null);
|
||||
const logError = ref<string | null>(null);
|
||||
const logTotalCount = ref(0);
|
||||
const logMessages = shallowRef<LogEntry<LogMessage>[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const saveError = ref<string | null>(null);
|
||||
|
||||
const canSave = computed(
|
||||
() =>
|
||||
alertName.value.trim() &&
|
||||
containerExpression.value.trim() &&
|
||||
dispatcherId.value > 0 &&
|
||||
!containerResult.value?.error &&
|
||||
!logError.value &&
|
||||
!isSaving.value,
|
||||
);
|
||||
|
||||
async function saveAlert() {
|
||||
if (!canSave.value) return;
|
||||
|
||||
isSaving.value = true;
|
||||
saveError.value = null;
|
||||
|
||||
try {
|
||||
const input = {
|
||||
name: alertName.value.trim(),
|
||||
containerExpression: containerExpression.value,
|
||||
logExpression: logExpression.value,
|
||||
dispatcherId: dispatcherId.value!,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const url = isEditing.value
|
||||
? withBase(`/api/notifications/rules/${alert!.id}`)
|
||||
: withBase("/api/notifications/rules");
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: isEditing.value ? "PUT" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Failed to save alert");
|
||||
}
|
||||
|
||||
onCreated?.();
|
||||
close?.();
|
||||
} catch (e) {
|
||||
saveError.value = e instanceof Error ? e.message : "Failed to save alert";
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function validateExpressions() {
|
||||
if (!containerExpression.value && !logExpression.value) {
|
||||
containerResult.value = null;
|
||||
logError.value = null;
|
||||
logTotalCount.value = 0;
|
||||
logMessages.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(withBase("/api/notifications/preview"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
containerExpression: containerExpression.value,
|
||||
logExpression: logExpression.value || undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
throw new Error(errData.error || "Preview failed");
|
||||
}
|
||||
|
||||
const data: PreviewResult = await res.json();
|
||||
|
||||
// Update container result
|
||||
containerResult.value = containerExpression.value
|
||||
? {
|
||||
error: data.containerError ?? undefined,
|
||||
containers: data.matchedContainers?.map((c) => Container.fromJSON(c as ContainerJson)),
|
||||
}
|
||||
: null;
|
||||
|
||||
// Update log result
|
||||
if (logExpression.value && !data.containerError) {
|
||||
logError.value = data.logError ?? null;
|
||||
logTotalCount.value = data.totalLogs;
|
||||
logMessages.value = data.matchedLogs?.map((event) => asLogEntry(event as LogEvent)) ?? [];
|
||||
} else {
|
||||
logError.value = null;
|
||||
logTotalCount.value = 0;
|
||||
logMessages.value = [];
|
||||
}
|
||||
} catch (e) {
|
||||
containerResult.value = { error: e instanceof Error ? e.message : "Unknown error" };
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedValidate = useDebounceFn(validateExpressions, 500);
|
||||
|
||||
watch(
|
||||
[containerExpression, logExpression],
|
||||
() => {
|
||||
isLoading.value = true;
|
||||
debouncedValidate();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
let containerEditorView: Awaited<ReturnType<typeof createExprEditor>> | undefined;
|
||||
let logEditorView: Awaited<ReturnType<typeof createExprEditor>> | undefined;
|
||||
|
||||
onMounted(async () => {
|
||||
if (containerEditorRef.value) {
|
||||
containerEditorView = await createExprEditor({
|
||||
parent: containerEditorRef.value,
|
||||
placeholder: 'name contains "api"',
|
||||
initialValue: alert?.containerExpression ?? prefill?.containerExpression ?? "",
|
||||
getHints: () => createContainerHints(containerNames.value, imageNames.value, hostNames.value),
|
||||
onChange: (v) => (containerExpression.value = v),
|
||||
});
|
||||
}
|
||||
|
||||
if (logEditorRef.value) {
|
||||
logEditorView = await createExprEditor({
|
||||
parent: logEditorRef.value,
|
||||
placeholder: 'level == "error" && message contains "timeout"',
|
||||
initialValue: alert?.logExpression ?? prefill?.logExpression ?? "",
|
||||
getHints: createLogHints,
|
||||
onChange: (v) => (logExpression.value = v),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
containerEditorView?.destroy();
|
||||
logEditorView?.destroy();
|
||||
});
|
||||
</script>
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Cloud linked (when editing with prefix) -->
|
||||
<fieldset v-if="destination?.prefix" class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.api-key") }}</legend>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
type="text"
|
||||
:value="destination.prefix + '**************************************'"
|
||||
readonly
|
||||
disabled
|
||||
class="input join-item w-full font-mono"
|
||||
:class="cloudStatusError ? 'input-error' : 'input-success'"
|
||||
/>
|
||||
<span class="join-item btn pointer-events-none" :class="cloudStatusError ? 'btn-error' : 'btn-success'">
|
||||
<mdi:alert-circle v-if="cloudStatusError" class="text-lg" />
|
||||
<mdi:check v-else class="text-lg" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Cloud Status -->
|
||||
<div v-if="isLoadingCloudStatus" class="mt-3 flex items-center gap-2">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<span class="text-base-content/60 text-sm">{{ $t("notifications.destination-form.cloud-checking") }}</span>
|
||||
</div>
|
||||
<div v-else-if="cloudStatusError" class="mt-3">
|
||||
<div class="alert alert-error">
|
||||
<mdi:alert-circle class="text-lg" />
|
||||
<span>{{ $t("notifications.destination-form.cloud-relink") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="cloudStatus" class="mt-3 space-y-3">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-base-content/60">{{ $t("notifications.destination-form.cloud-plan") }}</span>
|
||||
<span class="badge badge-primary badge-sm capitalize">{{ cloudStatus.plan.name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-base-content/60">{{ $t("notifications.destination-form.cloud-usage") }}</span>
|
||||
<span
|
||||
>{{ cloudStatus.usage.events_used.toLocaleString() }} /
|
||||
{{ cloudStatus.usage.events_limit.toLocaleString() }}</span
|
||||
>
|
||||
</div>
|
||||
<progress
|
||||
class="progress w-full"
|
||||
:class="usagePercent > 90 ? 'progress-error' : usagePercent > 70 ? 'progress-warning' : 'progress-primary'"
|
||||
:value="cloudStatus.usage.events_used"
|
||||
:max="cloudStatus.usage.events_limit"
|
||||
></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-base-content/60 mt-2 text-sm">
|
||||
{{ $t("notifications.destination-form.cloud-settings-hint") }}
|
||||
<a :href="cloudSettingsUrl" target="_blank" class="link link-primary">
|
||||
{{ $t("notifications.destination-form.cloud-settings-link") }}
|
||||
</a>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<!-- Link Dozzle Cloud (when creating or not linked) -->
|
||||
<div v-else class="card card-border border-primary/30 bg-primary/5">
|
||||
<div class="card-body items-center text-center">
|
||||
<mdi:cloud-outline class="text-primary text-4xl" />
|
||||
<h3 class="card-title">{{ $t("notifications.destination-form.link-cloud") }}</h3>
|
||||
<p class="text-base-content/60 text-sm">{{ $t("notifications.destination-form.cloud-description") }}</p>
|
||||
<a :href="cloudLinkUrl" class="btn btn-primary btn-lg mt-2">
|
||||
<mdi:link-variant class="text-lg" />
|
||||
{{ $t("notifications.destination-form.link-cloud-button") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2 pt-4">
|
||||
<div class="flex-1"></div>
|
||||
<button class="btn btn-primary" @click="close?.()">
|
||||
{{ $t("notifications.destination-form.close") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dispatcher } from "@/types/notifications";
|
||||
|
||||
const { destination, close } = defineProps<{
|
||||
destination?: Dispatcher;
|
||||
close?: () => void;
|
||||
}>();
|
||||
|
||||
const callbackUrl = `${window.location.origin}${withBase("/")}`;
|
||||
const cloudLinkUrl = `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
const cloudSettingsUrl = `${__CLOUD_URL__}/settings`;
|
||||
|
||||
// Cloud status
|
||||
interface CloudStatus {
|
||||
user: { email: string; name: string };
|
||||
plan: { name: string; events_per_month: number; retention_days: number };
|
||||
usage: { events_used: number; events_limit: number; period: string };
|
||||
}
|
||||
|
||||
const cloudStatus = ref<CloudStatus | null>(null);
|
||||
const cloudStatusError = ref(false);
|
||||
const isLoadingCloudStatus = ref(false);
|
||||
|
||||
const usagePercent = computed(() => {
|
||||
if (!cloudStatus.value) return 0;
|
||||
return (cloudStatus.value.usage.events_used / cloudStatus.value.usage.events_limit) * 100;
|
||||
});
|
||||
|
||||
async function fetchCloudStatus() {
|
||||
isLoadingCloudStatus.value = true;
|
||||
cloudStatusError.value = false;
|
||||
try {
|
||||
const res = await fetch(withBase("/api/cloud/status"));
|
||||
if (!res.ok) {
|
||||
cloudStatusError.value = true;
|
||||
return;
|
||||
}
|
||||
cloudStatus.value = await res.json();
|
||||
} catch {
|
||||
cloudStatusError.value = true;
|
||||
} finally {
|
||||
isLoadingCloudStatus.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (destination?.prefix) {
|
||||
fetchCloudStatus();
|
||||
}
|
||||
</script>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<div class="card bg-base-100 hover:border-primary cursor-pointer border border-transparent" @click="editDestination">
|
||||
<div class="card-body gap-2 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg">
|
||||
<mdi:webhook v-if="destination.type === 'webhook'" class="text-lg" />
|
||||
<mdi:cloud v-else class="text-primary-content text-lg" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-semibold">{{ destination.name }}</h4>
|
||||
<p class="text-base-content/60 text-sm">
|
||||
{{
|
||||
destination.type === "webhook"
|
||||
? $t("notifications.destination.http-webhook")
|
||||
: $t("notifications.destination.dozzle-cloud")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dropdown dropdown-end" @click.stop>
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm btn-square">
|
||||
<ion:ellipsis-vertical />
|
||||
</label>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box bg-base-100 border-base-content/20 z-50 w-40 border p-1 shadow-sm"
|
||||
>
|
||||
<li>
|
||||
<a @click="editDestination">{{ $t("notifications.destination.edit") }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-error" @click="deleteDestination">{{ $t("notifications.destination.delete") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dispatcher } from "@/types/notifications";
|
||||
import DestinationForm from "./DestinationForm.vue";
|
||||
|
||||
const { destination, onUpdated, existingDispatchers } = defineProps<{
|
||||
destination: Dispatcher;
|
||||
onUpdated?: () => void;
|
||||
existingDispatchers: Dispatcher[];
|
||||
}>();
|
||||
|
||||
const showDrawer = useDrawer();
|
||||
|
||||
function editDestination() {
|
||||
showDrawer(
|
||||
DestinationForm,
|
||||
{
|
||||
destination,
|
||||
onCreated: onUpdated,
|
||||
existingDispatchers,
|
||||
},
|
||||
"md",
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteDestination() {
|
||||
await fetch(withBase(`/api/notifications/dispatchers/${destination.id}`), { method: "DELETE" });
|
||||
onUpdated?.();
|
||||
}
|
||||
</script>
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{
|
||||
isEditing
|
||||
? $t("notifications.destination-form.edit-title")
|
||||
: $t("notifications.destination-form.create-title")
|
||||
}}
|
||||
</h2>
|
||||
<p class="text-base-content/60">{{ $t("notifications.destination-form.description") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Link Success Alert -->
|
||||
<div v-if="showLinkSuccess" class="alert alert-success">
|
||||
<mdi:check-circle class="text-lg" />
|
||||
<div>
|
||||
<div class="font-semibold">{{ $t("notifications.cloud-link-success.title") }}</div>
|
||||
<div class="text-sm">{{ $t("notifications.cloud-link-success.message") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type Selection -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.type") }}</legend>
|
||||
<div class="space-y-3">
|
||||
<label
|
||||
class="card card-border 20 cursor-pointer transition-colors"
|
||||
:class="type === 'webhook' ? 'border-primary bg-primary/10' : ''"
|
||||
>
|
||||
<div class="card-body flex-row items-center gap-3 p-4">
|
||||
<input type="radio" v-model="type" value="webhook" class="radio radio-primary" />
|
||||
<div>
|
||||
<div class="font-semibold">{{ $t("notifications.destination-form.webhook-title") }}</div>
|
||||
<div class="text-base-content/60 text-sm">
|
||||
{{ $t("notifications.destination-form.webhook-description") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
class="card card-border border-base-content/20 transition-colors"
|
||||
:class="[
|
||||
type === 'cloud' ? 'border-primary bg-primary/10' : '',
|
||||
hasExistingCloudDestination && type !== 'cloud' ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
|
||||
]"
|
||||
>
|
||||
<div class="card-body flex-row items-center gap-3 p-4">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="type"
|
||||
value="cloud"
|
||||
class="radio radio-primary"
|
||||
:disabled="hasExistingCloudDestination && type !== 'cloud'"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-semibold">{{ $t("notifications.destination-form.cloud-title") }}</div>
|
||||
<div class="text-base-content/60 text-sm">
|
||||
{{ $t("notifications.destination-form.cloud-description") }}
|
||||
</div>
|
||||
<div v-if="hasExistingCloudDestination && type !== 'cloud'" class="text-warning mt-1 text-xs">
|
||||
{{ $t("notifications.destination-form.cloud-exists") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Type-specific form -->
|
||||
<WebhookDestinationForm
|
||||
v-if="type === 'webhook'"
|
||||
:destination="destination"
|
||||
:close="close"
|
||||
:on-created="onCreated"
|
||||
:is-editing="isEditing"
|
||||
/>
|
||||
<CloudDestinationForm v-else :destination="destination" :close="close" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dispatcher } from "@/types/notifications";
|
||||
import WebhookDestinationForm from "./WebhookDestinationForm.vue";
|
||||
import CloudDestinationForm from "./CloudDestinationForm.vue";
|
||||
|
||||
const {
|
||||
close,
|
||||
onCreated,
|
||||
destination,
|
||||
existingDispatchers = [],
|
||||
showLinkSuccess = false,
|
||||
} = defineProps<{
|
||||
close?: () => void;
|
||||
onCreated?: () => void;
|
||||
destination?: Dispatcher;
|
||||
existingDispatchers?: Dispatcher[];
|
||||
showLinkSuccess?: boolean;
|
||||
}>();
|
||||
|
||||
const isEditing = !!destination;
|
||||
const type = ref<"webhook" | "cloud">((destination?.type as "webhook" | "cloud") ?? "webhook");
|
||||
|
||||
const hasExistingCloudDestination = computed(() => {
|
||||
const others = isEditing ? existingDispatchers.filter((d) => d.id !== destination!.id) : existingDispatchers;
|
||||
return others.some((d) => d.type === "cloud");
|
||||
});
|
||||
</script>
|
||||
@@ -1,226 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Name -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.name") }}</legend>
|
||||
<input
|
||||
ref="nameInput"
|
||||
v-model="name"
|
||||
type="text"
|
||||
class="input focus:input-primary w-full text-base"
|
||||
required
|
||||
:class="{ 'input-primary': name.trim().length > 0 }"
|
||||
:placeholder="$t('notifications.destination-form.name-placeholder')"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<!-- Webhook URL -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.webhook-url") }}</legend>
|
||||
<input
|
||||
v-model="webhookUrl"
|
||||
type="url"
|
||||
class="input focus:input-primary w-full text-base"
|
||||
:class="{ 'input-primary': isValidUrl, 'input-error': webhookUrl.trim() && !isValidUrl }"
|
||||
:placeholder="$t('notifications.destination-form.webhook-url-placeholder')"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<!-- Payload Format (create mode only) -->
|
||||
<fieldset v-if="!isEditing" class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.payload-format") }}</legend>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="format in ['slack', 'discord', 'ntfy', 'custom'] as const"
|
||||
:key="format"
|
||||
type="button"
|
||||
class="btn btn-sm"
|
||||
:class="payloadFormat === format ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="selectPayloadFormat(format)"
|
||||
>
|
||||
{{ $t(`notifications.destination-form.format-${format}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Template -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">
|
||||
{{ $t("notifications.destination-form.template") }}
|
||||
<span class="text-base-content/60 ml-2 text-sm font-normal">{{
|
||||
$t("notifications.destination-form.template-hint")
|
||||
}}</span>
|
||||
</legend>
|
||||
<div
|
||||
ref="templateEditorRef"
|
||||
class="border-base-content/20 focus-within:border-primary min-h-48 w-full overflow-auto rounded-lg border"
|
||||
></div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Error -->
|
||||
<div v-if="error" class="alert alert-error">
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Test Result -->
|
||||
<div v-if="testResult" class="alert" :class="testResult.success ? 'alert-success' : 'alert-error'">
|
||||
<span v-if="testResult.success">
|
||||
{{ $t("notifications.destination-form.test-success") }}
|
||||
<span v-if="testResult.statusCode" class="opacity-70">({{ testResult.statusCode }})</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ testResult.error }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2 pt-4">
|
||||
<button class="btn" @click="testDestination" :disabled="!canTest || !isValidUrl || isTesting">
|
||||
<span v-if="isTesting" class="loading loading-spinner loading-sm"></span>
|
||||
{{ $t("notifications.destination-form.test") }}
|
||||
</button>
|
||||
<div class="flex-1"></div>
|
||||
<button class="btn" @click="close?.()">
|
||||
{{ $t("notifications.destination-form.cancel") }}
|
||||
</button>
|
||||
<button class="btn btn-primary" :disabled="!canSave" @click="saveDestination">
|
||||
<span v-if="isSaving" class="loading loading-spinner loading-sm"></span>
|
||||
{{ isEditing ? $t("notifications.destination-form.save") : $t("notifications.destination-form.add") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dispatcher, TestWebhookResult } from "@/types/notifications";
|
||||
import { createTemplateEditor } from "@/composable/templateEditor";
|
||||
import { PAYLOAD_TEMPLATES, type PayloadFormat } from "./payloadTemplates";
|
||||
|
||||
const { close, onCreated, destination, isEditing } = defineProps<{
|
||||
close?: () => void;
|
||||
onCreated?: () => void;
|
||||
destination?: Dispatcher;
|
||||
isEditing: boolean;
|
||||
}>();
|
||||
|
||||
const nameInput = ref<HTMLInputElement>();
|
||||
const templateEditorRef = ref<HTMLElement>();
|
||||
const name = ref(destination?.name ?? "");
|
||||
useFocus(nameInput, { initialValue: true });
|
||||
const webhookUrl = ref(destination?.url ?? "");
|
||||
const payloadFormat = ref<PayloadFormat>(isEditing ? "custom" : "slack");
|
||||
const template = ref(isEditing ? (destination?.template ?? "") : PAYLOAD_TEMPLATES[payloadFormat.value]);
|
||||
const isTesting = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const testResult = ref<TestWebhookResult | null>(null);
|
||||
|
||||
let templateEditorView: Awaited<ReturnType<typeof createTemplateEditor>> | undefined;
|
||||
|
||||
function selectPayloadFormat(format: PayloadFormat) {
|
||||
payloadFormat.value = format;
|
||||
template.value = PAYLOAD_TEMPLATES[format];
|
||||
setEditorContent(template.value);
|
||||
}
|
||||
|
||||
function setEditorContent(value: string) {
|
||||
if (!templateEditorView) return;
|
||||
templateEditorView.dispatch({
|
||||
changes: { from: 0, to: templateEditorView.state.doc.length, insert: value },
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!templateEditorRef.value) return;
|
||||
templateEditorView = await createTemplateEditor({
|
||||
parent: templateEditorRef.value,
|
||||
initialValue: template.value,
|
||||
onChange: (v) => (template.value = v),
|
||||
});
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
templateEditorView?.destroy();
|
||||
});
|
||||
|
||||
const canTest = computed(() => webhookUrl.value.trim().length > 0);
|
||||
|
||||
const isValidUrl = computed(() => {
|
||||
try {
|
||||
new URL(webhookUrl.value.trim());
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const canSave = computed(() => {
|
||||
if (isSaving.value) return false;
|
||||
if (!name.value.trim()) return false;
|
||||
if (!isValidUrl.value) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
async function testDestination() {
|
||||
if (!canTest.value) return;
|
||||
|
||||
isTesting.value = true;
|
||||
testResult.value = null;
|
||||
|
||||
try {
|
||||
const res = await fetch(withBase("/api/notifications/test-webhook"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
url: webhookUrl.value.trim(),
|
||||
template: template.value.trim() || undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
const data: TestWebhookResult = await res.json();
|
||||
testResult.value = data;
|
||||
} catch (e) {
|
||||
testResult.value = { success: false, error: e instanceof Error ? e.message : "Test failed" };
|
||||
} finally {
|
||||
isTesting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDestination() {
|
||||
if (!canSave.value) return;
|
||||
|
||||
isSaving.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const input = {
|
||||
name: name.value.trim(),
|
||||
type: "webhook",
|
||||
url: webhookUrl.value.trim(),
|
||||
template: template.value.trim() || undefined,
|
||||
};
|
||||
|
||||
const url = isEditing
|
||||
? withBase(`/api/notifications/dispatchers/${destination!.id}`)
|
||||
: withBase("/api/notifications/dispatchers");
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: isEditing ? "PUT" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Failed to save destination");
|
||||
}
|
||||
|
||||
onCreated?.();
|
||||
close?.();
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Failed to save destination";
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,64 +0,0 @@
|
||||
export type PayloadFormat = "slack" | "discord" | "ntfy" | "custom";
|
||||
|
||||
export const PAYLOAD_TEMPLATES: Record<PayloadFormat, string> = {
|
||||
slack: JSON.stringify(
|
||||
{
|
||||
text: "{{ .Container.Name }}",
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: "*{{ .Container.Name }}*\n{{ .Log.Message }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "Host: {{ .Container.HostName }} | Image: {{ .Container.Image }}",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
discord: JSON.stringify(
|
||||
{
|
||||
content: "{{ .Container.Name }}",
|
||||
embeds: [
|
||||
{
|
||||
title: "{{ .Container.Name }}",
|
||||
description: "{{ .Log.Message }}",
|
||||
fields: [
|
||||
{ name: "Host", value: "{{ .Container.HostName }}", inline: true },
|
||||
{ name: "Image", value: "{{ .Container.Image }}", inline: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
ntfy: JSON.stringify(
|
||||
{
|
||||
topic: "dozzle-{{ .Container.HostName }}",
|
||||
title: "{{ .Container.Name }}",
|
||||
message: "{{ .Log.Message }}",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
custom: JSON.stringify(
|
||||
{
|
||||
container: "{{ .Container.Name }}",
|
||||
level: "{{ .Log.Level }}",
|
||||
message: "{{ .Log.Message }}",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
<ph:stack-simple />
|
||||
<ContainerDropdown :containers="service.containers">{{ service.name }}</ContainerDropdown>
|
||||
<MultiContainerStat class="ml-auto" :containers="service.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" :name="service.name" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ContainerDropdown>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="stack.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" :name="stack.name" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<mdi:close class="swap-on" />
|
||||
</button>
|
||||
</form>
|
||||
<slot v-if="open" :close="close"></slot>
|
||||
<slot v-if="open"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
@@ -28,16 +28,11 @@ const { width } = defineProps<{
|
||||
width: DrawerWidth;
|
||||
}>();
|
||||
|
||||
function close() {
|
||||
panel.value?.close();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open: () => {
|
||||
open.value = true;
|
||||
panel.value?.showModal();
|
||||
},
|
||||
close,
|
||||
});
|
||||
|
||||
useEventListener(panel, "close", () => (open.value = false));
|
||||
|
||||
@@ -5,7 +5,6 @@ export function useDownloadUrl(
|
||||
containers: Ref<Container[]> | ComputedRef<Container[]>,
|
||||
streamConfig: { stdout: boolean; stderr: boolean } | Ref<{ stdout: boolean; stderr: boolean }>,
|
||||
levels: Ref<Set<string>>,
|
||||
name?: Ref<string> | ComputedRef<string> | string,
|
||||
) {
|
||||
const { debouncedSearchFilter } = useSearchFilter();
|
||||
|
||||
@@ -28,11 +27,6 @@ export function useDownloadUrl(
|
||||
selectedLevels.forEach((level) => params.append("levels", level));
|
||||
}
|
||||
|
||||
const nameValue = toValue(name);
|
||||
if (nameValue) {
|
||||
params.append("name", nameValue);
|
||||
}
|
||||
|
||||
const containerIds = toValue(containers)
|
||||
.map((c) => c.host + "~" + c.id)
|
||||
.join(",");
|
||||
|
||||
@@ -260,12 +260,7 @@ export async function loadBetween(
|
||||
params: Ref<URLSearchParams>,
|
||||
from: Date,
|
||||
to: Date,
|
||||
{
|
||||
lastSeenId,
|
||||
startId,
|
||||
min,
|
||||
maxStart,
|
||||
}: { lastSeenId?: number; startId?: number; min?: number; maxStart?: number } = {},
|
||||
{ lastSeenId, min, maxStart }: { lastSeenId?: number; min?: number; maxStart?: number } = {},
|
||||
) {
|
||||
const url = computed(() => `/api/hosts/${container.value.host}/containers/${container.value.id}/logs`);
|
||||
const abortController = new AbortController();
|
||||
@@ -284,9 +279,6 @@ export async function loadBetween(
|
||||
if (lastSeenId) {
|
||||
loadMoreParams.append("lastSeenId", String(lastSeenId));
|
||||
}
|
||||
if (startId) {
|
||||
loadMoreParams.append("startId", String(startId));
|
||||
}
|
||||
return withBase(`${url.value}?${loadMoreParams.toString()}`);
|
||||
});
|
||||
const stopWatcher = watchOnce(urlWithMoreParams, () => abortController.abort("stream changed"));
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import type { Completion } from "@codemirror/autocomplete";
|
||||
|
||||
export interface ExprEditorOptions {
|
||||
parent: HTMLElement;
|
||||
placeholder: string;
|
||||
initialValue: string;
|
||||
getHints: () => Completion[];
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
// Common operators for expr language
|
||||
const exprOperators: Completion[] = [
|
||||
{ label: "==", detail: "equals", type: "operator" },
|
||||
{ label: "!=", detail: "not equals", type: "operator" },
|
||||
{ label: "contains", detail: "string contains", type: "keyword" },
|
||||
{ label: "startsWith", detail: "string starts with", type: "keyword" },
|
||||
{ label: "endsWith", detail: "string ends with", type: "keyword" },
|
||||
{ label: "matches", detail: "regex match", type: "keyword" },
|
||||
{ label: "&&", detail: "logical AND", type: "operator" },
|
||||
{ label: "||", detail: "logical OR", type: "operator" },
|
||||
{ label: "!", detail: "logical NOT", type: "operator" },
|
||||
{ label: "in", detail: "membership test", type: "keyword" },
|
||||
{ label: "not in", detail: "negative membership", type: "keyword" },
|
||||
];
|
||||
|
||||
export function createContainerHints(
|
||||
containerNames: string[],
|
||||
imageNames: string[],
|
||||
hostNames: string[],
|
||||
): Completion[] {
|
||||
return [
|
||||
{ label: "name", detail: "container name", type: "property" },
|
||||
{ label: "id", detail: "container ID", type: "property" },
|
||||
{ label: "image", detail: "container image", type: "property" },
|
||||
{ label: "state", detail: "running, exited, etc.", type: "property" },
|
||||
{ label: "health", detail: "healthy, unhealthy, none", type: "property" },
|
||||
{ label: "host", detail: "docker host", type: "property" },
|
||||
{ label: "labels", detail: "container labels map", type: "property" },
|
||||
...exprOperators,
|
||||
{ label: '"running"', detail: "state value", type: "string" },
|
||||
{ label: '"exited"', detail: "state value", type: "string" },
|
||||
{ label: '"created"', detail: "state value", type: "string" },
|
||||
{ label: '"paused"', detail: "state value", type: "string" },
|
||||
{ label: '"healthy"', detail: "health value", type: "string" },
|
||||
{ label: '"unhealthy"', detail: "health value", type: "string" },
|
||||
{ label: '"none"', detail: "health value", type: "string" },
|
||||
...containerNames.map((name) => ({ label: `"${name}"`, detail: "container name", type: "string" }) as Completion),
|
||||
...imageNames.map((image) => ({ label: `"${image}"`, detail: "image name", type: "string" }) as Completion),
|
||||
...hostNames.map((host) => ({ label: `"${host}"`, detail: "host name", type: "string" }) as Completion),
|
||||
];
|
||||
}
|
||||
|
||||
export function createLogHints(): Completion[] {
|
||||
return [
|
||||
{ label: "message", detail: "log message content", type: "property" },
|
||||
{ label: "level", detail: "log level", type: "property" },
|
||||
{ label: "stream", detail: "stdout or stderr", type: "property" },
|
||||
{ label: "type", detail: "log type", type: "property" },
|
||||
{ label: "timestamp", detail: "unix timestamp", type: "property" },
|
||||
{ label: "id", detail: "log entry ID", type: "property" },
|
||||
...exprOperators,
|
||||
{ label: '"error"', detail: "level value", type: "string" },
|
||||
{ label: '"warn"', detail: "level value", type: "string" },
|
||||
{ label: '"info"', detail: "level value", type: "string" },
|
||||
{ label: '"debug"', detail: "level value", type: "string" },
|
||||
{ label: '"trace"', detail: "level value", type: "string" },
|
||||
{ label: '"stdout"', detail: "stream value", type: "string" },
|
||||
{ label: '"stderr"', detail: "stream value", type: "string" },
|
||||
{ label: 'level == "error"', detail: "match error logs", type: "text", boost: 10 },
|
||||
{ label: 'message contains ""', detail: "search in message", type: "text", boost: 10 },
|
||||
{ label: 'stream == "stderr"', detail: "match stderr", type: "text", boost: 10 },
|
||||
];
|
||||
}
|
||||
|
||||
function createAutocomplete(getHints: () => Completion[]) {
|
||||
return (context: any) => {
|
||||
const word = context.matchBefore(/[\w"=!&|]+/);
|
||||
if (!word && !context.explicit) return null;
|
||||
|
||||
const currentWord = word ? word.text.toLowerCase() : "";
|
||||
const hints = getHints();
|
||||
const filtered = currentWord ? hints.filter((h) => h.label.toLowerCase().includes(currentWord)) : hints;
|
||||
|
||||
return { from: word ? word.from : context.pos, options: filtered };
|
||||
};
|
||||
}
|
||||
|
||||
export async function createExprEditor(options: ExprEditorOptions) {
|
||||
const [
|
||||
{ EditorView, keymap, placeholder },
|
||||
{ EditorState },
|
||||
{ autocompletion, completionKeymap },
|
||||
{ HighlightStyle, syntaxHighlighting },
|
||||
{ tags },
|
||||
] = await Promise.all([
|
||||
import("@codemirror/view"),
|
||||
import("@codemirror/state"),
|
||||
import("@codemirror/autocomplete"),
|
||||
import("@codemirror/language"),
|
||||
import("@lezer/highlight"),
|
||||
]);
|
||||
|
||||
const editorTheme = EditorView.theme({
|
||||
"&": {
|
||||
backgroundColor: "var(--color-base-100)",
|
||||
color: "var(--color-base-content)",
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: "var(--color-primary)",
|
||||
},
|
||||
".cm-cursor": {
|
||||
borderLeftColor: "var(--color-primary)",
|
||||
},
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
|
||||
backgroundColor: "var(--color-base-300)",
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: "color-mix(in oklch, var(--color-base-200) 50%, transparent)",
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "var(--color-base-200)",
|
||||
color: "color-mix(in oklch, var(--color-base-content) 50%, transparent)",
|
||||
border: "none",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "var(--color-base-300)",
|
||||
},
|
||||
});
|
||||
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: "var(--color-primary)" },
|
||||
{ tag: tags.operator, color: "var(--color-secondary)" },
|
||||
{ tag: tags.string, color: "var(--color-success)" },
|
||||
{ tag: tags.number, color: "var(--color-warning)" },
|
||||
{ tag: tags.bool, color: "var(--color-warning)" },
|
||||
{ tag: tags.propertyName, color: "var(--color-info)" },
|
||||
{ tag: tags.variableName, color: "var(--color-base-content)" },
|
||||
{
|
||||
tag: tags.comment,
|
||||
color: "color-mix(in oklch, var(--color-base-content) 50%, transparent)",
|
||||
fontStyle: "italic",
|
||||
},
|
||||
]);
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: options.initialValue,
|
||||
extensions: [
|
||||
EditorView.lineWrapping,
|
||||
placeholder(options.placeholder),
|
||||
autocompletion({
|
||||
override: [createAutocomplete(options.getHints)],
|
||||
activateOnTyping: true,
|
||||
}),
|
||||
keymap.of(completionKeymap),
|
||||
editorTheme,
|
||||
syntaxHighlighting(highlightStyle),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && options.onChange) {
|
||||
options.onChange(update.view.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return new EditorView({ state, parent: options.parent });
|
||||
}
|
||||
@@ -96,7 +96,6 @@ export function useHistoricalContainerLog(historicalContainer: Ref<HistoricalCon
|
||||
const item = messages.value.at(-2)!;
|
||||
const { logs, signal } = await loadBetween(container, params, item.date, new Date(), {
|
||||
maxStart: 100,
|
||||
startId: item.id,
|
||||
});
|
||||
|
||||
if (signal.aborted) {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
export interface TemplateEditorOptions {
|
||||
parent: HTMLElement;
|
||||
initialValue: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export async function createTemplateEditor(options: TemplateEditorOptions) {
|
||||
const [{ EditorView }, { EditorState }, { json }, { HighlightStyle, syntaxHighlighting }, { tags }] =
|
||||
await Promise.all([
|
||||
import("@codemirror/view"),
|
||||
import("@codemirror/state"),
|
||||
import("@codemirror/lang-json"),
|
||||
import("@codemirror/language"),
|
||||
import("@lezer/highlight"),
|
||||
]);
|
||||
|
||||
const editorTheme = EditorView.theme({
|
||||
"&": {
|
||||
backgroundColor: "var(--color-base-100)",
|
||||
color: "var(--color-base-content)",
|
||||
fontSize: "0.875rem",
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: "var(--color-primary)",
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
},
|
||||
".cm-cursor": {
|
||||
borderLeftColor: "var(--color-primary)",
|
||||
},
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
|
||||
backgroundColor: "var(--color-base-300)",
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: "color-mix(in oklch, var(--color-base-200) 50%, transparent)",
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "var(--color-base-200)",
|
||||
color: "color-mix(in oklch, var(--color-base-content) 50%, transparent)",
|
||||
border: "none",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "var(--color-base-300)",
|
||||
},
|
||||
});
|
||||
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.propertyName, color: "var(--color-info)" },
|
||||
{ tag: tags.string, color: "var(--color-success)" },
|
||||
{ tag: tags.number, color: "var(--color-warning)" },
|
||||
{ tag: tags.bool, color: "var(--color-warning)" },
|
||||
{ tag: tags.null, color: "var(--color-secondary)" },
|
||||
{ tag: tags.punctuation, color: "var(--color-base-content)" },
|
||||
]);
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: options.initialValue,
|
||||
extensions: [
|
||||
EditorView.lineWrapping,
|
||||
json(),
|
||||
editorTheme,
|
||||
syntaxHighlighting(highlightStyle),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && options.onChange) {
|
||||
options.onChange(update.view.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return new EditorView({ state, parent: options.parent });
|
||||
}
|
||||
@@ -3,7 +3,7 @@ type Toast = {
|
||||
createdAt: Date;
|
||||
title?: string;
|
||||
message: string;
|
||||
type: "error" | "warning" | "info";
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
action?: {
|
||||
label: string;
|
||||
handler: () => void;
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<SideDrawer ref="drawer" :width="drawerWidth" v-slot="{ close }">
|
||||
<SideDrawer ref="drawer" :width="drawerWidth">
|
||||
<Suspense :timeout="0">
|
||||
<component :is="drawerComponent" v-bind="drawerProperties" :close="close" />
|
||||
<template #fallback> <span class="loading loading-spinner loading-sm"></span></template>
|
||||
<component :is="drawerComponent" v-bind="drawerProperties" />
|
||||
<template #fallback> Loading dependencies... </template>
|
||||
</Suspense>
|
||||
</SideDrawer>
|
||||
<ToastModal />
|
||||
|
||||
@@ -151,44 +151,3 @@ body {
|
||||
.splitpanes--vertical .splitpanes__pane {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* CodeMirror autocomplete tooltip styles */
|
||||
.cm-tooltip {
|
||||
@apply bg-base-200! border-base-content/40! min-w-96 rounded-sm border shadow-md;
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete ul {
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete ul li {
|
||||
@apply my-1 px-2;
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete ul li[aria-selected] {
|
||||
@apply bg-primary/20 text-base-content!;
|
||||
}
|
||||
|
||||
.cm-completionLabel {
|
||||
@apply text-base-content!;
|
||||
}
|
||||
|
||||
.cm-completionDetail {
|
||||
@apply text-base-content/60! ml-2 italic;
|
||||
}
|
||||
|
||||
.cm-completionMatchedText {
|
||||
@apply text-primary! font-bold no-underline;
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply mr-2 opacity-70;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
+11
-55
@@ -1,4 +1,5 @@
|
||||
import type { ContainerHealth, ContainerJson, ContainerStat, ContainerState } from "@/types/Container";
|
||||
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
|
||||
import { useExponentialMovingAverage, useSimpleRefHistory } from "@/utils";
|
||||
import { Ref } from "vue";
|
||||
|
||||
export type Stat = Omit<ContainerStat, "id">;
|
||||
@@ -50,12 +51,13 @@ export class Container {
|
||||
public readonly group?: string,
|
||||
public health?: ContainerHealth,
|
||||
) {
|
||||
const defaultStat = { cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 } as Stat;
|
||||
this._stat = ref(stats.at(-1) || defaultStat);
|
||||
const recentStats = stats.slice(-300);
|
||||
const padding = Array(300 - recentStats.length).fill(defaultStat);
|
||||
this._statsHistory = ref([...padding, ...recentStats]);
|
||||
this.movingAverageStat = ref(stats.at(-1) || defaultStat);
|
||||
this._stat = ref(
|
||||
stats.at(-1) || ({ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 } as Stat),
|
||||
);
|
||||
const { history } = useSimpleRefHistory(this._stat, { capacity: 300, deep: true, initial: stats });
|
||||
this._statsHistory = history;
|
||||
const { movingAverage } = useExponentialMovingAverage(this._stat, 0.2);
|
||||
this.movingAverageStat = movingAverage;
|
||||
|
||||
this._name = name;
|
||||
}
|
||||
@@ -83,7 +85,6 @@ export class Container {
|
||||
get namespace() {
|
||||
return (
|
||||
this.labels["dev.dozzle.group"] ||
|
||||
this.labels["coolify.projectName"] ||
|
||||
this.labels["com.docker.stack.namespace"] ||
|
||||
this.labels["com.docker.compose.project"]
|
||||
);
|
||||
@@ -114,56 +115,11 @@ export class Container {
|
||||
}
|
||||
|
||||
public updateStat(stat: Stat) {
|
||||
// When Container is inside a reactive array, refs get unwrapped
|
||||
if (isRef(this._stat)) {
|
||||
this._stat.value = stat;
|
||||
} else {
|
||||
(this._stat as unknown as Stat) = stat;
|
||||
// @ts-ignore
|
||||
this._stat = stat;
|
||||
}
|
||||
|
||||
// Update history directly (no watcher needed)
|
||||
const history = isRef(this._statsHistory) ? this._statsHistory.value : (this._statsHistory as unknown as Stat[]);
|
||||
history.push(stat);
|
||||
if (history.length > 300) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
// Calculate EMA directly (no watcher needed)
|
||||
const alpha = 0.2;
|
||||
const prev = isRef(this.movingAverageStat)
|
||||
? this.movingAverageStat.value
|
||||
: (this.movingAverageStat as unknown as Stat);
|
||||
const newEma = {
|
||||
cpu: alpha * stat.cpu + (1 - alpha) * prev.cpu,
|
||||
memory: alpha * stat.memory + (1 - alpha) * prev.memory,
|
||||
memoryUsage: alpha * stat.memoryUsage + (1 - alpha) * prev.memoryUsage,
|
||||
networkRxTotal: stat.networkRxTotal,
|
||||
networkTxTotal: stat.networkTxTotal,
|
||||
};
|
||||
if (isRef(this.movingAverageStat)) {
|
||||
this.movingAverageStat.value = newEma;
|
||||
} else {
|
||||
(this.movingAverageStat as unknown as Stat) = newEma;
|
||||
}
|
||||
}
|
||||
|
||||
static fromJSON(c: ContainerJson): Container {
|
||||
return new Container(
|
||||
c.id,
|
||||
new Date(c.created),
|
||||
new Date(c.startedAt),
|
||||
new Date(c.finishedAt),
|
||||
c.image,
|
||||
c.name,
|
||||
c.command,
|
||||
c.host,
|
||||
c.labels,
|
||||
c.state,
|
||||
c.cpuLimit,
|
||||
c.memoryLimit,
|
||||
c.stats ?? [],
|
||||
c.group,
|
||||
c.health,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<template>
|
||||
<PageWithLinks>
|
||||
<section>
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold">{{ $t("notifications.title") }}</h2>
|
||||
<p class="text-base-content/60">{{ $t("notifications.description") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Destinations Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-base-content/60 mb-4 font-semibold tracking-wide uppercase">
|
||||
{{ $t("notifications.destinations") }}
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<DestinationCard
|
||||
v-for="dest in dispatchers"
|
||||
:key="dest.id"
|
||||
:destination="dest"
|
||||
:on-updated="fetchAll"
|
||||
:existing-dispatchers="dispatchers"
|
||||
class="w-full md:w-72"
|
||||
/>
|
||||
<!-- Add Destination Card -->
|
||||
<button
|
||||
class="card card-border border-base-content/30 hover:border-base-content/50 w-full cursor-pointer border-dashed transition-colors md:w-72"
|
||||
@click="openAddDestination"
|
||||
>
|
||||
<div class="card-body items-center justify-center gap-1 p-4">
|
||||
<mdi:plus class="text-2xl" />
|
||||
<span class="text-base-content/60 text-sm">{{ $t("notifications.add-destination") }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts Section -->
|
||||
<div>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-base-content/60 font-semibold tracking-wide uppercase">{{ $t("notifications.alerts") }}</h3>
|
||||
<button class="btn btn-primary btn-sm" @click="openCreateAlert">
|
||||
<mdi:plus />
|
||||
{{ $t("notifications.add") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Tabs -->
|
||||
<div class="tabs tabs-box mb-6">
|
||||
<button class="tab" :class="{ 'tab-active': filter === 'all' }" @click="filter = 'all'">
|
||||
{{ $t("notifications.filter.all", { count: alerts.length }) }}
|
||||
</button>
|
||||
<button class="tab" :class="{ 'tab-active': filter === 'enabled' }" @click="filter = 'enabled'">
|
||||
{{ $t("notifications.filter.enabled", { count: enabledCount }) }}
|
||||
</button>
|
||||
<button class="tab" :class="{ 'tab-active': filter === 'paused' }" @click="filter = 'paused'">
|
||||
{{ $t("notifications.filter.paused", { count: pausedCount }) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alerts List -->
|
||||
<div v-if="!alerts.length" class="text-base-content/60 py-4">
|
||||
{{ $t("notifications.no-alerts") }}
|
||||
</div>
|
||||
<div v-else class="space-y-4">
|
||||
<AlertCard v-for="alert in filteredAlerts" :key="alert.id" :alert="alert" :on-updated="fetchAlerts" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageWithLinks>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationRule, Dispatcher } from "@/types/notifications";
|
||||
import AlertForm from "@/components/Notification/AlertForm.vue";
|
||||
import DestinationForm from "@/components/Notification/DestinationForm.vue";
|
||||
import DestinationCard from "@/components/Notification/DestinationCard.vue";
|
||||
|
||||
const showDrawer = useDrawer();
|
||||
const router = useRouter();
|
||||
|
||||
// State
|
||||
const alerts = ref<NotificationRule[]>([]);
|
||||
const dispatchers = ref<Dispatcher[]>([]);
|
||||
|
||||
async function fetchAlerts() {
|
||||
const res = await fetch(withBase("/api/notifications/rules"));
|
||||
alerts.value = await res.json();
|
||||
}
|
||||
|
||||
async function fetchDispatchers() {
|
||||
const res = await fetch(withBase("/api/notifications/dispatchers"));
|
||||
dispatchers.value = await res.json();
|
||||
}
|
||||
|
||||
async function fetchAll() {
|
||||
await Promise.all([fetchAlerts(), fetchDispatchers()]);
|
||||
}
|
||||
|
||||
// Handle cloudLinkSuccess hash param
|
||||
onMounted(async () => {
|
||||
await fetchAll();
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith("#cloudLinkSuccess=")) {
|
||||
const id = Number(hash.replace("#cloudLinkSuccess=", ""));
|
||||
if (!isNaN(id)) {
|
||||
const destination = dispatchers.value.find((d) => d.id === id);
|
||||
if (destination) {
|
||||
showDrawer(
|
||||
DestinationForm,
|
||||
{
|
||||
destination,
|
||||
existingDispatchers: dispatchers.value,
|
||||
showLinkSuccess: true,
|
||||
},
|
||||
"md",
|
||||
);
|
||||
}
|
||||
}
|
||||
router.replace({ hash: "" });
|
||||
}
|
||||
});
|
||||
|
||||
// Local state
|
||||
const filter = ref<"all" | "enabled" | "paused">("all");
|
||||
|
||||
const enabledCount = computed(() => alerts.value.filter((a) => a.enabled).length);
|
||||
const pausedCount = computed(() => alerts.value.filter((a) => !a.enabled).length);
|
||||
|
||||
const filteredAlerts = computed(() => {
|
||||
if (filter.value === "enabled") return alerts.value.filter((a) => a.enabled);
|
||||
if (filter.value === "paused") return alerts.value.filter((a) => !a.enabled);
|
||||
return alerts.value;
|
||||
});
|
||||
|
||||
function openCreateAlert() {
|
||||
showDrawer(AlertForm, { onCreated: fetchAlerts }, "lg");
|
||||
}
|
||||
|
||||
function openAddDestination() {
|
||||
showDrawer(
|
||||
DestinationForm,
|
||||
{
|
||||
onCreated: fetchDispatchers,
|
||||
existingDispatchers: dispatchers.value,
|
||||
},
|
||||
"md",
|
||||
);
|
||||
}
|
||||
</script>
|
||||
Vendored
-2
@@ -4,5 +4,3 @@ declare module "*.vue" {
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare const __CLOUD_URL__: string;
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
interface Release {
|
||||
type Announcement = {
|
||||
name: string;
|
||||
mentionsCount: number;
|
||||
tag: string;
|
||||
announcement: boolean;
|
||||
createdAt: Date;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
tag: string;
|
||||
htmlUrl: string;
|
||||
latest: boolean;
|
||||
mentionsCount: number;
|
||||
features: number;
|
||||
bugFixes: number;
|
||||
breaking: number;
|
||||
}
|
||||
|
||||
type Announcement = Omit<Release, "createdAt"> & {
|
||||
announcement: boolean;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
const releases = ref<Announcement[]>([]);
|
||||
@@ -24,14 +20,8 @@ async function fetchReleases() {
|
||||
fetched = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(withBase("/api/releases"));
|
||||
const data: Release[] = await res.json();
|
||||
releases.value =
|
||||
data?.map((r) => ({
|
||||
...r,
|
||||
createdAt: new Date(r.createdAt),
|
||||
announcement: false,
|
||||
})) || [];
|
||||
const { data } = await useFetch(withBase("/api/releases")).get().json<Announcement[]>();
|
||||
releases.value = data.value || [];
|
||||
} catch (error) {
|
||||
console.error("Error while fetching releases:\n", error);
|
||||
fetched = false;
|
||||
@@ -45,7 +35,10 @@ if (config.releaseCheckMode === "automatic") {
|
||||
const otherAnnouncements = [] as Announcement[];
|
||||
|
||||
const announcements = computed(() => {
|
||||
return [...releases.value, ...otherAnnouncements].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
const newReleases =
|
||||
releases.value?.map((release) => ({ ...release, createdAt: new Date(release.createdAt), announcement: false })) ??
|
||||
[];
|
||||
return [...newReleases, ...otherAnnouncements].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
});
|
||||
|
||||
const mostRecent = computed(() => announcements.value?.[0]);
|
||||
|
||||
@@ -140,7 +140,28 @@ export const useContainerStore = defineStore("container", () => {
|
||||
existing.name = c.name;
|
||||
});
|
||||
|
||||
containers.value = [...containers.value, ...newContainers.map(Container.fromJSON)];
|
||||
containers.value = [
|
||||
...containers.value,
|
||||
...newContainers.map((c) => {
|
||||
return new Container(
|
||||
c.id,
|
||||
new Date(c.created),
|
||||
new Date(c.startedAt),
|
||||
new Date(c.finishedAt),
|
||||
c.image,
|
||||
c.name,
|
||||
c.command,
|
||||
c.host,
|
||||
c.labels,
|
||||
c.state,
|
||||
c.cpuLimit,
|
||||
c.memoryLimit,
|
||||
c.stats,
|
||||
c.group,
|
||||
c.health,
|
||||
);
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
|
||||
|
||||
Vendored
+13
-31
@@ -1,29 +1,26 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// oxfmt-ignore
|
||||
// @ts-nocheck
|
||||
// noinspection ES6UnusedImports
|
||||
// Generated by vue-router. !! DO NOT MODIFY THIS FILE !!
|
||||
// Generated by unplugin-vue-router. !! DO NOT MODIFY THIS FILE !!
|
||||
// It's recommended to commit this file.
|
||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||
|
||||
import type {
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'vue-router'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface TypesConfig {
|
||||
ParamParsers: never
|
||||
}
|
||||
declare module 'vue-router/auto-resolver' {
|
||||
export type ParamParserCustom = never
|
||||
}
|
||||
|
||||
declare module 'vue-router/auto-routes' {
|
||||
import type {
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'vue-router'
|
||||
|
||||
/**
|
||||
* Route name map generated by vue-router
|
||||
* Route name map generated by unplugin-vue-router
|
||||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<
|
||||
@@ -89,13 +86,6 @@ declare module 'vue-router/auto-routes' {
|
||||
{ name: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/notifications': RouteRecordInfo<
|
||||
'/notifications',
|
||||
'/notifications',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/owner/[name]': RouteRecordInfo<
|
||||
'/owner/[name]',
|
||||
'/owner/:name',
|
||||
@@ -134,7 +124,7 @@ declare module 'vue-router/auto-routes' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Route file to route info map by vue-router.
|
||||
* Route file to route info map by unplugin-vue-router.
|
||||
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
|
||||
*
|
||||
* Each key is a file path relative to the project root with 2 properties:
|
||||
@@ -198,12 +188,6 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'assets/pages/notifications.vue': {
|
||||
routes:
|
||||
| '/notifications'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'assets/pages/owner/[name].vue': {
|
||||
routes:
|
||||
| '/owner/[name]'
|
||||
@@ -247,5 +231,3 @@ declare module 'vue-router/auto-routes' {
|
||||
? Info['routes']
|
||||
: keyof RouteNamedMap
|
||||
}
|
||||
|
||||
export {}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
export interface NotificationRule {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
containerExpression: string;
|
||||
logExpression: string;
|
||||
triggerCount: number;
|
||||
triggeredContainers: number;
|
||||
lastTriggeredAt: string | null;
|
||||
dispatcher: Dispatcher | null;
|
||||
}
|
||||
|
||||
export interface Dispatcher {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
url?: string;
|
||||
template?: string;
|
||||
prefix?: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export interface NotificationRuleInput {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
dispatcherId: number;
|
||||
logExpression: string;
|
||||
containerExpression: string;
|
||||
}
|
||||
|
||||
export interface PreviewResult {
|
||||
containerError?: string;
|
||||
logError?: string;
|
||||
matchedContainers: {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
host: string;
|
||||
}[];
|
||||
matchedLogs: {
|
||||
id: number;
|
||||
t: string;
|
||||
m: unknown;
|
||||
rm: string;
|
||||
ts: number;
|
||||
l: string;
|
||||
s: string;
|
||||
}[];
|
||||
totalLogs: number;
|
||||
}
|
||||
|
||||
export interface TestWebhookResult {
|
||||
success: boolean;
|
||||
statusCode?: number;
|
||||
error?: string;
|
||||
}
|
||||
+1
-1
@@ -95,7 +95,7 @@ services:
|
||||
|
||||
playwright:
|
||||
container_name: playwright
|
||||
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
@@ -37,7 +37,6 @@ export default defineConfig({
|
||||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "Guide", link: "/guide/what-is-dozzle", activeMatch: "/guide/" },
|
||||
{ text: "Dozzle Cloud", link: "https://cloud.dozzle.dev" },
|
||||
{
|
||||
text: `v${pkg.version}`,
|
||||
items: [
|
||||
@@ -69,13 +68,6 @@ export default defineConfig({
|
||||
{ text: "Podman", link: "/guide/podman" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Notifications",
|
||||
items: [
|
||||
{ text: "Alerts & Webhooks", link: "/guide/alerts-and-webhooks" },
|
||||
{ text: "Dozzle Cloud", link: "/guide/dozzle-cloud" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Advanced Configuration",
|
||||
items: [
|
||||
|
||||
@@ -3,8 +3,15 @@
|
||||
class="banner fixed top-0 right-0 left-0 z-(--vp-z-index-layout-top) flex items-center overflow-hidden bg-[oklch(74%_0.16_232.661)] p-4 font-bold text-gray-800 dark:bg-[oklch(60%_0.126_221.723)] dark:text-white"
|
||||
>
|
||||
<div class="mx-auto flex items-center gap-2 lg:gap-4">
|
||||
<span class="animate-bounce">🎉</span>Dozzle v10 is here! Alerts, webhooks, and Dozzle Cloud are now available.
|
||||
<a href="/guide/alerts-and-webhooks" class="btn btn-sm btn-primary text-white!"> Learn more </a>
|
||||
<span class="animate-bounce">🚀</span>K8s users, I need your help! I have added K8s support to Dozzle and I need
|
||||
your feedback! 🙏
|
||||
<a
|
||||
href="https://github.com/amir20/dozzle/discussions/3614"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-primary text-white!"
|
||||
>
|
||||
See the discussion
|
||||
</a>
|
||||
</div>
|
||||
<button class="btn btn-circle btn-ghost btn-xs ml-auto" @click="dismiss">
|
||||
<svg class="swap-on fill-current" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512">
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
"home-hero-image": () => h(HeroVideo),
|
||||
"sidebar-nav-after": () => h(BuyMeCoffee),
|
||||
"home-hero-actions-after": () => h(Stats),
|
||||
"layout-top": () => h(Banner),
|
||||
// "layout-top": () => h(Banner),
|
||||
"home-hero-after": () => h(Supported),
|
||||
});
|
||||
},
|
||||
|
||||
+2
-5
@@ -49,7 +49,7 @@ To connect to an agent, you need to provide the agent's IP address and port. Her
|
||||
::: code-group
|
||||
|
||||
```sh
|
||||
docker run -p 8080:8080 amir20/dozzle:latest --remote-agent agent:7007
|
||||
docker run -p 8080:8080 amir20/dozzle:latest --remote-agent agent-ip:7007
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
@@ -64,10 +64,7 @@ services:
|
||||
|
||||
:::
|
||||
|
||||
Note that it is not necessary to mount the local Docker socket when connecting to agents, in which case the UI will only show the containers that are available on the agents.
|
||||
|
||||
> [!TIP]
|
||||
> If you want to include the host containers in the UI as well, mount the `docker.sock` socket as shown in the [getting started](/guide/getting-started) example.
|
||||
Note that when connecting remotely, you don't need to mount local Docker socket. The UI will only show the containers that are available on the agent.
|
||||
|
||||
> [!TIP]
|
||||
> You can connect to multiple agents by providing multiple `DOZZLE_REMOTE_AGENT` environment variables. For example, `DOZZLE_REMOTE_AGENT=agent1:7007,agent2:7007`.
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
---
|
||||
title: Alerts & Webhooks
|
||||
---
|
||||
|
||||
# Alerts & Webhooks
|
||||
|
||||
<Badge type="tip" text="New in v10" />
|
||||
|
||||
Dozzle v10 introduces a powerful alerting system that lets you monitor container logs and receive notifications when specific conditions are met. Alerts use customizable expressions to filter containers and log messages, and can send notifications to webhooks, Slack, Discord, ntfy, or [Dozzle Cloud](/guide/dozzle-cloud).
|
||||
|
||||
## How It Works
|
||||
|
||||
Alerts are configured with two expressions:
|
||||
|
||||
1. **Container filter** — selects which containers to monitor
|
||||
2. **Log filter** — defines which log messages trigger the alert
|
||||
|
||||
When a log entry matches both filters, Dozzle sends a notification to the configured destination.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Alert and destination configurations are stored in the `/data` directory. You must mount this directory as a volume to persist your notification settings across container restarts.
|
||||
|
||||
::: code-group
|
||||
|
||||
```sh
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/data:/data -p 8080:8080 amir20/dozzle:latest
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /path/to/data:/data
|
||||
ports:
|
||||
- 8080:8080
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Setting Up a Destination
|
||||
|
||||
Before creating alerts, you need to configure at least one notification destination. Navigate to the **Notifications** page in Dozzle and click **Add Destination**.
|
||||
|
||||
### Webhook
|
||||
|
||||
Webhooks send an HTTP POST request to a URL of your choice. Dozzle includes built-in payload templates for popular services:
|
||||
|
||||
- **Slack** — formatted with blocks and markdown
|
||||
- **Discord** — formatted for Discord webhook API
|
||||
- **ntfy** — formatted for [ntfy.sh](https://ntfy.sh) push notifications
|
||||
- **Custom** — generic JSON payload you can customize
|
||||
|
||||
You can also write your own payload template using Go's `text/template` syntax. The following variables are available:
|
||||
|
||||
<div v-pre>
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------- | --------------------------- |
|
||||
| `{{.Container.Name}}` | Container name |
|
||||
| `{{.Container.Image}}` | Container image |
|
||||
| `{{.Container.HostName}}` | Docker host name |
|
||||
| `{{.Container.State}}` | Container state |
|
||||
| `{{.Log.Message}}` | Log message content |
|
||||
| `{{.Log.Level}}` | Log level |
|
||||
| `{{.Log.Timestamp}}` | Log timestamp |
|
||||
| `{{.Log.Stream}}` | Stream type (stdout/stderr) |
|
||||
| `{{.Subscription.Name}}` | Alert rule name |
|
||||
|
||||
</div>
|
||||
|
||||
> [!TIP]
|
||||
> Use the **Test** button to verify your webhook is working before saving.
|
||||
|
||||
### Dozzle Cloud
|
||||
|
||||
You can also send alerts to [Dozzle Cloud](/guide/dozzle-cloud) for centralized monitoring across multiple Dozzle instances. See the [Dozzle Cloud guide](/guide/dozzle-cloud) for more details.
|
||||
|
||||
## Creating an Alert
|
||||
|
||||
Navigate to the **Notifications** page and click **Add Alert**. You'll need to configure:
|
||||
|
||||
### Container Expression
|
||||
|
||||
The container expression selects which containers to monitor. Available properties:
|
||||
|
||||
| Property | Type | Example |
|
||||
| ---------- | ------ | ------------------------------- |
|
||||
| `name` | string | `name contains "api"` |
|
||||
| `image` | string | `image == "nginx:latest"` |
|
||||
| `state` | string | `state == "running"` |
|
||||
| `health` | string | `health == "unhealthy"` |
|
||||
| `hostName` | string | `hostName == "prod-host"` |
|
||||
| `labels` | map | `labels["env"] == "production"` |
|
||||
|
||||
You can combine conditions with `&&` (AND), `||` (OR), and `!` (NOT):
|
||||
|
||||
```
|
||||
name contains "api" && labels["env"] == "production"
|
||||
```
|
||||
|
||||
### Log Expression
|
||||
|
||||
The log expression filters which log messages trigger the alert. Available properties:
|
||||
|
||||
| Property | Type | Example |
|
||||
| --------- | ---------- | -------------------------- |
|
||||
| `message` | string/map | `message contains "error"` |
|
||||
| `level` | string | `level == "error"` |
|
||||
| `stream` | string | `stream == "stderr"` |
|
||||
| `type` | string | `type == "complex"` |
|
||||
|
||||
For JSON logs, you can access nested fields using dot notation:
|
||||
|
||||
```
|
||||
message.status >= 500 && message.path contains "/api"
|
||||
```
|
||||
|
||||
Supported string operators include `contains`, `startsWith`, `endsWith`, and `matches` (regex).
|
||||
|
||||
### Examples
|
||||
|
||||
**Alert on all errors from production containers:**
|
||||
|
||||
```
|
||||
Container: labels["env"] == "production"
|
||||
Log: level == "error"
|
||||
```
|
||||
|
||||
**Alert on HTTP 5xx errors from API containers:**
|
||||
|
||||
```
|
||||
Container: name contains "api"
|
||||
Log: message.status >= 500
|
||||
```
|
||||
|
||||
**Alert on any stderr output from a specific image:**
|
||||
|
||||
```
|
||||
Container: image startsWith "myapp/"
|
||||
Log: stream == "stderr"
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The alert editor includes autocomplete and real-time validation. You can preview matched containers and logs before saving.
|
||||
|
||||
## Managing Alerts
|
||||
|
||||
From the Notifications page, you can:
|
||||
|
||||
- **Enable/disable** alerts without deleting them
|
||||
- **Edit** alert expressions and destinations
|
||||
- **View statistics** including trigger count, matched containers, and last triggered time
|
||||
- **Delete** alerts that are no longer needed
|
||||
@@ -27,12 +27,3 @@ services:
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Coolify Integration
|
||||
|
||||
If you're using [Coolify](https://coolify.io/), Dozzle automatically recognizes Coolify's labels as fallbacks:
|
||||
|
||||
- `coolify.resourceName` → Used as container name if `dev.dozzle.name` is not set
|
||||
- `coolify.projectName` → Used for grouping if `dev.dozzle.group` is not set
|
||||
|
||||
No additional configuration is needed for Coolify deployments.
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
title: Dozzle Cloud
|
||||
---
|
||||
|
||||
# Dozzle Cloud
|
||||
|
||||
<Badge type="tip" text="New in v10" />
|
||||
|
||||
[Dozzle Cloud](https://cloud.dozzle.dev) is a companion service that extends your self-hosted Dozzle instances with centralized monitoring, smart alerting, and log intelligence. While Dozzle remains fully open source and self-hosted, Dozzle Cloud adds a managed layer on top for teams that need more visibility across their infrastructure.
|
||||
|
||||
## Why Dozzle Cloud?
|
||||
|
||||
Container logs are noisy. Dozzle Cloud helps you cut through the noise with intelligent summarization and multi-instance aggregation — without requiring additional agents or complex setup.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Log Summaries
|
||||
|
||||
Dozzle Cloud automatically batches related container events into concise summaries. Each summary includes severity levels, source container information, and direct links to the full logs in your Dozzle instance.
|
||||
|
||||
### Pattern Clustering
|
||||
|
||||
Instead of showing duplicate errors, Dozzle Cloud groups similar errors together and displays frequency counts. This makes it easy to identify recurring issues across multiple containers.
|
||||
|
||||
### Smart Alert Distribution
|
||||
|
||||
Receive notifications through multiple channels:
|
||||
|
||||
- **Email**
|
||||
- **Slack**
|
||||
- **ntfy**
|
||||
- **Webhooks**
|
||||
- **Browser push notifications**
|
||||
|
||||
You can enable or disable channels independently with unlimited configurations.
|
||||
|
||||
### Multi-Instance Dashboard
|
||||
|
||||
Monitor all your Dozzle servers from a single dashboard. Connecting is simple — just link your instance with an API key. No additional agents are required.
|
||||
|
||||
### Searchable Event History
|
||||
|
||||
Search across all logged events with full-text search. Filter by container, severity (error, warning, info), and keywords. Retention is configurable from 24 hours to 30 days.
|
||||
|
||||
### Security
|
||||
|
||||
- API keys are hashed with expiration support
|
||||
- GitHub OAuth authentication
|
||||
- Your logs remain your own — Dozzle Cloud is committed to data privacy
|
||||
|
||||
## Connecting to Dozzle Cloud
|
||||
|
||||
To link your Dozzle instance to Dozzle Cloud:
|
||||
|
||||
1. Navigate to the **Notifications** page in Dozzle
|
||||
2. Click **Add Destination** and select **Dozzle Cloud**
|
||||
3. Click **Link Dozzle Cloud** — you'll be redirected to authenticate
|
||||
4. Once linked, your API key is automatically configured
|
||||
|
||||
You can then select Dozzle Cloud as a destination when creating [alerts](/guide/alerts-and-webhooks).
|
||||
|
||||
## Pricing
|
||||
|
||||
Dozzle Cloud offers a free tier with 500 events per month — no credit card required. Visit [cloud.dozzle.dev](https://cloud.dozzle.dev) for more details.
|
||||
@@ -35,11 +35,6 @@ features:
|
||||
link: /guide/remote-hosts
|
||||
linkText: Learn More
|
||||
icon: 🌐
|
||||
- title: Alerts & Webhooks
|
||||
details: Set up customizable alerts with powerful expressions to monitor container logs and receive notifications via Slack, Discord, ntfy, or webhooks.
|
||||
icon: 🔔
|
||||
linkText: Learn More
|
||||
link: /guide/alerts-and-webhooks
|
||||
- title: SQL Engine
|
||||
details: Use SQL queries to analyze logs inside your browser with WebAssembly and DuckDB.
|
||||
icon: 📊
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
users:
|
||||
admin:
|
||||
name: Admin
|
||||
password: "$2y$11$pTGu6dnTT7Uh3ob7uC6X7OkAamlhHpJ0/mEbsmiPyO85pumillZme"
|
||||
password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Parse arguments
|
||||
USE_LOCAL=false
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--local)
|
||||
USE_LOCAL=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Configuration
|
||||
VM_NAME="${POSITIONAL_ARGS[0]:-dozzle-agent}"
|
||||
DISTRO="${POSITIONAL_ARGS[1]:-ubuntu}"
|
||||
AGENT_PORT="${POSITIONAL_ARGS[2]:-7007}"
|
||||
VM_NAME="${1:-dozzle-agent}"
|
||||
DISTRO="${2:-ubuntu}"
|
||||
AGENT_PORT="${3:-7007}"
|
||||
SHARED_CERT="./shared_cert.pem"
|
||||
SHARED_KEY="./shared_key.pem"
|
||||
DOZZLE_IMAGE="amir20/dozzle:latest"
|
||||
|
||||
if [ "$USE_LOCAL" = true ]; then
|
||||
DOZZLE_IMAGE="amir20/dozzle:local"
|
||||
fi
|
||||
|
||||
echo "🚀 Setting up Dozzle Agent on OrbStack VM: $VM_NAME"
|
||||
if [ "$USE_LOCAL" = true ]; then
|
||||
echo " Using locally built image"
|
||||
fi
|
||||
|
||||
# Verify shared certificates exist
|
||||
if [ ! -f "$SHARED_CERT" ]; then
|
||||
@@ -65,7 +40,10 @@ sleep 3
|
||||
|
||||
# Step 2: Install Docker in the VM
|
||||
echo "🐳 Installing Docker..."
|
||||
if ! orb exec -m "$VM_NAME" bash -c 'curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $(whoami)'; then
|
||||
if ! orb exec -m "$VM_NAME" bash -c '
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker $(whoami)
|
||||
'; then
|
||||
echo "❌ Docker installation failed"
|
||||
exit 1
|
||||
fi
|
||||
@@ -84,44 +62,17 @@ cat "$SHARED_KEY" | orb exec -m "$VM_NAME" bash -c 'cat > ~/dozzle-certs/shared_
|
||||
|
||||
echo "✅ Certificates copied"
|
||||
|
||||
# Step 4: Load or pull Dozzle image
|
||||
if [ "$USE_LOCAL" = true ]; then
|
||||
echo "🔨 Building local Docker image..."
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
if ! (cd "$PROJECT_ROOT" && make docker); then
|
||||
echo "❌ Failed to build Docker image"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Loading image into VM..."
|
||||
if ! docker save amir20/dozzle:local | orb exec -m "$VM_NAME" docker load; then
|
||||
echo "❌ Failed to load image into VM"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Local image loaded"
|
||||
else
|
||||
echo "📥 Pulling Dozzle image..."
|
||||
if ! orb exec -m "$VM_NAME" docker pull amir20/dozzle:latest; then
|
||||
echo "❌ Failed to pull image"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 5: Start Dozzle agent
|
||||
# Step 4: Start Dozzle agent
|
||||
echo "🎯 Starting Dozzle agent..."
|
||||
orb exec -m "$VM_NAME" bash -c 'mkdir -p ~/dozzle-data'
|
||||
if ! orb exec -m "$VM_NAME" bash -c "
|
||||
set -e
|
||||
docker pull amir20/dozzle:latest
|
||||
docker run -d --name dozzle-agent \
|
||||
--restart unless-stopped \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/dozzle-certs:/certs \
|
||||
-v ~/dozzle-data:/data \
|
||||
-p $AGENT_PORT:7007 \
|
||||
-e DOZZLE_LEVEL=debug \
|
||||
$DOZZLE_IMAGE agent \
|
||||
amir20/dozzle:latest agent \
|
||||
--cert /certs/shared_cert.pem \
|
||||
--key /certs/shared_key.pem
|
||||
"; then
|
||||
@@ -131,11 +82,11 @@ fi
|
||||
|
||||
echo "✅ Dozzle agent started"
|
||||
|
||||
# Step 6: Wait for agent to be ready
|
||||
# Step 5: Wait for agent to be ready
|
||||
echo "⏳ Waiting for agent to be ready..."
|
||||
sleep 3
|
||||
|
||||
# Step 7: Verify agent is running
|
||||
# Step 6: Verify agent is running
|
||||
echo "🧪 Verifying agent is running..."
|
||||
if orb exec -m "$VM_NAME" docker ps --filter name=dozzle-agent --format "{{.Status}}" | grep -q "Up"; then
|
||||
echo "✅ Agent is running"
|
||||
|
||||
@@ -8,6 +8,7 @@ require (
|
||||
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
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@@ -16,43 +17,37 @@ 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.49.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.11.0
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/expr-lang/expr v1.17.7
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
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.7.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/sync v0.19.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
k8s.io/api v0.35.1
|
||||
k8s.io/apimachinery v0.35.1
|
||||
k8s.io/client-go v0.35.1
|
||||
k8s.io/metrics v0.35.1
|
||||
k8s.io/api v0.35.0
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
k8s.io/metrics v0.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/air-verse/air v1.64.5 // indirect
|
||||
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bep/godartsass/v2 v2.5.0 // indirect
|
||||
github.com/bep/golibsass v1.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
@@ -62,21 +57,20 @@ require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/expr-lang/expr v1.17.7 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gohugoio/hugo v0.149.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
@@ -95,28 +89,24 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
@@ -125,14 +115,9 @@ require (
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
go 1.26.0
|
||||
|
||||
tool (
|
||||
github.com/air-verse/air
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc
|
||||
google.golang.org/protobuf/cmd/protoc-gen-go
|
||||
)
|
||||
go 1.25.5
|
||||
|
||||
@@ -1,74 +1,35 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||
github.com/air-verse/air v1.64.4 h1:P0alz5Jia5NucZew1HYZy69lzPWZHFjLTCKo0VhxME8=
|
||||
github.com/air-verse/air v1.64.4/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
|
||||
github.com/air-verse/air v1.64.5/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
|
||||
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
|
||||
github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||
github.com/alexflint/go-arg v1.6.1 h1:uZogJ6VDBjcuosydKgvYYRhh9sRCusjOvoOLZopBlnA=
|
||||
github.com/alexflint/go-arg v1.6.1/go.mod h1:nQ0LFYftLJ6njcaee0sU+G0iS2+2XJQfA8I062D0LGc=
|
||||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760 h1:FvTM5NSN5HYvfKpgL+8x73U5v063vHsd7AX05eV1DnM=
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ=
|
||||
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
|
||||
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
|
||||
github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
|
||||
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
|
||||
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
|
||||
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
|
||||
github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU=
|
||||
github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
|
||||
github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
|
||||
github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5kujw=
|
||||
github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg=
|
||||
github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q=
|
||||
github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc=
|
||||
github.com/bep/helpers v0.6.0 h1:qtqMCK8XPFNM9hp5Ztu9piPjxNNkk8PIyUVjg6v8Bsw=
|
||||
github.com/bep/helpers v0.6.0/go.mod h1:IOZlgx5PM/R/2wgyCatfsgg5qQ6rNZJNDpWGXqDR044=
|
||||
github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k=
|
||||
github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
|
||||
github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8=
|
||||
github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk=
|
||||
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
|
||||
github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
|
||||
github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE=
|
||||
github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro=
|
||||
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
|
||||
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
@@ -83,12 +44,28 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
|
||||
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.2.1+incompatible h1:aTSWVTDStpHbnRu0xBcGoJEjRf5EQKt6nik6Vif8sWw=
|
||||
github.com/docker/docker v28.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
|
||||
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ=
|
||||
github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY=
|
||||
github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
|
||||
github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
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/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
@@ -97,36 +74,37 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanw/esbuild v0.25.9 h1:aU7GVC4lxJGC1AyaPwySWjSIaNLAdVEEuq3chD0Khxs=
|
||||
github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
|
||||
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.3 h1:50Uzmacu35/ZP9ER2Ht6SazwPsnLQ9LRJy6zTZJpHEo=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.3/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
|
||||
github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs=
|
||||
github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0=
|
||||
github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt4k=
|
||||
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=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
@@ -139,62 +117,43 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
|
||||
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
|
||||
github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg=
|
||||
github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec=
|
||||
github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs=
|
||||
github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
|
||||
github.com/gohugoio/hugo v0.149.1 h1:uWOc8Ve4h4e48FyYhBquRoHCJviyxA5yGrFJLT48yio=
|
||||
github.com/gohugoio/hugo v0.149.1/go.mod h1:HS6BP6e8FGxungP4CHC3zeLDvhBLnTJIjHJZWTZjs7o=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0 h1:dco+7YiOryRoPOMXwwaf+kktZSCtlFtreNdiJbETvYE=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0/go.mod h1:CRrxQTKeM3imw+UoS4EHKyrqB7Zp6sAJiqHit+aMGTE=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1 h1:nUzXfRTszLliZuN0JTKeunXTRaiFX6ksaWP0puLLYAY=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1/go.mod h1:Wy8ThAA8p2/w1DY05vEzq6EIeI2mzDjvHsu7ULBVwog=
|
||||
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
|
||||
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
|
||||
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
|
||||
github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
|
||||
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -202,8 +161,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
@@ -218,10 +175,6 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
|
||||
github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
|
||||
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -229,12 +182,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
@@ -248,62 +195,47 @@ github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXy
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
|
||||
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
||||
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.0.0 h1:F1za+MBXzDQtQq+OVgFsojSX4w66rsNDmQNebPFAncA=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.0.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
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=
|
||||
@@ -312,11 +244,13 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -327,76 +261,125 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tdewolff/minify/v2 v2.24.2 h1:vnY3nTulEAbCAAlxTxPPDkzG24rsq31SOzp63yT+7mo=
|
||||
github.com/tdewolff/minify/v2 v2.24.2/go.mod h1:1JrCtoZXaDbqioQZfk3Jdmr0GPJKiU7c1Apmb+7tCeE=
|
||||
github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I=
|
||||
github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI=
|
||||
github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
|
||||
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
|
||||
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.14 h1:9F3UqVQdZ5GG5y6TU0l1TbbDhZmqfevaOcinQt88Qi8=
|
||||
github.com/yuin/goldmark v1.7.14/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
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/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
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=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
@@ -405,23 +388,57 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
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/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
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=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -434,10 +451,22 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.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=
|
||||
@@ -447,10 +476,22 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
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/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
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=
|
||||
@@ -460,47 +501,102 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
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/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
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/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
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=
|
||||
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
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/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1/go.mod h1:YNKnb2OAApgYn2oYY47Rn7alMr1zWjb2U8Q0aoGWiNc=
|
||||
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=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
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=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/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=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -508,35 +604,124 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
|
||||
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
|
||||
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
|
||||
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
|
||||
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
|
||||
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
|
||||
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
|
||||
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
|
||||
k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8=
|
||||
k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
|
||||
k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk=
|
||||
k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc=
|
||||
k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
|
||||
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
|
||||
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
|
||||
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
|
||||
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
|
||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||
k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
|
||||
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
|
||||
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
|
||||
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
|
||||
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
|
||||
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
|
||||
k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s=
|
||||
k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
|
||||
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
|
||||
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
|
||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
|
||||
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
|
||||
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
|
||||
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
|
||||
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
|
||||
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
|
||||
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
|
||||
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
|
||||
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
|
||||
k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA=
|
||||
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
|
||||
k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw=
|
||||
k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY=
|
||||
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
|
||||
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
|
||||
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
||||
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
|
||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
|
||||
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
|
||||
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/metrics v0.32.3 h1:2vsBvw0v8rIIlczZ/lZ8Kcqk9tR6Fks9h+dtFNbc2a4=
|
||||
k8s.io/metrics v0.32.3/go.mod h1:9R1Wk5cb+qJpCQon9h52mgkVCcFeYxcY+YkumfwHVCU=
|
||||
k8s.io/metrics v0.33.0 h1:sKe5sC9qb1RakMhs8LWYNuN2ne6OTCWexj8Jos3rO2Y=
|
||||
k8s.io/metrics v0.33.0/go.mod h1:XewckTFXmE2AJiP7PT3EXaY7hi7bler3t2ZLyOdQYzU=
|
||||
k8s.io/metrics v0.33.1 h1:Ypd5ITCf+fM+LDNFk7hESXTc3vh02CQYGiwRoVRaGsM=
|
||||
k8s.io/metrics v0.33.1/go.mod h1:wK8cFTK5ykBdhL0Wy4RZwLH28XM7j/Klc+NQrMRWVxg=
|
||||
k8s.io/metrics v0.33.2 h1:gNCBmtnUMDMCRg9Ly5ehxP3OdKISMsOnh1vzk01iCgE=
|
||||
k8s.io/metrics v0.33.2/go.mod h1:yxoAosKGRsZisv3BGekC5W6T1J8XSV+PoUEevACRv7c=
|
||||
k8s.io/metrics v0.33.3 h1:9CcqBz15JZfISqwca33gdHS8I6XfsK1vA8WUdEnG70g=
|
||||
k8s.io/metrics v0.33.3/go.mod h1:Aw+cdg4AYHw0HvUY+lCyq40FOO84awrqvJRTw0cmXDs=
|
||||
k8s.io/metrics v0.33.4 h1:eJ6UdTpKTUQVZbKpUdm5ve39aPpAvvNwLrs13oQcWKc=
|
||||
k8s.io/metrics v0.33.4/go.mod h1:NO/lgFtyIPTurz56debdSh5qRqRfpO8MlkMpau1Ue8U=
|
||||
k8s.io/metrics v0.34.0 h1:nYSfG2+tnL6/MRC2I+sGHjtNEGoEoM/KktgGOoQFwws=
|
||||
k8s.io/metrics v0.34.0/go.mod h1:KCuXmotE0v4AvoARKUP8NC4lUnbK/Du1mluGdor5h4M=
|
||||
k8s.io/metrics v0.34.1 h1:374Rexmp1xxgRt64Bi0TsjAM8cA/Y8skwCoPdjtIslE=
|
||||
k8s.io/metrics v0.34.1/go.mod h1:Drf5kPfk2NJrlpcNdSiAAHn/7Y9KqxpRNagByM7Ei80=
|
||||
k8s.io/metrics v0.34.2 h1:zao91FNDVPRGIiHLO2vqqe21zZVPien1goyzn0hsz90=
|
||||
k8s.io/metrics v0.34.2/go.mod h1:Ydulln+8uZZctUM8yrUQX4rfq/Ay6UzsuXf24QJ37Vc=
|
||||
k8s.io/metrics v0.34.3 h1:zKco9A0q7Ibl3alcO1kqRandTt4GKwKGOBflYJTIBHc=
|
||||
k8s.io/metrics v0.34.3/go.mod h1:BWmkYCQ9x4I120OmCtMUeuXn0VTGkJLwBErneDL5aSQ=
|
||||
k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8=
|
||||
k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M=
|
||||
k8s.io/metrics v0.35.1 h1:MUcrUcWlq81XiripkydzCGsY9zQawDXfP9IICNNcVVw=
|
||||
k8s.io/metrics v0.35.1/go.mod h1:9x7xWOAOiWzHA0vaqLgSE4PXF3vyT5ts5XIbx8OSjiI=
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
|
||||
schema:
|
||||
- graph/*.graphqls
|
||||
|
||||
# Where should the generated server code go?
|
||||
exec:
|
||||
package: graph
|
||||
layout: single-file # Only other option is "follow-schema," ie multi-file.
|
||||
|
||||
# Only for single-file layout:
|
||||
filename: graph/generated.go
|
||||
|
||||
# Only for follow-schema layout:
|
||||
# dir: graph
|
||||
# filename_template: "{name}.generated.go"
|
||||
|
||||
# Optional: Maximum number of goroutines in concurrency to use per child resolvers(default: unlimited)
|
||||
# worker_limit: 1000
|
||||
|
||||
# Uncomment to enable federation
|
||||
# federation:
|
||||
# filename: graph/federation.go
|
||||
# package: graph
|
||||
# version: 2
|
||||
# options:
|
||||
# computed_requires: true
|
||||
|
||||
# Where should any generated models go?
|
||||
model:
|
||||
filename: graph/model/models_gen.go
|
||||
package: model
|
||||
|
||||
# Optional: Pass in a path to a new gotpl template to use for generating the models
|
||||
# model_template: [your/path/model.gotpl]
|
||||
|
||||
# Where should the resolver implementations go?
|
||||
resolver:
|
||||
package: graph
|
||||
layout: follow-schema # Only other option is "single-file."
|
||||
|
||||
# Only for single-file layout:
|
||||
# filename: graph/resolver.go
|
||||
|
||||
# Only for follow-schema layout:
|
||||
dir: graph
|
||||
filename_template: "{name}.resolvers.go"
|
||||
|
||||
# Optional: turn on to not generate template comments above resolvers
|
||||
# omit_template_comment: false
|
||||
# Optional: Pass in a path to a new gotpl template to use for generating resolvers
|
||||
# resolver_template: [your/path/resolver.gotpl]
|
||||
# Optional: turn on to avoid rewriting existing resolver(s) when generating
|
||||
# preserve_resolver: false
|
||||
|
||||
# Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models
|
||||
# struct_tag: json
|
||||
|
||||
# Optional: turn on to use []Thing instead of []*Thing
|
||||
# omit_slice_element_pointers: false
|
||||
|
||||
# Optional: turn on to omit Is<Name>() methods to interface and unions
|
||||
# omit_interface_checks: true
|
||||
|
||||
# Optional: turn on to skip generation of ComplexityRoot struct content and Complexity function
|
||||
# omit_complexity: false
|
||||
|
||||
# Optional: turn on to not generate any file notice comments in generated files
|
||||
# omit_gqlgen_file_notice: false
|
||||
|
||||
# Optional: turn on to exclude the gqlgen version in the generated file notice. No effect if `omit_gqlgen_file_notice` is true.
|
||||
# omit_gqlgen_version_in_file_notice: false
|
||||
|
||||
# Optional: turn on to exclude root models such as Query and Mutation from the generated models file.
|
||||
# omit_root_models: false
|
||||
|
||||
# Optional: turn on to exclude resolver fields from the generated models file.
|
||||
# omit_resolver_fields: false
|
||||
|
||||
# Optional: turn off to make struct-type struct fields not use pointers
|
||||
# e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing }
|
||||
# struct_fields_always_pointers: true
|
||||
|
||||
# Optional: turn off to make resolvers return values instead of pointers for structs
|
||||
# resolvers_always_return_pointers: true
|
||||
|
||||
# Optional: turn on to return pointers instead of values in unmarshalInput
|
||||
# return_pointers_in_unmarshalinput: false
|
||||
|
||||
# Optional: wrap nullable input fields with Omittable
|
||||
# nullable_input_omittable: true
|
||||
|
||||
# Optional: set to speed up generation time by not performing a final validation pass.
|
||||
# skip_validation: true
|
||||
|
||||
# Optional: set to skip running `go mod tidy` when generating server code
|
||||
# skip_mod_tidy: true
|
||||
|
||||
# Optional: if this is set to true, argument directives that
|
||||
# decorate a field with a null value will still be called.
|
||||
#
|
||||
# This enables argumment directives to not just mutate
|
||||
# argument values but to set them even if they're null.
|
||||
call_argument_directives_with_null: true
|
||||
|
||||
# This enables gql server to use function syntax for execution context
|
||||
# instead of generating receiver methods of the execution context.
|
||||
# use_function_syntax_for_execution_context: true
|
||||
|
||||
# Optional: set build tags that will be used to load packages
|
||||
# go_build_tags:
|
||||
# - private
|
||||
# - enterprise
|
||||
|
||||
# Optional: set to modify the initialisms regarded for Go names
|
||||
# go_initialisms:
|
||||
# replace_defaults: false # if true, the default initialisms will get dropped in favor of the new ones instead of being added
|
||||
# initialisms: # List of initialisms to for Go names
|
||||
# - 'CC'
|
||||
# - 'BCC'
|
||||
|
||||
# gqlgen will search for any type names in the schema in these go packages
|
||||
# if they match it will use them, otherwise it will generate them.
|
||||
autobind:
|
||||
# - "github.com/amir20/dozzle/graph/model"
|
||||
|
||||
# This section declares type mapping between the GraphQL and go type systems
|
||||
#
|
||||
# The first line in each type will be used as defaults for resolver arguments and
|
||||
# modelgen, the others will be allowed when binding to fields. Configure them to
|
||||
# your liking
|
||||
models:
|
||||
ID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.ID
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
# gqlgen provides a default GraphQL UUID convenience wrapper for github.com/google/uuid
|
||||
# but you can override this to provide your own GraphQL UUID implementation
|
||||
UUID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.UUID
|
||||
|
||||
# The GraphQL spec explicitly states that the Int type is a signed 32-bit
|
||||
# integer. Using Go int or int64 to represent it can lead to unexpected
|
||||
# behavior, and some GraphQL tools like Apollo Router will fail when
|
||||
# communicating numbers that overflow 32-bits.
|
||||
#
|
||||
# You may choose to use the custom, built-in Int64 scalar to represent 64-bit
|
||||
# integers, or ignore the spec and bind Int to graphql.Int / graphql.Int64
|
||||
# (the default behavior of gqlgen). This is fine in simple use cases when you
|
||||
# do not need to worry about interoperability and only expect small numbers.
|
||||
Int:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
Int64:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
Time:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Time
|
||||
Any:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Any
|
||||
Map:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Map
|
||||
Container:
|
||||
model:
|
||||
- github.com/amir20/dozzle/internal/container.Container
|
||||
LogEvent:
|
||||
model:
|
||||
- github.com/amir20/dozzle/internal/container.LogEvent
|
||||
Release:
|
||||
model:
|
||||
- github.com/amir20/dozzle/internal/releases.Release
|
||||
+54
-101
@@ -8,14 +8,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent/pb"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
"google.golang.org/grpc"
|
||||
@@ -276,17 +274,8 @@ func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- cont
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) FindContainer(ctx context.Context, containerID string, labels container.ContainerLabels) (container.Container, error) {
|
||||
in := &pb.FindContainerRequest{ContainerId: containerID}
|
||||
|
||||
if labels != nil {
|
||||
in.Filter = make(map[string]*pb.RepeatedString)
|
||||
for k, v := range labels {
|
||||
in.Filter[k] = &pb.RepeatedString{Values: v}
|
||||
}
|
||||
}
|
||||
|
||||
response, err := c.client.FindContainer(ctx, in)
|
||||
func (c *Client) FindContainer(ctx context.Context, containerID string) (container.Container, error) {
|
||||
response, err := c.client.FindContainer(ctx, &pb.FindContainerRequest{ContainerId: containerID})
|
||||
if err != nil {
|
||||
return container.Container{}, err
|
||||
}
|
||||
@@ -376,41 +365,30 @@ func (c *Client) ContainerAttach(ctx context.Context, containerId string) (*cont
|
||||
defer stdoutWriter.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
default:
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stdoutWriter.Write(msg.Stdout)
|
||||
}
|
||||
|
||||
stdoutWriter.Write(msg.Stdout)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer stdinReader.Close()
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
n, err := stdinReader.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
default:
|
||||
n, err := stdinReader.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := stream.Send(&pb.ContainerAttachRequest{
|
||||
Payload: &pb.ContainerAttachRequest_Stdin{
|
||||
Stdin: buffer[:n],
|
||||
},
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if err := stream.Send(&pb.ContainerAttachRequest{
|
||||
Payload: &pb.ContainerAttachRequest_Stdin{
|
||||
Stdin: buffer[:n],
|
||||
},
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -434,101 +412,76 @@ func (c *Client) ContainerAttach(ctx context.Context, containerId string) (*cont
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Exec(ctx context.Context, containerId string, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
func (c *Client) ContainerExec(ctx context.Context, containerId string, cmd []string) (*container.ExecSession, error) {
|
||||
stream, err := c.client.ContainerExec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = stream.Send(&pb.ContainerExecRequest{
|
||||
ContainerId: containerId,
|
||||
Command: cmd,
|
||||
}); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
stdinReader, stdinWriter := io.Pipe()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
go func() {
|
||||
defer stdoutWriter.Close()
|
||||
|
||||
// Read from gRPC stream and write to stdout
|
||||
wg.Go(func() {
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
stdout.Write(msg.Stdout)
|
||||
}
|
||||
})
|
||||
|
||||
// Read events and convert to gRPC messages
|
||||
wg.Go(func() {
|
||||
stdoutWriter.Write(msg.Stdout)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
for {
|
||||
event, err := events.ReadEvent()
|
||||
n, err := stdinReader.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch event.Type {
|
||||
case "userinput":
|
||||
stream.Send(&pb.ContainerExecRequest{
|
||||
Payload: &pb.ContainerExecRequest_Stdin{
|
||||
Stdin: []byte(event.Data),
|
||||
},
|
||||
})
|
||||
case "resize":
|
||||
stream.Send(&pb.ContainerExecRequest{
|
||||
Payload: &pb.ContainerExecRequest_Resize{
|
||||
Resize: &pb.ResizePayload{
|
||||
Width: uint32(event.Width),
|
||||
Height: uint32(event.Height),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := stream.Send(&pb.ContainerExecRequest{
|
||||
Payload: &pb.ContainerExecRequest_Stdin{
|
||||
Stdin: buffer[:n],
|
||||
},
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
// Create resize closure that sends via gRPC
|
||||
resizeFn := func(width uint, height uint) error {
|
||||
return stream.Send(&pb.ContainerExecRequest{
|
||||
Payload: &pb.ContainerExecRequest_Resize{
|
||||
Resize: &pb.ResizePayload{
|
||||
Width: uint32(width),
|
||||
Height: uint32(height),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &container.ExecSession{
|
||||
Writer: stdinWriter,
|
||||
Reader: stdoutReader,
|
||||
Resize: resizeFn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) UpdateNotificationConfig(ctx context.Context, subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
|
||||
// Convert to proto
|
||||
pbSubs := make([]*pb.NotificationSubscription, len(subscriptions))
|
||||
for i, sub := range subscriptions {
|
||||
pbSubs[i] = &pb.NotificationSubscription{
|
||||
Id: int32(sub.ID),
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
DispatcherId: int32(sub.DispatcherID),
|
||||
LogExpression: sub.LogExpression,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
}
|
||||
}
|
||||
|
||||
pbDispatchers := make([]*pb.NotificationDispatcher, len(dispatchers))
|
||||
for i, d := range dispatchers {
|
||||
pbDispatchers[i] = &pb.NotificationDispatcher{
|
||||
Id: int32(d.ID),
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
Url: d.URL,
|
||||
Template: d.Template,
|
||||
}
|
||||
}
|
||||
|
||||
_, err := c.client.UpdateNotificationConfig(ctx, &pb.UpdateNotificationConfigRequest{
|
||||
Subscriptions: pbSubs,
|
||||
Dispatchers: pbDispatchers,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func jsonBytesToOrderedMap(b []byte) *orderedmap.OrderedMap[string, any] {
|
||||
var data *orderedmap.OrderedMap[string, any]
|
||||
reader := bytes.NewReader(b)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/go-faker/faker/v4"
|
||||
"github.com/go-faker/faker/v4/pkg/options"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -26,67 +27,58 @@ const bufSize = 1024 * 1024
|
||||
|
||||
var lis *bufconn.Listener
|
||||
var certs tls.Certificate
|
||||
var mockService *MockedClientService
|
||||
var client *MockedClient
|
||||
|
||||
type MockedClientService struct {
|
||||
type MockedClient struct {
|
||||
mock.Mock
|
||||
container.Client
|
||||
}
|
||||
|
||||
func (m *MockedClientService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
|
||||
args := m.Called(ctx, id, labels)
|
||||
func (m *MockedClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
|
||||
func (m *MockedClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
|
||||
args := m.Called(ctx, action, containerID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
|
||||
args := m.Called(ctx, events)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
|
||||
args := m.Called(ctx, filter)
|
||||
return args.Get(0).([]container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) Host(ctx context.Context) (container.Host, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(container.Host), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) ContainerAction(ctx context.Context, c container.Container, action container.ContainerAction) error {
|
||||
args := m.Called(ctx, c, action)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
args := m.Called(ctx, c, from, to, stdTypes)
|
||||
return args.Get(0).(<-chan *container.LogEvent), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) RawLogs(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, c, from, to, stdTypes)
|
||||
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, id, since, stdType)
|
||||
return args.Get(0).(io.ReadCloser), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
m.Called(ctx, stats)
|
||||
func (m *MockedClient) ContainerStats(context.Context, string, chan<- container.ContainerStat) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockedClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
m.Called(ctx, events)
|
||||
func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, id, from, to, stdType)
|
||||
return args.Get(0).(io.ReadCloser), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
|
||||
m.Called(ctx, containers)
|
||||
func (m *MockedClient) Host() container.Host {
|
||||
args := m.Called()
|
||||
return args.Get(0).(container.Host)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
args := m.Called(ctx, c, from, stdTypes, events)
|
||||
return args.Error(0)
|
||||
func (m *MockedClient) IsSwarmMode() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *MockedClientService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
|
||||
args := m.Called(ctx, c, events, stdout)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) Exec(ctx context.Context, c container.Container, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
args := m.Called(ctx, c, cmd, events, stdout)
|
||||
return args.Error(0)
|
||||
func (m *MockedClient) SystemInfo() system.Info {
|
||||
return system.Info{ID: "123"}
|
||||
}
|
||||
|
||||
var wantedContainer = container.Container{}
|
||||
@@ -111,30 +103,28 @@ func init() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mockService = &MockedClientService{}
|
||||
mockService.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
|
||||
wantedContainer,
|
||||
client = &MockedClient{}
|
||||
client.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
|
||||
{
|
||||
ID: "123456",
|
||||
Name: "test",
|
||||
Host: "localhost",
|
||||
State: "running",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mockService.On("Host", mock.Anything).Return(container.Host{
|
||||
client.On("Host").Return(container.Host{
|
||||
ID: "localhost",
|
||||
Endpoint: "local",
|
||||
Name: "local",
|
||||
}, nil)
|
||||
})
|
||||
|
||||
mockService.On("SubscribeEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return().Run(func(args mock.Arguments) {
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
|
||||
time.Sleep(5 * time.Second)
|
||||
})
|
||||
|
||||
mockService.On("SubscribeStats", mock.Anything, mock.AnythingOfType("chan<- container.ContainerStat")).Return()
|
||||
|
||||
mockService.On("SubscribeContainersStarted", mock.Anything, mock.AnythingOfType("chan<- container.Container")).Return()
|
||||
|
||||
mockService.On("FindContainer", mock.Anything, "123456", mock.Anything).Return(wantedContainer, nil)
|
||||
|
||||
mockService.On("Client").Return(nil)
|
||||
|
||||
server, _ := NewServer(mockService, certs, "test", nil)
|
||||
client.On("FindContainer", mock.Anything, "123456").Return(wantedContainer, nil)
|
||||
server, _ := NewServer(client, certs, "test", container.ContainerLabels{})
|
||||
go server.Serve(lis)
|
||||
}
|
||||
|
||||
@@ -148,7 +138,7 @@ func TestFindContainer(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c, _ := rpc.FindContainer(context.Background(), "123456", container.ContainerLabels{})
|
||||
c, _ := rpc.FindContainer(context.Background(), "123456")
|
||||
|
||||
assert.Equal(t, wantedContainer, c)
|
||||
}
|
||||
|
||||
+86
-187
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.4
|
||||
// protoc v6.33.3
|
||||
// source: rpc.proto
|
||||
|
||||
package pb
|
||||
@@ -1270,99 +1270,11 @@ func (x *ContainerAttachResponse) GetStdout() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdateNotificationConfigRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Subscriptions []*NotificationSubscription `protobuf:"bytes,1,rep,name=subscriptions,proto3" json:"subscriptions,omitempty"`
|
||||
Dispatchers []*NotificationDispatcher `protobuf:"bytes,2,rep,name=dispatchers,proto3" json:"dispatchers,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UpdateNotificationConfigRequest) Reset() {
|
||||
*x = UpdateNotificationConfigRequest{}
|
||||
mi := &file_rpc_proto_msgTypes[25]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UpdateNotificationConfigRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UpdateNotificationConfigRequest) ProtoMessage() {}
|
||||
|
||||
func (x *UpdateNotificationConfigRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_proto_msgTypes[25]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UpdateNotificationConfigRequest.ProtoReflect.Descriptor instead.
|
||||
func (*UpdateNotificationConfigRequest) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_proto_rawDescGZIP(), []int{25}
|
||||
}
|
||||
|
||||
func (x *UpdateNotificationConfigRequest) GetSubscriptions() []*NotificationSubscription {
|
||||
if x != nil {
|
||||
return x.Subscriptions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *UpdateNotificationConfigRequest) GetDispatchers() []*NotificationDispatcher {
|
||||
if x != nil {
|
||||
return x.Dispatchers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdateNotificationConfigResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UpdateNotificationConfigResponse) Reset() {
|
||||
*x = UpdateNotificationConfigResponse{}
|
||||
mi := &file_rpc_proto_msgTypes[26]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UpdateNotificationConfigResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UpdateNotificationConfigResponse) ProtoMessage() {}
|
||||
|
||||
func (x *UpdateNotificationConfigResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_proto_msgTypes[26]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UpdateNotificationConfigResponse.ProtoReflect.Descriptor instead.
|
||||
func (*UpdateNotificationConfigResponse) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_proto_rawDescGZIP(), []int{26}
|
||||
}
|
||||
|
||||
var File_rpc_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_rpc_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\trpc.proto\x12\bprotobuf\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\vtypes.proto\"\xb1\x01\n" +
|
||||
"\trpc.proto\x12\bprotobuf\x1a\vtypes.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb1\x01\n" +
|
||||
"\x15ListContainersRequest\x12C\n" +
|
||||
"\x06filter\x18\x01 \x03(\v2+.protobuf.ListContainersRequest.FilterEntryR\x06filter\x1aS\n" +
|
||||
"\vFilterEntry\x12\x10\n" +
|
||||
@@ -1433,11 +1345,7 @@ const file_rpc_proto_rawDesc = "" +
|
||||
"\x06resize\x18\x03 \x01(\v2\x17.protobuf.ResizePayloadH\x00R\x06resizeB\t\n" +
|
||||
"\apayload\"1\n" +
|
||||
"\x17ContainerAttachResponse\x12\x16\n" +
|
||||
"\x06stdout\x18\x01 \x01(\fR\x06stdout\"\xaf\x01\n" +
|
||||
"\x1fUpdateNotificationConfigRequest\x12H\n" +
|
||||
"\rsubscriptions\x18\x01 \x03(\v2\".protobuf.NotificationSubscriptionR\rsubscriptions\x12B\n" +
|
||||
"\vdispatchers\x18\x02 \x03(\v2 .protobuf.NotificationDispatcherR\vdispatchers\"\"\n" +
|
||||
" UpdateNotificationConfigResponse2\x96\t\n" +
|
||||
"\x06stdout\x18\x01 \x01(\fR\x06stdout2\xa1\b\n" +
|
||||
"\fAgentService\x12U\n" +
|
||||
"\x0eListContainers\x12\x1f.protobuf.ListContainersRequest\x1a .protobuf.ListContainersResponse\"\x00\x12R\n" +
|
||||
"\rFindContainer\x12\x1e.protobuf.FindContainerRequest\x1a\x1f.protobuf.FindContainerResponse\"\x00\x12K\n" +
|
||||
@@ -1451,8 +1359,7 @@ const file_rpc_proto_rawDesc = "" +
|
||||
"\bHostInfo\x12\x19.protobuf.HostInfoRequest\x1a\x1a.protobuf.HostInfoResponse\"\x00\x12X\n" +
|
||||
"\x0fContainerAction\x12 .protobuf.ContainerActionRequest\x1a!.protobuf.ContainerActionResponse\"\x00\x12V\n" +
|
||||
"\rContainerExec\x12\x1e.protobuf.ContainerExecRequest\x1a\x1f.protobuf.ContainerExecResponse\"\x00(\x010\x01\x12\\\n" +
|
||||
"\x0fContainerAttach\x12 .protobuf.ContainerAttachRequest\x1a!.protobuf.ContainerAttachResponse\"\x00(\x010\x01\x12s\n" +
|
||||
"\x18UpdateNotificationConfig\x12).protobuf.UpdateNotificationConfigRequest\x1a*.protobuf.UpdateNotificationConfigResponse\"\x00B\x13Z\x11internal/agent/pbb\x06proto3"
|
||||
"\x0fContainerAttach\x12 .protobuf.ContainerAttachRequest\x1a!.protobuf.ContainerAttachResponse\"\x00(\x010\x01B\x13Z\x11internal/agent/pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_rpc_proto_rawDescOnce sync.Once
|
||||
@@ -1466,100 +1373,92 @@ func file_rpc_proto_rawDescGZIP() []byte {
|
||||
return file_rpc_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
|
||||
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
|
||||
var file_rpc_proto_goTypes = []any{
|
||||
(*ListContainersRequest)(nil), // 0: protobuf.ListContainersRequest
|
||||
(*RepeatedString)(nil), // 1: protobuf.RepeatedString
|
||||
(*ListContainersResponse)(nil), // 2: protobuf.ListContainersResponse
|
||||
(*FindContainerRequest)(nil), // 3: protobuf.FindContainerRequest
|
||||
(*FindContainerResponse)(nil), // 4: protobuf.FindContainerResponse
|
||||
(*StreamLogsRequest)(nil), // 5: protobuf.StreamLogsRequest
|
||||
(*StreamLogsResponse)(nil), // 6: protobuf.StreamLogsResponse
|
||||
(*LogsBetweenDatesRequest)(nil), // 7: protobuf.LogsBetweenDatesRequest
|
||||
(*StreamRawBytesRequest)(nil), // 8: protobuf.StreamRawBytesRequest
|
||||
(*StreamRawBytesResponse)(nil), // 9: protobuf.StreamRawBytesResponse
|
||||
(*StreamEventsRequest)(nil), // 10: protobuf.StreamEventsRequest
|
||||
(*StreamEventsResponse)(nil), // 11: protobuf.StreamEventsResponse
|
||||
(*StreamStatsRequest)(nil), // 12: protobuf.StreamStatsRequest
|
||||
(*StreamStatsResponse)(nil), // 13: protobuf.StreamStatsResponse
|
||||
(*HostInfoRequest)(nil), // 14: protobuf.HostInfoRequest
|
||||
(*HostInfoResponse)(nil), // 15: protobuf.HostInfoResponse
|
||||
(*StreamContainerStartedRequest)(nil), // 16: protobuf.StreamContainerStartedRequest
|
||||
(*StreamContainerStartedResponse)(nil), // 17: protobuf.StreamContainerStartedResponse
|
||||
(*ContainerActionRequest)(nil), // 18: protobuf.ContainerActionRequest
|
||||
(*ContainerActionResponse)(nil), // 19: protobuf.ContainerActionResponse
|
||||
(*ContainerExecRequest)(nil), // 20: protobuf.ContainerExecRequest
|
||||
(*ResizePayload)(nil), // 21: protobuf.ResizePayload
|
||||
(*ContainerExecResponse)(nil), // 22: protobuf.ContainerExecResponse
|
||||
(*ContainerAttachRequest)(nil), // 23: protobuf.ContainerAttachRequest
|
||||
(*ContainerAttachResponse)(nil), // 24: protobuf.ContainerAttachResponse
|
||||
(*UpdateNotificationConfigRequest)(nil), // 25: protobuf.UpdateNotificationConfigRequest
|
||||
(*UpdateNotificationConfigResponse)(nil), // 26: protobuf.UpdateNotificationConfigResponse
|
||||
nil, // 27: protobuf.ListContainersRequest.FilterEntry
|
||||
nil, // 28: protobuf.FindContainerRequest.FilterEntry
|
||||
(*Container)(nil), // 29: protobuf.Container
|
||||
(*timestamppb.Timestamp)(nil), // 30: google.protobuf.Timestamp
|
||||
(*LogEvent)(nil), // 31: protobuf.LogEvent
|
||||
(*ContainerEvent)(nil), // 32: protobuf.ContainerEvent
|
||||
(*ContainerStat)(nil), // 33: protobuf.ContainerStat
|
||||
(*Host)(nil), // 34: protobuf.Host
|
||||
(ContainerAction)(0), // 35: protobuf.ContainerAction
|
||||
(*NotificationSubscription)(nil), // 36: protobuf.NotificationSubscription
|
||||
(*NotificationDispatcher)(nil), // 37: protobuf.NotificationDispatcher
|
||||
(*ListContainersRequest)(nil), // 0: protobuf.ListContainersRequest
|
||||
(*RepeatedString)(nil), // 1: protobuf.RepeatedString
|
||||
(*ListContainersResponse)(nil), // 2: protobuf.ListContainersResponse
|
||||
(*FindContainerRequest)(nil), // 3: protobuf.FindContainerRequest
|
||||
(*FindContainerResponse)(nil), // 4: protobuf.FindContainerResponse
|
||||
(*StreamLogsRequest)(nil), // 5: protobuf.StreamLogsRequest
|
||||
(*StreamLogsResponse)(nil), // 6: protobuf.StreamLogsResponse
|
||||
(*LogsBetweenDatesRequest)(nil), // 7: protobuf.LogsBetweenDatesRequest
|
||||
(*StreamRawBytesRequest)(nil), // 8: protobuf.StreamRawBytesRequest
|
||||
(*StreamRawBytesResponse)(nil), // 9: protobuf.StreamRawBytesResponse
|
||||
(*StreamEventsRequest)(nil), // 10: protobuf.StreamEventsRequest
|
||||
(*StreamEventsResponse)(nil), // 11: protobuf.StreamEventsResponse
|
||||
(*StreamStatsRequest)(nil), // 12: protobuf.StreamStatsRequest
|
||||
(*StreamStatsResponse)(nil), // 13: protobuf.StreamStatsResponse
|
||||
(*HostInfoRequest)(nil), // 14: protobuf.HostInfoRequest
|
||||
(*HostInfoResponse)(nil), // 15: protobuf.HostInfoResponse
|
||||
(*StreamContainerStartedRequest)(nil), // 16: protobuf.StreamContainerStartedRequest
|
||||
(*StreamContainerStartedResponse)(nil), // 17: protobuf.StreamContainerStartedResponse
|
||||
(*ContainerActionRequest)(nil), // 18: protobuf.ContainerActionRequest
|
||||
(*ContainerActionResponse)(nil), // 19: protobuf.ContainerActionResponse
|
||||
(*ContainerExecRequest)(nil), // 20: protobuf.ContainerExecRequest
|
||||
(*ResizePayload)(nil), // 21: protobuf.ResizePayload
|
||||
(*ContainerExecResponse)(nil), // 22: protobuf.ContainerExecResponse
|
||||
(*ContainerAttachRequest)(nil), // 23: protobuf.ContainerAttachRequest
|
||||
(*ContainerAttachResponse)(nil), // 24: protobuf.ContainerAttachResponse
|
||||
nil, // 25: protobuf.ListContainersRequest.FilterEntry
|
||||
nil, // 26: protobuf.FindContainerRequest.FilterEntry
|
||||
(*Container)(nil), // 27: protobuf.Container
|
||||
(*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp
|
||||
(*LogEvent)(nil), // 29: protobuf.LogEvent
|
||||
(*ContainerEvent)(nil), // 30: protobuf.ContainerEvent
|
||||
(*ContainerStat)(nil), // 31: protobuf.ContainerStat
|
||||
(*Host)(nil), // 32: protobuf.Host
|
||||
(ContainerAction)(0), // 33: protobuf.ContainerAction
|
||||
}
|
||||
var file_rpc_proto_depIdxs = []int32{
|
||||
27, // 0: protobuf.ListContainersRequest.filter:type_name -> protobuf.ListContainersRequest.FilterEntry
|
||||
29, // 1: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
|
||||
28, // 2: protobuf.FindContainerRequest.filter:type_name -> protobuf.FindContainerRequest.FilterEntry
|
||||
29, // 3: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
|
||||
30, // 4: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
|
||||
31, // 5: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
|
||||
30, // 6: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
|
||||
30, // 7: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
|
||||
30, // 8: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
|
||||
30, // 9: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
|
||||
32, // 10: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
|
||||
33, // 11: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
|
||||
34, // 12: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
|
||||
29, // 13: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
|
||||
35, // 14: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
|
||||
25, // 0: protobuf.ListContainersRequest.filter:type_name -> protobuf.ListContainersRequest.FilterEntry
|
||||
27, // 1: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
|
||||
26, // 2: protobuf.FindContainerRequest.filter:type_name -> protobuf.FindContainerRequest.FilterEntry
|
||||
27, // 3: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
|
||||
28, // 4: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
|
||||
29, // 5: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
|
||||
28, // 6: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
|
||||
28, // 7: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
|
||||
28, // 8: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
|
||||
28, // 9: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
|
||||
30, // 10: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
|
||||
31, // 11: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
|
||||
32, // 12: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
|
||||
27, // 13: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
|
||||
33, // 14: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
|
||||
21, // 15: protobuf.ContainerExecRequest.resize:type_name -> protobuf.ResizePayload
|
||||
21, // 16: protobuf.ContainerAttachRequest.resize:type_name -> protobuf.ResizePayload
|
||||
36, // 17: protobuf.UpdateNotificationConfigRequest.subscriptions:type_name -> protobuf.NotificationSubscription
|
||||
37, // 18: protobuf.UpdateNotificationConfigRequest.dispatchers:type_name -> protobuf.NotificationDispatcher
|
||||
1, // 19: protobuf.ListContainersRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
|
||||
1, // 20: protobuf.FindContainerRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
|
||||
0, // 21: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
|
||||
3, // 22: protobuf.AgentService.FindContainer:input_type -> protobuf.FindContainerRequest
|
||||
5, // 23: protobuf.AgentService.StreamLogs:input_type -> protobuf.StreamLogsRequest
|
||||
7, // 24: protobuf.AgentService.LogsBetweenDates:input_type -> protobuf.LogsBetweenDatesRequest
|
||||
8, // 25: protobuf.AgentService.StreamRawBytes:input_type -> protobuf.StreamRawBytesRequest
|
||||
10, // 26: protobuf.AgentService.StreamEvents:input_type -> protobuf.StreamEventsRequest
|
||||
12, // 27: protobuf.AgentService.StreamStats:input_type -> protobuf.StreamStatsRequest
|
||||
16, // 28: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
|
||||
14, // 29: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
|
||||
18, // 30: protobuf.AgentService.ContainerAction:input_type -> protobuf.ContainerActionRequest
|
||||
20, // 31: protobuf.AgentService.ContainerExec:input_type -> protobuf.ContainerExecRequest
|
||||
23, // 32: protobuf.AgentService.ContainerAttach:input_type -> protobuf.ContainerAttachRequest
|
||||
25, // 33: protobuf.AgentService.UpdateNotificationConfig:input_type -> protobuf.UpdateNotificationConfigRequest
|
||||
2, // 34: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
|
||||
4, // 35: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
|
||||
6, // 36: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
|
||||
6, // 37: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
|
||||
9, // 38: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
|
||||
11, // 39: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
|
||||
13, // 40: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
|
||||
17, // 41: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
|
||||
15, // 42: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
|
||||
19, // 43: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
|
||||
22, // 44: protobuf.AgentService.ContainerExec:output_type -> protobuf.ContainerExecResponse
|
||||
24, // 45: protobuf.AgentService.ContainerAttach:output_type -> protobuf.ContainerAttachResponse
|
||||
26, // 46: protobuf.AgentService.UpdateNotificationConfig:output_type -> protobuf.UpdateNotificationConfigResponse
|
||||
34, // [34:47] is the sub-list for method output_type
|
||||
21, // [21:34] is the sub-list for method input_type
|
||||
21, // [21:21] is the sub-list for extension type_name
|
||||
21, // [21:21] is the sub-list for extension extendee
|
||||
0, // [0:21] is the sub-list for field type_name
|
||||
1, // 17: protobuf.ListContainersRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
|
||||
1, // 18: protobuf.FindContainerRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
|
||||
0, // 19: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
|
||||
3, // 20: protobuf.AgentService.FindContainer:input_type -> protobuf.FindContainerRequest
|
||||
5, // 21: protobuf.AgentService.StreamLogs:input_type -> protobuf.StreamLogsRequest
|
||||
7, // 22: protobuf.AgentService.LogsBetweenDates:input_type -> protobuf.LogsBetweenDatesRequest
|
||||
8, // 23: protobuf.AgentService.StreamRawBytes:input_type -> protobuf.StreamRawBytesRequest
|
||||
10, // 24: protobuf.AgentService.StreamEvents:input_type -> protobuf.StreamEventsRequest
|
||||
12, // 25: protobuf.AgentService.StreamStats:input_type -> protobuf.StreamStatsRequest
|
||||
16, // 26: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
|
||||
14, // 27: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
|
||||
18, // 28: protobuf.AgentService.ContainerAction:input_type -> protobuf.ContainerActionRequest
|
||||
20, // 29: protobuf.AgentService.ContainerExec:input_type -> protobuf.ContainerExecRequest
|
||||
23, // 30: protobuf.AgentService.ContainerAttach:input_type -> protobuf.ContainerAttachRequest
|
||||
2, // 31: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
|
||||
4, // 32: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
|
||||
6, // 33: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
|
||||
6, // 34: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
|
||||
9, // 35: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
|
||||
11, // 36: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
|
||||
13, // 37: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
|
||||
17, // 38: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
|
||||
15, // 39: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
|
||||
19, // 40: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
|
||||
22, // 41: protobuf.AgentService.ContainerExec:output_type -> protobuf.ContainerExecResponse
|
||||
24, // 42: protobuf.AgentService.ContainerAttach:output_type -> protobuf.ContainerAttachResponse
|
||||
31, // [31:43] is the sub-list for method output_type
|
||||
19, // [19:31] is the sub-list for method input_type
|
||||
19, // [19:19] is the sub-list for extension type_name
|
||||
19, // [19:19] is the sub-list for extension extendee
|
||||
0, // [0:19] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_rpc_proto_init() }
|
||||
@@ -1582,7 +1481,7 @@ func file_rpc_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_proto_rawDesc), len(file_rpc_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 29,
|
||||
NumMessages: 27,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.4
|
||||
// - protoc v6.33.3
|
||||
// source: rpc.proto
|
||||
|
||||
package pb
|
||||
@@ -19,19 +19,18 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
AgentService_ListContainers_FullMethodName = "/protobuf.AgentService/ListContainers"
|
||||
AgentService_FindContainer_FullMethodName = "/protobuf.AgentService/FindContainer"
|
||||
AgentService_StreamLogs_FullMethodName = "/protobuf.AgentService/StreamLogs"
|
||||
AgentService_LogsBetweenDates_FullMethodName = "/protobuf.AgentService/LogsBetweenDates"
|
||||
AgentService_StreamRawBytes_FullMethodName = "/protobuf.AgentService/StreamRawBytes"
|
||||
AgentService_StreamEvents_FullMethodName = "/protobuf.AgentService/StreamEvents"
|
||||
AgentService_StreamStats_FullMethodName = "/protobuf.AgentService/StreamStats"
|
||||
AgentService_StreamContainerStarted_FullMethodName = "/protobuf.AgentService/StreamContainerStarted"
|
||||
AgentService_HostInfo_FullMethodName = "/protobuf.AgentService/HostInfo"
|
||||
AgentService_ContainerAction_FullMethodName = "/protobuf.AgentService/ContainerAction"
|
||||
AgentService_ContainerExec_FullMethodName = "/protobuf.AgentService/ContainerExec"
|
||||
AgentService_ContainerAttach_FullMethodName = "/protobuf.AgentService/ContainerAttach"
|
||||
AgentService_UpdateNotificationConfig_FullMethodName = "/protobuf.AgentService/UpdateNotificationConfig"
|
||||
AgentService_ListContainers_FullMethodName = "/protobuf.AgentService/ListContainers"
|
||||
AgentService_FindContainer_FullMethodName = "/protobuf.AgentService/FindContainer"
|
||||
AgentService_StreamLogs_FullMethodName = "/protobuf.AgentService/StreamLogs"
|
||||
AgentService_LogsBetweenDates_FullMethodName = "/protobuf.AgentService/LogsBetweenDates"
|
||||
AgentService_StreamRawBytes_FullMethodName = "/protobuf.AgentService/StreamRawBytes"
|
||||
AgentService_StreamEvents_FullMethodName = "/protobuf.AgentService/StreamEvents"
|
||||
AgentService_StreamStats_FullMethodName = "/protobuf.AgentService/StreamStats"
|
||||
AgentService_StreamContainerStarted_FullMethodName = "/protobuf.AgentService/StreamContainerStarted"
|
||||
AgentService_HostInfo_FullMethodName = "/protobuf.AgentService/HostInfo"
|
||||
AgentService_ContainerAction_FullMethodName = "/protobuf.AgentService/ContainerAction"
|
||||
AgentService_ContainerExec_FullMethodName = "/protobuf.AgentService/ContainerExec"
|
||||
AgentService_ContainerAttach_FullMethodName = "/protobuf.AgentService/ContainerAttach"
|
||||
)
|
||||
|
||||
// AgentServiceClient is the client API for AgentService service.
|
||||
@@ -50,7 +49,6 @@ type AgentServiceClient interface {
|
||||
ContainerAction(ctx context.Context, in *ContainerActionRequest, opts ...grpc.CallOption) (*ContainerActionResponse, error)
|
||||
ContainerExec(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerExecRequest, ContainerExecResponse], error)
|
||||
ContainerAttach(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerAttachRequest, ContainerAttachResponse], error)
|
||||
UpdateNotificationConfig(ctx context.Context, in *UpdateNotificationConfigRequest, opts ...grpc.CallOption) (*UpdateNotificationConfigResponse, error)
|
||||
}
|
||||
|
||||
type agentServiceClient struct {
|
||||
@@ -241,16 +239,6 @@ func (c *agentServiceClient) ContainerAttach(ctx context.Context, opts ...grpc.C
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type AgentService_ContainerAttachClient = grpc.BidiStreamingClient[ContainerAttachRequest, ContainerAttachResponse]
|
||||
|
||||
func (c *agentServiceClient) UpdateNotificationConfig(ctx context.Context, in *UpdateNotificationConfigRequest, opts ...grpc.CallOption) (*UpdateNotificationConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(UpdateNotificationConfigResponse)
|
||||
err := c.cc.Invoke(ctx, AgentService_UpdateNotificationConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AgentServiceServer is the server API for AgentService service.
|
||||
// All implementations must embed UnimplementedAgentServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -267,7 +255,6 @@ type AgentServiceServer interface {
|
||||
ContainerAction(context.Context, *ContainerActionRequest) (*ContainerActionResponse, error)
|
||||
ContainerExec(grpc.BidiStreamingServer[ContainerExecRequest, ContainerExecResponse]) error
|
||||
ContainerAttach(grpc.BidiStreamingServer[ContainerAttachRequest, ContainerAttachResponse]) error
|
||||
UpdateNotificationConfig(context.Context, *UpdateNotificationConfigRequest) (*UpdateNotificationConfigResponse, error)
|
||||
mustEmbedUnimplementedAgentServiceServer()
|
||||
}
|
||||
|
||||
@@ -314,9 +301,6 @@ func (UnimplementedAgentServiceServer) ContainerExec(grpc.BidiStreamingServer[Co
|
||||
func (UnimplementedAgentServiceServer) ContainerAttach(grpc.BidiStreamingServer[ContainerAttachRequest, ContainerAttachResponse]) error {
|
||||
return status.Error(codes.Unimplemented, "method ContainerAttach not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) UpdateNotificationConfig(context.Context, *UpdateNotificationConfigRequest) (*UpdateNotificationConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateNotificationConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {}
|
||||
func (UnimplementedAgentServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -490,24 +474,6 @@ func _AgentService_ContainerAttach_Handler(srv interface{}, stream grpc.ServerSt
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type AgentService_ContainerAttachServer = grpc.BidiStreamingServer[ContainerAttachRequest, ContainerAttachResponse]
|
||||
|
||||
func _AgentService_UpdateNotificationConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpdateNotificationConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AgentServiceServer).UpdateNotificationConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AgentService_UpdateNotificationConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AgentServiceServer).UpdateNotificationConfig(ctx, req.(*UpdateNotificationConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AgentService_ServiceDesc is the grpc.ServiceDesc for AgentService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -531,10 +497,6 @@ var AgentService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ContainerAction",
|
||||
Handler: _AgentService_ContainerAction_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateNotificationConfig",
|
||||
Handler: _AgentService_UpdateNotificationConfig_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
+26
-201
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.4
|
||||
// protoc v6.33.3
|
||||
// source: types.proto
|
||||
|
||||
package pb
|
||||
@@ -820,166 +820,6 @@ func (x *Host) GetDockerVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type NotificationSubscription struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||
DispatcherId int32 `protobuf:"varint,4,opt,name=dispatcherId,proto3" json:"dispatcherId,omitempty"`
|
||||
LogExpression string `protobuf:"bytes,5,opt,name=logExpression,proto3" json:"logExpression,omitempty"`
|
||||
ContainerExpression string `protobuf:"bytes,6,opt,name=containerExpression,proto3" json:"containerExpression,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *NotificationSubscription) Reset() {
|
||||
*x = NotificationSubscription{}
|
||||
mi := &file_types_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *NotificationSubscription) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*NotificationSubscription) ProtoMessage() {}
|
||||
|
||||
func (x *NotificationSubscription) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use NotificationSubscription.ProtoReflect.Descriptor instead.
|
||||
func (*NotificationSubscription) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *NotificationSubscription) GetId() int32 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *NotificationSubscription) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NotificationSubscription) GetEnabled() bool {
|
||||
if x != nil {
|
||||
return x.Enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *NotificationSubscription) GetDispatcherId() int32 {
|
||||
if x != nil {
|
||||
return x.DispatcherId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *NotificationSubscription) GetLogExpression() string {
|
||||
if x != nil {
|
||||
return x.LogExpression
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NotificationSubscription) GetContainerExpression() string {
|
||||
if x != nil {
|
||||
return x.ContainerExpression
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type NotificationDispatcher struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Template string `protobuf:"bytes,5,opt,name=template,proto3" json:"template,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) Reset() {
|
||||
*x = NotificationDispatcher{}
|
||||
mi := &file_types_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*NotificationDispatcher) ProtoMessage() {}
|
||||
|
||||
func (x *NotificationDispatcher) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_types_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use NotificationDispatcher.ProtoReflect.Descriptor instead.
|
||||
func (*NotificationDispatcher) Descriptor() ([]byte, []int) {
|
||||
return file_types_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) GetId() int32 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) GetUrl() string {
|
||||
if x != nil {
|
||||
return x.Url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *NotificationDispatcher) GetTemplate() string {
|
||||
if x != nil {
|
||||
return x.Template
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_types_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_types_proto_rawDesc = "" +
|
||||
@@ -1058,20 +898,7 @@ const file_types_proto_rawDesc = "" +
|
||||
"\rdockerVersion\x18\f \x01(\tR\rdockerVersion\x1a9\n" +
|
||||
"\vLabelsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd4\x01\n" +
|
||||
"\x18NotificationSubscription\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x05R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" +
|
||||
"\aenabled\x18\x03 \x01(\bR\aenabled\x12\"\n" +
|
||||
"\fdispatcherId\x18\x04 \x01(\x05R\fdispatcherId\x12$\n" +
|
||||
"\rlogExpression\x18\x05 \x01(\tR\rlogExpression\x120\n" +
|
||||
"\x13containerExpression\x18\x06 \x01(\tR\x13containerExpression\"~\n" +
|
||||
"\x16NotificationDispatcher\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x05R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
|
||||
"\x04type\x18\x03 \x01(\tR\x04type\x12\x10\n" +
|
||||
"\x03url\x18\x04 \x01(\tR\x03url\x12\x1a\n" +
|
||||
"\btemplate\x18\x05 \x01(\tR\btemplate*3\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*3\n" +
|
||||
"\x0fContainerAction\x12\t\n" +
|
||||
"\x05Start\x10\x00\x12\b\n" +
|
||||
"\x04Stop\x10\x01\x12\v\n" +
|
||||
@@ -1090,36 +917,34 @@ func file_types_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_types_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
||||
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_types_proto_goTypes = []any{
|
||||
(ContainerAction)(0), // 0: protobuf.ContainerAction
|
||||
(*Container)(nil), // 1: protobuf.Container
|
||||
(*ContainerStat)(nil), // 2: protobuf.ContainerStat
|
||||
(*LogFragment)(nil), // 3: protobuf.LogFragment
|
||||
(*LogEvent)(nil), // 4: protobuf.LogEvent
|
||||
(*SingleMessage)(nil), // 5: protobuf.SingleMessage
|
||||
(*GroupMessage)(nil), // 6: protobuf.GroupMessage
|
||||
(*ComplexMessage)(nil), // 7: protobuf.ComplexMessage
|
||||
(*ContainerEvent)(nil), // 8: protobuf.ContainerEvent
|
||||
(*Host)(nil), // 9: protobuf.Host
|
||||
(*NotificationSubscription)(nil), // 10: protobuf.NotificationSubscription
|
||||
(*NotificationDispatcher)(nil), // 11: protobuf.NotificationDispatcher
|
||||
nil, // 12: protobuf.Container.LabelsEntry
|
||||
nil, // 13: protobuf.Host.LabelsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 15: google.protobuf.Any
|
||||
(ContainerAction)(0), // 0: protobuf.ContainerAction
|
||||
(*Container)(nil), // 1: protobuf.Container
|
||||
(*ContainerStat)(nil), // 2: protobuf.ContainerStat
|
||||
(*LogFragment)(nil), // 3: protobuf.LogFragment
|
||||
(*LogEvent)(nil), // 4: protobuf.LogEvent
|
||||
(*SingleMessage)(nil), // 5: protobuf.SingleMessage
|
||||
(*GroupMessage)(nil), // 6: protobuf.GroupMessage
|
||||
(*ComplexMessage)(nil), // 7: protobuf.ComplexMessage
|
||||
(*ContainerEvent)(nil), // 8: protobuf.ContainerEvent
|
||||
(*Host)(nil), // 9: protobuf.Host
|
||||
nil, // 10: protobuf.Container.LabelsEntry
|
||||
nil, // 11: protobuf.Host.LabelsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 13: google.protobuf.Any
|
||||
}
|
||||
var file_types_proto_depIdxs = []int32{
|
||||
14, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
|
||||
14, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
|
||||
12, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
|
||||
12, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
|
||||
12, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
|
||||
10, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
|
||||
2, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
|
||||
14, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
|
||||
15, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
|
||||
14, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
12, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
|
||||
13, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
|
||||
12, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
3, // 7: protobuf.GroupMessage.fragments:type_name -> protobuf.LogFragment
|
||||
14, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
13, // 9: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
|
||||
12, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
11, // 9: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
|
||||
10, // [10:10] is the sub-list for method output_type
|
||||
10, // [10:10] is the sub-list for method input_type
|
||||
10, // [10:10] is the sub-list for extension type_name
|
||||
@@ -1138,7 +963,7 @@ func file_types_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 13,
|
||||
NumMessages: 11,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
+143
-177
@@ -6,7 +6,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent/pb"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
"google.golang.org/grpc"
|
||||
@@ -26,40 +26,21 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// NotificationConfigHandler handles notification config updates received from the main server
|
||||
type NotificationConfigHandler interface {
|
||||
HandleNotificationConfig(subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error
|
||||
}
|
||||
|
||||
// ClientService is the interface for container operations used by the agent server
|
||||
type ClientService interface {
|
||||
FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error)
|
||||
ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error)
|
||||
Host(ctx context.Context) (container.Host, error)
|
||||
ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error
|
||||
LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error)
|
||||
RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error)
|
||||
SubscribeStats(context.Context, chan<- container.ContainerStat)
|
||||
SubscribeEvents(context.Context, chan<- container.ContainerEvent)
|
||||
SubscribeContainersStarted(context.Context, chan<- container.Container)
|
||||
StreamLogs(context.Context, container.Container, time.Time, container.StdType, chan<- *container.LogEvent) error
|
||||
Attach(context.Context, container.Container, container.ExecEventReader, io.Writer) error
|
||||
Exec(context.Context, container.Container, []string, container.ExecEventReader, io.Writer) error
|
||||
}
|
||||
|
||||
type server struct {
|
||||
service ClientService
|
||||
version string
|
||||
notificationConfigHandler NotificationConfigHandler
|
||||
client container.Client
|
||||
store *container.ContainerStore
|
||||
version string
|
||||
|
||||
pb.UnimplementedAgentServiceServer
|
||||
}
|
||||
|
||||
func newServer(service ClientService, dozzleVersion string, notificationHandler NotificationConfigHandler) pb.AgentServiceServer {
|
||||
func newServer(client container.Client, dozzleVersion string, labels container.ContainerLabels) pb.AgentServiceServer {
|
||||
statsCollector := docker.NewDockerStatsCollector(client, labels)
|
||||
return &server{
|
||||
service: service,
|
||||
version: dozzleVersion,
|
||||
notificationConfigHandler: notificationHandler,
|
||||
client: client,
|
||||
version: dozzleVersion,
|
||||
|
||||
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,18 +50,20 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
since = in.Since.AsTime()
|
||||
}
|
||||
|
||||
c, err := s.service.FindContainer(out.Context(), in.ContainerId, container.ContainerLabels{})
|
||||
c, err := s.store.FindContainer(in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
events := make(chan *container.LogEvent)
|
||||
go func() {
|
||||
defer close(events)
|
||||
s.service.StreamLogs(out.Context(), c, since, container.StdType(in.StreamTypes), events)
|
||||
}()
|
||||
reader, err := s.client.ContainerLogs(out.Context(), in.ContainerId, since, container.StdType(in.StreamTypes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for event := range events {
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(out.Context(), dockerReader, c)
|
||||
|
||||
for event := range g.Events {
|
||||
if event != nil {
|
||||
out.Send(&pb.StreamLogsResponse{
|
||||
Event: logEventToPb(event),
|
||||
@@ -88,23 +71,31 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
select {
|
||||
case e := <-g.Errors:
|
||||
return e
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentService_LogsBetweenDatesServer) error {
|
||||
c, err := s.service.FindContainer(out.Context(), in.ContainerId, container.ContainerLabels{})
|
||||
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
events, err := s.service.LogsBetweenDates(out.Context(), c, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
c, err := s.client.FindContainer(out.Context(), in.ContainerId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(out.Context(), dockerReader, c)
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-events:
|
||||
case event, ok := <-g.Events:
|
||||
if !ok {
|
||||
// Channel closed, exit cleanly
|
||||
return nil
|
||||
@@ -112,6 +103,8 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
|
||||
out.Send(&pb.StreamLogsResponse{
|
||||
Event: logEventToPb(event),
|
||||
})
|
||||
case e := <-g.Errors:
|
||||
return e
|
||||
case <-out.Context().Done():
|
||||
return nil
|
||||
}
|
||||
@@ -119,24 +112,16 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
|
||||
}
|
||||
|
||||
func (s *server) StreamRawBytes(in *pb.StreamRawBytesRequest, out pb.AgentService_StreamRawBytesServer) error {
|
||||
c, err := s.service.FindContainer(out.Context(), in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
|
||||
reader, err := s.service.RawLogs(out.Context(), c, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -157,7 +142,7 @@ func (s *server) StreamRawBytes(in *pb.StreamRawBytesRequest, out pb.AgentServic
|
||||
func (s *server) StreamEvents(in *pb.StreamEventsRequest, out pb.AgentService_StreamEventsServer) error {
|
||||
events := make(chan container.ContainerEvent)
|
||||
|
||||
s.service.SubscribeEvents(out.Context(), events)
|
||||
s.store.SubscribeEvents(out.Context(), events)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -179,7 +164,7 @@ func (s *server) StreamEvents(in *pb.StreamEventsRequest, out pb.AgentService_St
|
||||
func (s *server) StreamStats(in *pb.StreamStatsRequest, out pb.AgentService_StreamStatsServer) error {
|
||||
stats := make(chan container.ContainerStat)
|
||||
|
||||
s.service.SubscribeStats(out.Context(), stats)
|
||||
s.store.SubscribeStats(out.Context(), stats)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -208,13 +193,13 @@ func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := s.service.FindContainer(ctx, in.ContainerId, labels)
|
||||
container, err := s.store.FindContainer(in.ContainerId, labels)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
proto := c.ToProto()
|
||||
c := container.ToProto()
|
||||
return &pb.FindContainerResponse{
|
||||
Container: &proto,
|
||||
Container: &c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -226,15 +211,15 @@ func (s *server) ListContainers(ctx context.Context, in *pb.ListContainersReques
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := s.service.ListContainers(ctx, labels)
|
||||
containers, err := s.store.ListContainers(labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pbContainers []*pb.Container
|
||||
for _, c := range containers {
|
||||
proto := c.ToProto()
|
||||
pbContainers = append(pbContainers, &proto)
|
||||
for _, container := range containers {
|
||||
c := container.ToProto()
|
||||
pbContainers = append(pbContainers, &c)
|
||||
}
|
||||
|
||||
return &pb.ListContainersResponse{
|
||||
@@ -243,10 +228,7 @@ func (s *server) ListContainers(ctx context.Context, in *pb.ListContainersReques
|
||||
}
|
||||
|
||||
func (s *server) HostInfo(ctx context.Context, in *pb.HostInfoRequest) (*pb.HostInfoResponse, error) {
|
||||
host, err := s.service.Host(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
host := s.client.Host()
|
||||
return &pb.HostInfoResponse{
|
||||
Host: &pb.Host{
|
||||
Id: host.ID,
|
||||
@@ -262,7 +244,7 @@ func (s *server) HostInfo(ctx context.Context, in *pb.HostInfoRequest) (*pb.Host
|
||||
func (s *server) StreamContainerStarted(in *pb.StreamContainerStartedRequest, out pb.AgentService_StreamContainerStartedServer) error {
|
||||
containers := make(chan container.Container)
|
||||
|
||||
go s.service.SubscribeContainersStarted(out.Context(), containers)
|
||||
go s.store.SubscribeNewContainers(out.Context(), containers)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -293,12 +275,8 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid action")
|
||||
}
|
||||
|
||||
c, err := s.service.FindContainer(ctx, in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
err := s.client.ContainerActions(ctx, action, in.ContainerId)
|
||||
|
||||
err = s.service.ContainerAction(ctx, c, action)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
@@ -306,63 +284,63 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
|
||||
return &pb.ContainerActionResponse{}, nil
|
||||
}
|
||||
|
||||
// terminalMessage represents a message from a terminal gRPC stream (exec or attach)
|
||||
type terminalMessage interface {
|
||||
GetStdin() []byte
|
||||
GetResize() *pb.ResizePayload
|
||||
}
|
||||
|
||||
// protoEventReader converts gRPC protobuf messages directly to ExecEvents (no JSON)
|
||||
type protoEventReader struct {
|
||||
recv func() (terminalMessage, error)
|
||||
}
|
||||
|
||||
func (r *protoEventReader) ReadEvent() (*container.ExecEvent, error) {
|
||||
msg, err := r.recv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stdin := msg.GetStdin(); stdin != nil {
|
||||
return &container.ExecEvent{Type: "userinput", Data: string(stdin)}, nil
|
||||
} else if resize := msg.GetResize(); resize != nil {
|
||||
return &container.ExecEvent{Type: "resize", Width: uint(resize.Width), Height: uint(resize.Height)}, nil
|
||||
}
|
||||
|
||||
// Skip unknown message types
|
||||
return r.ReadEvent()
|
||||
}
|
||||
|
||||
// terminalStreamWriter adapts a gRPC terminal stream to io.Writer
|
||||
type terminalStreamWriter struct {
|
||||
send func([]byte) error
|
||||
}
|
||||
|
||||
func (w *terminalStreamWriter) Write(p []byte) (int, error) {
|
||||
if err := w.send(p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (s *server) ContainerExec(stream pb.AgentService_ContainerExecServer) error {
|
||||
request, err := stream.Recv()
|
||||
if err != nil {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
c, err := s.service.FindContainer(stream.Context(), request.ContainerId, container.ContainerLabels{})
|
||||
cancelCtx, cancel := context.WithCancel(stream.Context())
|
||||
session, err := s.client.ContainerExec(cancelCtx, request.ContainerId, request.Command)
|
||||
if err != nil {
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
reader := &protoEventReader{recv: func() (terminalMessage, error) { return stream.Recv() }}
|
||||
writer := &terminalStreamWriter{send: func(p []byte) error { return stream.Send(&pb.ContainerExecResponse{Stdout: p}) }}
|
||||
|
||||
if err := s.service.Exec(stream.Context(), c, request.Command, reader, writer); err != nil {
|
||||
cancel()
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Read from container and send to client
|
||||
wg.Go(func() {
|
||||
defer cancel()
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
n, err := session.Reader.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := stream.Send(&pb.ContainerExecResponse{Stdout: buffer[:n]}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Read from client stream and handle stdin/resize
|
||||
wg.Go(func() {
|
||||
defer cancel()
|
||||
defer session.Writer.Close()
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch payload := req.Payload.(type) {
|
||||
case *pb.ContainerExecRequest_Stdin:
|
||||
if _, err := session.Writer.Write(payload.Stdin); err != nil {
|
||||
log.Error().Err(err).Msg("error writing stdin to container")
|
||||
return
|
||||
}
|
||||
case *pb.ContainerExecRequest_Resize:
|
||||
if err := session.Resize(uint(payload.Resize.Width), uint(payload.Resize.Height)); err != nil {
|
||||
log.Error().Err(err).Msg("error resizing terminal")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -372,73 +350,61 @@ func (s *server) ContainerAttach(stream pb.AgentService_ContainerAttachServer) e
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
c, err := s.service.FindContainer(stream.Context(), request.ContainerId, container.ContainerLabels{})
|
||||
cancelCtx, cancel := context.WithCancel(stream.Context())
|
||||
session, err := s.client.ContainerAttach(cancelCtx, request.ContainerId)
|
||||
if err != nil {
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
reader := &protoEventReader{recv: func() (terminalMessage, error) { return stream.Recv() }}
|
||||
writer := &terminalStreamWriter{send: func(p []byte) error { return stream.Send(&pb.ContainerAttachResponse{Stdout: p}) }}
|
||||
|
||||
if err := s.service.Attach(stream.Context(), c, reader, writer); err != nil {
|
||||
cancel()
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Read from container and send to client
|
||||
wg.Go(func() {
|
||||
defer cancel()
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
n, err := session.Reader.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := stream.Send(&pb.ContainerAttachResponse{Stdout: buffer[:n]}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Read from client stream and handle stdin/resize
|
||||
wg.Go(func() {
|
||||
defer cancel()
|
||||
defer session.Writer.Close()
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch payload := req.Payload.(type) {
|
||||
case *pb.ContainerAttachRequest_Stdin:
|
||||
if _, err := session.Writer.Write(payload.Stdin); err != nil {
|
||||
log.Error().Err(err).Msg("error writing stdin to container")
|
||||
return
|
||||
}
|
||||
case *pb.ContainerAttachRequest_Resize:
|
||||
if err := session.Resize(uint(payload.Resize.Width), uint(payload.Resize.Height)); err != nil {
|
||||
log.Error().Err(err).Msg("error resizing terminal")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) UpdateNotificationConfig(ctx context.Context, req *pb.UpdateNotificationConfigRequest) (*pb.UpdateNotificationConfigResponse, error) {
|
||||
if s.notificationConfigHandler == nil {
|
||||
log.Warn().Msg("No notification config handler registered, ignoring config update")
|
||||
return &pb.UpdateNotificationConfigResponse{}, nil
|
||||
}
|
||||
|
||||
// Validate request sizes to prevent memory exhaustion
|
||||
const maxSubscriptions = 1000
|
||||
const maxDispatchers = 100
|
||||
if len(req.Subscriptions) > maxSubscriptions {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "too many subscriptions: %d (max %d)", len(req.Subscriptions), maxSubscriptions)
|
||||
}
|
||||
if len(req.Dispatchers) > maxDispatchers {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "too many dispatchers: %d (max %d)", len(req.Dispatchers), maxDispatchers)
|
||||
}
|
||||
|
||||
// Convert proto subscriptions to types
|
||||
subscriptions := make([]types.SubscriptionConfig, len(req.Subscriptions))
|
||||
for i, sub := range req.Subscriptions {
|
||||
subscriptions[i] = types.SubscriptionConfig{
|
||||
ID: int(sub.Id),
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
DispatcherID: int(sub.DispatcherId),
|
||||
LogExpression: sub.LogExpression,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proto dispatchers to types
|
||||
dispatchers := make([]types.DispatcherConfig, len(req.Dispatchers))
|
||||
for i, d := range req.Dispatchers {
|
||||
dispatchers[i] = types.DispatcherConfig{
|
||||
ID: int(d.Id),
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.Url,
|
||||
Template: d.Template,
|
||||
}
|
||||
}
|
||||
|
||||
// Call the handler (handler is responsible for persisting if needed)
|
||||
if err := s.notificationConfigHandler.HandleNotificationConfig(subscriptions, dispatchers); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to handle notification config")
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
log.Info().Int("subscriptions", len(subscriptions)).Int("dispatchers", len(dispatchers)).Msg("Updated notification config from main server")
|
||||
return &pb.UpdateNotificationConfigResponse{}, nil
|
||||
}
|
||||
|
||||
func NewServer(service ClientService, certificates tls.Certificate, dozzleVersion string, notificationHandler NotificationConfigHandler) (*grpc.Server, error) {
|
||||
func NewServer(client container.Client, certificates tls.Certificate, dozzleVersion string, labels container.ContainerLabels) (*grpc.Server, error) {
|
||||
caCertPool := x509.NewCertPool()
|
||||
c, err := x509.ParseCertificate(certificates.Certificate[0])
|
||||
if err != nil {
|
||||
@@ -457,7 +423,7 @@ func NewServer(service ClientService, certificates tls.Certificate, dozzleVersio
|
||||
creds := credentials.NewTLS(tlsConfig)
|
||||
|
||||
grpcServer := grpc.NewServer(grpc.Creds(creds))
|
||||
pb.RegisterAgentServiceServer(grpcServer, newServer(service, dozzleVersion, notificationHandler))
|
||||
pb.RegisterAgentServiceServer(grpcServer, newServer(client, dozzleVersion, labels))
|
||||
|
||||
return grpcServer, nil
|
||||
}
|
||||
|
||||
@@ -46,9 +46,7 @@ func (p *proxyAuthContext) AuthMiddleware(next http.Handler) http.Handler {
|
||||
if r.Header.Get(p.headerUser) != "" {
|
||||
containerFilter, err := container.ParseContainerFilter(r.Header.Get(p.headerFilter))
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("filter", r.Header.Get(p.headerFilter)).Msg("Failed to parse container filter")
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
log.Fatal().Str("filter", r.Header.Get(p.headerFilter)).Msg("Failed to parse container filter")
|
||||
}
|
||||
userRoles := All
|
||||
if strings.TrimSpace(r.Header.Get(p.headerRoles)) != "" {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestForwardProxyAuthRejectsInvalidFilter(t *testing.T) {
|
||||
auth := NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name", "Remote-Filter", "Remote-Roles")
|
||||
called := false
|
||||
handler := auth.AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Remote-User", "alice")
|
||||
req.Header.Set("Remote-Filter", "invalid-filter")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
handler.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
require.False(t, called)
|
||||
}
|
||||
|
||||
func TestUserFromContextInvalidFilterReturnsNil(t *testing.T) {
|
||||
tokenAuth := jwtauth.New("HS256", []byte("secret"), nil)
|
||||
_, tokenString, err := tokenAuth.Encode(map[string]interface{}{
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"name": "Alice",
|
||||
"filter": "invalid-filter",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := jwtauth.Verifier(tokenAuth)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if UserFromContext(r.Context()) == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenString)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
handler.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, resp.Code)
|
||||
}
|
||||
+10
-3
@@ -3,6 +3,8 @@ package auth
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -170,9 +172,15 @@ func (u *UserDatabase) FindByPassword(username, password string) *User {
|
||||
return user
|
||||
}
|
||||
|
||||
func sha256sum(s string) string {
|
||||
bytes := sha256.Sum256([]byte(s))
|
||||
return hex.EncodeToString(bytes[:])
|
||||
}
|
||||
|
||||
func CompareHashAndPassword(hash, password string) bool {
|
||||
if len(hash) == 64 {
|
||||
log.Fatal().Msg("sha256 passwords are no longer supported. Please use bcrypt. See https://github.com/amir20/dozzle/security/advisories/GHSA-w7qr-q9fh-fj35 for more details.")
|
||||
log.Warn().Msg("Using sha256sum for password comparison. Consider using a more secure hash algorithm to protected against brute-force attacks. See https://github.com/amir20/dozzle/security/advisories/GHSA-w7qr-q9fh-fj35 for more details.")
|
||||
return hash == sha256sum(password)
|
||||
}
|
||||
|
||||
if len(hash) == 60 {
|
||||
@@ -204,8 +212,7 @@ func UserFromContext(ctx context.Context) *User {
|
||||
if filter, ok := claims["filter"].(string); ok {
|
||||
containerFilter, err = container.ParseContainerFilter(filter)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("filter", filter).Msg("Failed to parse container filter")
|
||||
return nil
|
||||
log.Fatal().Err(err).Str("filter", filter).Msg("Failed to parse container filter")
|
||||
}
|
||||
}
|
||||
roles := None
|
||||
|
||||
@@ -41,11 +41,6 @@ type ExecEvent struct {
|
||||
Height uint `json:"height,omitempty"`
|
||||
}
|
||||
|
||||
// ExecEventReader provides structured exec events (userinput, resize)
|
||||
type ExecEventReader interface {
|
||||
ReadEvent() (*ExecEvent, error)
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
ListContainers(context.Context, ContainerLabels) ([]Container, error)
|
||||
FindContainer(context.Context, string) (Container, error)
|
||||
|
||||
@@ -171,52 +171,47 @@ func (s *ContainerStore) FindContainer(id string, labels ContainerLabels) (Conta
|
||||
}
|
||||
}
|
||||
|
||||
var updated bool
|
||||
container, found := s.containers.Compute(id, func(c *Container, loaded bool) (*Container, xsync.ComputeOp) {
|
||||
if !loaded {
|
||||
return nil, xsync.CancelOp
|
||||
}
|
||||
if c.FullyLoaded {
|
||||
return c, xsync.CancelOp
|
||||
}
|
||||
log.Debug().Str("id", id).Msg("container is not fully loaded, fetching it")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
if newContainer, err := s.client.FindContainer(ctx, id); err == nil {
|
||||
updated = true
|
||||
return &newContainer, xsync.UpdateOp
|
||||
} else {
|
||||
log.Error().Err(err).Msg("failed to fetch container")
|
||||
return c, xsync.CancelOp
|
||||
}
|
||||
})
|
||||
if container, ok := s.containers.Load(id); ok {
|
||||
if !container.FullyLoaded {
|
||||
log.Debug().Str("id", id).Msg("container is not fully loaded, fetching it")
|
||||
if newContainer, ok := s.containers.Compute(id, func(c *Container, loaded bool) (*Container, xsync.ComputeOp) {
|
||||
if loaded {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
if newContainer, err := s.client.FindContainer(ctx, id); err == nil {
|
||||
return &newContainer, xsync.UpdateOp
|
||||
} else {
|
||||
log.Error().Err(err).Msg("failed to fetch container")
|
||||
return c, xsync.CancelOp
|
||||
}
|
||||
}
|
||||
return c, xsync.CancelOp
|
||||
}); ok {
|
||||
go func() {
|
||||
event := ContainerEvent{
|
||||
Name: "update",
|
||||
Host: newContainer.Host,
|
||||
ActorID: id,
|
||||
Container: newContainer,
|
||||
}
|
||||
|
||||
if !found {
|
||||
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
|
||||
select {
|
||||
case events <- event:
|
||||
case <-c.Done():
|
||||
s.subscribers.Delete(c)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}()
|
||||
return *newContainer, nil
|
||||
}
|
||||
}
|
||||
return *container, nil
|
||||
} else {
|
||||
log.Warn().Str("id", id).Msg("container not found")
|
||||
return Container{}, ErrContainerNotFound
|
||||
}
|
||||
|
||||
if updated {
|
||||
go func() {
|
||||
event := ContainerEvent{
|
||||
Name: "update",
|
||||
Host: container.Host,
|
||||
ActorID: id,
|
||||
Container: container,
|
||||
}
|
||||
|
||||
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
|
||||
select {
|
||||
case events <- event:
|
||||
case <-c.Done():
|
||||
s.subscribers.Delete(c)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
return *container, nil
|
||||
}
|
||||
|
||||
func (s *ContainerStore) Client() Client {
|
||||
|
||||
@@ -16,7 +16,7 @@ var logLevels = [][]string{
|
||||
{"warn", "warning", "wrn"},
|
||||
{"info", "inf"},
|
||||
{"debug", "dbg"},
|
||||
{"trace", "verbose", "ver", "vbs"},
|
||||
{"trace"},
|
||||
{"fatal", "sev", "severe", "crit", "critical"},
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func init() {
|
||||
func guessLogLevel(logEvent *LogEvent) string {
|
||||
switch value := logEvent.Message.(type) {
|
||||
case string:
|
||||
value = StripANSI(value)
|
||||
value = stripANSI(value)
|
||||
value = timestampRegex.ReplaceAllString(value, "")
|
||||
for _, levelGroup := range logLevels {
|
||||
first := levelGroup[0]
|
||||
@@ -125,7 +125,7 @@ func guessLogLevel(logEvent *LogEvent) string {
|
||||
}
|
||||
|
||||
func normalizeLogLevel(level string) string {
|
||||
level = StripANSI(level)
|
||||
level = stripANSI(level)
|
||||
level = strings.ToLower(level)
|
||||
if _, ok := SupportedLogLevels[level]; ok {
|
||||
return level
|
||||
|
||||
@@ -8,6 +8,6 @@ const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)
|
||||
|
||||
var re = regexp.MustCompile(ansi)
|
||||
|
||||
func StripANSI(str string) string {
|
||||
func stripANSI(str string) string {
|
||||
return re.ReplaceAllString(str, "")
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type Container struct {
|
||||
MemoryLimit uint64 `json:"memoryLimit"`
|
||||
CPULimit float64 `json:"cpuLimit"`
|
||||
Group string `json:"group,omitempty"`
|
||||
FullyLoaded bool `json:"-"`
|
||||
FullyLoaded bool `json:"-,omitempty"`
|
||||
}
|
||||
|
||||
func (container Container) ToProto() pb.Container {
|
||||
|
||||
@@ -386,8 +386,6 @@ func newContainer(c docker.Summary, host string) container.Container {
|
||||
name := "no name"
|
||||
if c.Labels["dev.dozzle.name"] != "" {
|
||||
name = c.Labels["dev.dozzle.name"]
|
||||
} else if c.Labels["coolify.resourceName"] != "" {
|
||||
name = c.Labels["coolify.resourceName"]
|
||||
} else if len(c.Names) > 0 {
|
||||
name = strings.TrimPrefix(c.Names[0], "/")
|
||||
}
|
||||
@@ -395,8 +393,6 @@ func newContainer(c docker.Summary, host string) container.Container {
|
||||
group := ""
|
||||
if c.Labels["dev.dozzle.group"] != "" {
|
||||
group = c.Labels["dev.dozzle.group"]
|
||||
} else if c.Labels["coolify.projectName"] != "" {
|
||||
group = c.Labels["coolify.projectName"]
|
||||
}
|
||||
return container.Container{
|
||||
ID: c.ID[:12],
|
||||
@@ -416,8 +412,6 @@ func newContainerFromJSON(c docker.InspectResponse, host string) container.Conta
|
||||
name := "no name"
|
||||
if c.Config.Labels["dev.dozzle.name"] != "" {
|
||||
name = c.Config.Labels["dev.dozzle.name"]
|
||||
} else if c.Config.Labels["coolify.resourceName"] != "" {
|
||||
name = c.Config.Labels["coolify.resourceName"]
|
||||
} else if len(c.Name) > 0 {
|
||||
name = strings.TrimPrefix(c.Name, "/")
|
||||
}
|
||||
@@ -425,8 +419,6 @@ func newContainerFromJSON(c docker.InspectResponse, host string) container.Conta
|
||||
group := ""
|
||||
if c.Config.Labels["dev.dozzle.group"] != "" {
|
||||
group = c.Config.Labels["dev.dozzle.group"]
|
||||
} else if c.Config.Labels["coolify.projectName"] != "" {
|
||||
group = c.Config.Labels["coolify.projectName"]
|
||||
}
|
||||
|
||||
container := container.Container{
|
||||
|
||||
@@ -260,105 +260,3 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) {
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_newContainer_labelPriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
labels map[string]string
|
||||
containerName string
|
||||
expectedName string
|
||||
expectedGroup string
|
||||
}{
|
||||
{
|
||||
name: "dozzle labels take priority",
|
||||
labels: map[string]string{"dev.dozzle.name": "dozzle-name", "coolify.resourceName": "coolify-name", "dev.dozzle.group": "dozzle-group", "coolify.projectName": "coolify-project"},
|
||||
containerName: "/docker-name",
|
||||
expectedName: "dozzle-name",
|
||||
expectedGroup: "dozzle-group",
|
||||
},
|
||||
{
|
||||
name: "coolify labels as fallback",
|
||||
labels: map[string]string{"coolify.resourceName": "coolify-name", "coolify.projectName": "coolify-project"},
|
||||
containerName: "/docker-name",
|
||||
expectedName: "coolify-name",
|
||||
expectedGroup: "coolify-project",
|
||||
},
|
||||
{
|
||||
name: "docker name as final fallback",
|
||||
labels: map[string]string{},
|
||||
containerName: "/docker-name",
|
||||
expectedName: "docker-name",
|
||||
expectedGroup: "",
|
||||
},
|
||||
{
|
||||
name: "coolify name with dozzle group",
|
||||
labels: map[string]string{"coolify.resourceName": "coolify-name", "dev.dozzle.group": "dozzle-group"},
|
||||
containerName: "/docker-name",
|
||||
expectedName: "coolify-name",
|
||||
expectedGroup: "dozzle-group",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
summary := docker.Summary{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Names: []string{tt.containerName},
|
||||
Labels: tt.labels,
|
||||
}
|
||||
c := newContainer(summary, "localhost")
|
||||
assert.Equal(t, tt.expectedName, c.Name)
|
||||
assert.Equal(t, tt.expectedGroup, c.Group)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newContainerFromJSON_labelPriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
labels map[string]string
|
||||
containerName string
|
||||
expectedName string
|
||||
expectedGroup string
|
||||
}{
|
||||
{
|
||||
name: "dozzle labels take priority",
|
||||
labels: map[string]string{"dev.dozzle.name": "dozzle-name", "coolify.resourceName": "coolify-name", "dev.dozzle.group": "dozzle-group", "coolify.projectName": "coolify-project"},
|
||||
containerName: "/docker-name",
|
||||
expectedName: "dozzle-name",
|
||||
expectedGroup: "dozzle-group",
|
||||
},
|
||||
{
|
||||
name: "coolify labels as fallback",
|
||||
labels: map[string]string{"coolify.resourceName": "coolify-name", "coolify.projectName": "coolify-project"},
|
||||
containerName: "/docker-name",
|
||||
expectedName: "coolify-name",
|
||||
expectedGroup: "coolify-project",
|
||||
},
|
||||
{
|
||||
name: "docker name as final fallback",
|
||||
labels: map[string]string{},
|
||||
containerName: "/docker-name",
|
||||
expectedName: "docker-name",
|
||||
expectedGroup: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||
json := docker.InspectResponse{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Name: tt.containerName,
|
||||
State: state,
|
||||
HostConfig: &docker.HostConfig{},
|
||||
},
|
||||
Config: &docker.Config{Labels: tt.labels},
|
||||
}
|
||||
c := newContainerFromJSON(json, "localhost")
|
||||
assert.Equal(t, tt.expectedName, c.Name)
|
||||
assert.Equal(t, tt.expectedGroup, c.Group)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
package dispatcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// CloudDispatcher sends notifications to Dozzle Cloud
|
||||
type CloudDispatcher struct {
|
||||
Name string
|
||||
URL string
|
||||
APIKey string
|
||||
Prefix string
|
||||
ExpiresAt *time.Time
|
||||
client *http.Client
|
||||
blockedUntil atomic.Int64
|
||||
}
|
||||
|
||||
// NewCloudDispatcher creates a new cloud dispatcher
|
||||
func NewCloudDispatcher(name string, apiKey string, prefix string, expiresAt *time.Time) (*CloudDispatcher, error) {
|
||||
url := os.Getenv("DOLIGENCE_URL")
|
||||
if url == "" {
|
||||
url = "https://doligence.dozzle.dev"
|
||||
}
|
||||
url = url + "/api/events"
|
||||
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("API key is required for cloud dispatcher")
|
||||
}
|
||||
|
||||
return &CloudDispatcher{
|
||||
Name: name,
|
||||
URL: url,
|
||||
APIKey: apiKey,
|
||||
Prefix: prefix,
|
||||
ExpiresAt: expiresAt,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const defaultRetryAfter = 60 * time.Second
|
||||
|
||||
// Send sends a notification to Dozzle Cloud
|
||||
func (c *CloudDispatcher) Send(ctx context.Context, notification types.Notification) error {
|
||||
if blockedUntil := c.blockedUntil.Load(); blockedUntil > 0 && time.Now().UnixNano() < blockedUntil {
|
||||
t := time.Unix(0, blockedUntil)
|
||||
log.Debug().
|
||||
Str("cloud", c.Name).
|
||||
Time("blocked_until", t).
|
||||
Msg("circuit breaker open, skipping cloud request")
|
||||
return fmt.Errorf("cloud dispatcher rate limited, retry after %s", t.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(notification)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.URL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-API-Key", c.APIKey)
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send to cloud: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
retryAfter := defaultRetryAfter
|
||||
if ra := resp.Header.Get("Retry-After"); ra != "" {
|
||||
if seconds, err := strconv.Atoi(ra); err == nil {
|
||||
retryAfter = time.Duration(seconds) * time.Second
|
||||
}
|
||||
}
|
||||
c.blockedUntil.Store(time.Now().Add(retryAfter).UnixNano())
|
||||
log.Warn().
|
||||
Str("cloud", c.Name).
|
||||
Dur("retry_after", retryAfter).
|
||||
Msg("rate limited by cloud, circuit breaker tripped")
|
||||
return fmt.Errorf("cloud rate limited, backing off for %s", retryAfter)
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
limitedReader := io.LimitReader(resp.Body, 1024*1024)
|
||||
responseBody, _ := io.ReadAll(limitedReader)
|
||||
log.Debug().
|
||||
Str("cloud", c.Name).
|
||||
Str("url", c.URL).
|
||||
Int("status_code", resp.StatusCode).
|
||||
Str("payload", string(payload)).
|
||||
Str("response_body", string(responseBody)).
|
||||
Msg("cloud returned non-success status code")
|
||||
return fmt.Errorf("cloud returned status code %d: %s", resp.StatusCode, string(responseBody))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package dispatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/amir20/dozzle/types"
|
||||
)
|
||||
|
||||
// Dispatcher is responsible for sending notifications to external systems
|
||||
type Dispatcher interface {
|
||||
// Send sends a notification to the configured destination
|
||||
Send(ctx context.Context, notification types.Notification) error
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package dispatcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// WebhookDispatcher sends notifications to a webhook URL
|
||||
type WebhookDispatcher struct {
|
||||
Name string
|
||||
URL string
|
||||
Template *template.Template
|
||||
TemplateText string // Original template string for serialization
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewWebhookDispatcher creates a new webhook dispatcher
|
||||
// If templateStr is empty, the notification will be marshaled as JSON directly
|
||||
func NewWebhookDispatcher(name, url, templateStr string) (*WebhookDispatcher, error) {
|
||||
w := &WebhookDispatcher{
|
||||
Name: name,
|
||||
URL: url,
|
||||
TemplateText: templateStr,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
if templateStr != "" {
|
||||
tmpl, err := template.New("webhook").Parse(templateStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse template: %w", err)
|
||||
}
|
||||
w.Template = tmpl
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// TestResult contains the result of a webhook test
|
||||
type TestResult struct {
|
||||
Success bool
|
||||
StatusCode int
|
||||
Error string
|
||||
}
|
||||
|
||||
// Send sends a notification to the webhook URL
|
||||
func (w *WebhookDispatcher) Send(ctx context.Context, notification types.Notification) error {
|
||||
result := w.SendTest(ctx, notification)
|
||||
if !result.Success {
|
||||
return fmt.Errorf("webhook notification failed: %s", result.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendTest sends a notification and returns detailed result for testing
|
||||
func (w *WebhookDispatcher) SendTest(ctx context.Context, notification types.Notification) TestResult {
|
||||
var payload []byte
|
||||
var err error
|
||||
|
||||
if w.Template != nil {
|
||||
var buf bytes.Buffer
|
||||
if err := w.Template.Execute(&buf, notification); err != nil {
|
||||
return TestResult{Success: false, Error: fmt.Sprintf("failed to execute template: %v", err)}
|
||||
}
|
||||
payload = buf.Bytes()
|
||||
} else {
|
||||
payload, err = json.Marshal(notification)
|
||||
if err != nil {
|
||||
return TestResult{Success: false, Error: fmt.Sprintf("failed to marshal notification: %v", err)}
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.URL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return TestResult{Success: false, Error: fmt.Sprintf("failed to create request: %v", err)}
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := w.client.Do(req)
|
||||
if err != nil {
|
||||
return TestResult{Success: false, Error: fmt.Sprintf("failed to send webhook: %v", err)}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
// Limit response body to 1MB to prevent memory exhaustion
|
||||
limitedReader := io.LimitReader(resp.Body, 1024*1024)
|
||||
responseBody, _ := io.ReadAll(limitedReader)
|
||||
log.Debug().
|
||||
Str("webhook", w.Name).
|
||||
Str("url", w.URL).
|
||||
Int("status_code", resp.StatusCode).
|
||||
Str("payload", string(payload)).
|
||||
Str("response_body", string(responseBody)).
|
||||
Msg("webhook returned non-success status code")
|
||||
return TestResult{
|
||||
Success: false,
|
||||
StatusCode: resp.StatusCode,
|
||||
Error: fmt.Sprintf("webhook returned status code %d: %s", resp.StatusCode, string(responseBody)),
|
||||
}
|
||||
}
|
||||
|
||||
return TestResult{Success: true, StatusCode: resp.StatusCode}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ContainerMatcher is an interface for checking if a container should be listened to
|
||||
type ContainerMatcher interface {
|
||||
ShouldListenToContainer(c container.Container) bool
|
||||
}
|
||||
|
||||
// ContainerLogListener manages active log streams for containers across multiple clients
|
||||
type ContainerLogListener struct {
|
||||
clients []container_support.ClientService
|
||||
containerClients *xsync.Map[string, container_support.ClientService] // containerID -> owning client
|
||||
activeStreams *xsync.Map[string, context.CancelFunc] // containerID -> cancel function
|
||||
matcher ContainerMatcher
|
||||
logChannel chan *container.LogEvent
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewContainerLogListener creates a new listener for multiple clients
|
||||
func NewContainerLogListener(ctx context.Context, clients []container_support.ClientService) *ContainerLogListener {
|
||||
return &ContainerLogListener{
|
||||
clients: clients,
|
||||
containerClients: xsync.NewMap[string, container_support.ClientService](),
|
||||
activeStreams: xsync.NewMap[string, context.CancelFunc](),
|
||||
logChannel: make(chan *container.LogEvent, 1000),
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins listening for container events and processes log streams
|
||||
func (l *ContainerLogListener) Start(matcher ContainerMatcher) error {
|
||||
l.matcher = matcher
|
||||
|
||||
// Subscribe to new containers from all clients
|
||||
containerChan := make(chan container.Container, 10)
|
||||
|
||||
// Get all current containers from all clients
|
||||
for _, client := range l.clients {
|
||||
containers, err := client.ListContainers(l.ctx, nil)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to list containers from client")
|
||||
continue
|
||||
}
|
||||
|
||||
// Start listening to containers that match
|
||||
for _, c := range containers {
|
||||
if l.matcher.ShouldListenToContainer(c) {
|
||||
l.startListening(c, client)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to new containers from this client
|
||||
client.SubscribeContainersStarted(l.ctx, containerChan)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
case c := <-containerChan:
|
||||
if l.matcher.ShouldListenToContainer(c) {
|
||||
l.startListeningByID(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStreams updates which containers to listen to based on current matcher rules
|
||||
func (l *ContainerLogListener) UpdateStreams() error {
|
||||
// Get all current containers from all clients
|
||||
for _, client := range l.clients {
|
||||
containers, err := client.ListContainers(l.ctx, nil)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to list containers from client")
|
||||
continue
|
||||
}
|
||||
|
||||
// Check each container against matcher
|
||||
for _, c := range containers {
|
||||
shouldListen := l.matcher.ShouldListenToContainer(c)
|
||||
isListening := l.isListening(c.ID)
|
||||
|
||||
if shouldListen && !isListening {
|
||||
l.startListening(c, client)
|
||||
} else if !shouldListen && isListening {
|
||||
l.stopListening(c.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startListening starts listening to a container's logs with a known client
|
||||
func (l *ContainerLogListener) startListening(c container.Container, client container_support.ClientService) {
|
||||
streamCtx, cancel := context.WithCancel(l.ctx)
|
||||
|
||||
// Only store if not already present
|
||||
_, loaded := l.activeStreams.LoadOrStore(c.ID, cancel)
|
||||
if loaded {
|
||||
cancel() // Already listening, cancel the new context
|
||||
return
|
||||
}
|
||||
|
||||
l.containerClients.Store(c.ID, client)
|
||||
|
||||
go func() {
|
||||
log.Debug().Str("containerID", c.ID).Str("name", c.Name).Msg("Started listening to container")
|
||||
if err := client.StreamLogs(streamCtx, c, time.Now(), container.STDALL, l.logChannel); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) {
|
||||
log.Error().Err(err).Str("containerID", c.ID).Msg("Error streaming logs")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// startListeningByID finds the client for a container and starts listening
|
||||
func (l *ContainerLogListener) startListeningByID(c container.Container) {
|
||||
for _, client := range l.clients {
|
||||
if found, err := client.FindContainer(l.ctx, c.ID, nil); err == nil {
|
||||
l.startListening(found, client)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Warn().Str("containerID", c.ID).Msg("Could not find client for container")
|
||||
}
|
||||
|
||||
// stopListening stops listening to a container's logs
|
||||
func (l *ContainerLogListener) stopListening(containerID string) {
|
||||
if cancel, exists := l.activeStreams.LoadAndDelete(containerID); exists {
|
||||
cancel()
|
||||
l.containerClients.Delete(containerID)
|
||||
log.Debug().Str("containerID", containerID).Msg("Stopped listening to container")
|
||||
}
|
||||
}
|
||||
|
||||
// isListening returns true if listening to a container
|
||||
func (l *ContainerLogListener) isListening(containerID string) bool {
|
||||
_, exists := l.activeStreams.Load(containerID)
|
||||
return exists
|
||||
}
|
||||
|
||||
// FindContainer finds a container by ID using the client that owns it
|
||||
func (l *ContainerLogListener) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
|
||||
client, exists := l.containerClients.Load(id)
|
||||
if !exists {
|
||||
return container.Container{}, fmt.Errorf("container %s not found in any client", id)
|
||||
}
|
||||
|
||||
return client.FindContainer(ctx, id, labels)
|
||||
}
|
||||
|
||||
// FindContainerWithHost finds a container and its host by container ID
|
||||
func (l *ContainerLogListener) FindContainerWithHost(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, container.Host, error) {
|
||||
client, exists := l.containerClients.Load(id)
|
||||
if !exists {
|
||||
return container.Container{}, container.Host{}, fmt.Errorf("container %s not found in any client", id)
|
||||
}
|
||||
|
||||
c, err := client.FindContainer(ctx, id, labels)
|
||||
if err != nil {
|
||||
return container.Container{}, container.Host{}, err
|
||||
}
|
||||
|
||||
host, err := client.Host(ctx)
|
||||
if err != nil {
|
||||
return container.Container{}, container.Host{}, fmt.Errorf("failed to get host for container %s: %w", id, err)
|
||||
}
|
||||
|
||||
return c, host, nil
|
||||
}
|
||||
|
||||
// LogChannel returns the channel for log events
|
||||
func (l *ContainerLogListener) LogChannel() <-chan *container.LogEvent {
|
||||
return l.logChannel
|
||||
}
|
||||
|
||||
// ListContainers returns all containers from all clients
|
||||
func (l *ContainerLogListener) ListContainers() []container.Container {
|
||||
var result []container.Container
|
||||
for _, client := range l.clients {
|
||||
containers, err := client.ListContainers(l.ctx, nil)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to list containers from client")
|
||||
continue
|
||||
}
|
||||
result = append(result, containers...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
+254
-509
@@ -3,578 +3,323 @@ package notification
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
"github.com/amir20/dozzle/types"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/expr-lang/expr/vm"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
// Manager manages notification subscriptions and dispatches notifications
|
||||
// Manager handles notification subscriptions and matching
|
||||
type Manager struct {
|
||||
subscriptions *xsync.Map[int, *Subscription]
|
||||
dispatchers *xsync.Map[int, dispatcher.Dispatcher]
|
||||
subscriptionCounter atomic.Int32
|
||||
dispatcherCounter atomic.Int32
|
||||
listener *ContainerLogListener
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
sendSem *semaphore.Weighted
|
||||
subscriptions []*compiledSubscription
|
||||
mu sync.RWMutex
|
||||
activeStreams map[string]*activeStream // containerID -> stream info
|
||||
containerCache map[string]Container // containerID -> notification.Container
|
||||
containerService ContainerService
|
||||
dispatcher Dispatcher
|
||||
}
|
||||
|
||||
// activeStream tracks a single log stream and all subscriptions watching it
|
||||
type activeStream struct {
|
||||
container Container // notification.Container
|
||||
subscriptions []*compiledSubscription // All subscriptions watching this container
|
||||
cancel context.CancelFunc // Cancel function for the stream
|
||||
}
|
||||
|
||||
// compiledSubscription wraps a Subscription with compiled expr programs
|
||||
type compiledSubscription struct {
|
||||
subscription *Subscription
|
||||
containerProgram *vm.Program // Always non-nil
|
||||
logProgram *vm.Program // Always non-nil
|
||||
}
|
||||
|
||||
// ContainerService provides access to containers and log streaming
|
||||
type ContainerService interface {
|
||||
ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error)
|
||||
FindContainer(host, id string, labels container.ContainerLabels) (*container_support.ContainerService, error)
|
||||
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter)
|
||||
}
|
||||
|
||||
// NewManager creates a new notification manager
|
||||
func NewManager(listener *ContainerLogListener) *Manager {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m := &Manager{
|
||||
subscriptions: xsync.NewMap[int, *Subscription](),
|
||||
dispatchers: xsync.NewMap[int, dispatcher.Dispatcher](),
|
||||
listener: listener,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
sendSem: semaphore.NewWeighted(5),
|
||||
func NewManager(containerService ContainerService, dispatcher Dispatcher) *Manager {
|
||||
return &Manager{
|
||||
subscriptions: make([]*compiledSubscription, 0),
|
||||
activeStreams: make(map[string]*activeStream),
|
||||
containerCache: make(map[string]Container),
|
||||
containerService: containerService,
|
||||
dispatcher: dispatcher,
|
||||
}
|
||||
|
||||
// Start processing log events from the listener
|
||||
go m.processLogEvents()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Start initializes the manager and starts the log listener
|
||||
func (m *Manager) Start() error {
|
||||
if m.listener != nil {
|
||||
return m.listener.Start(m)
|
||||
// LoadSubscriptions loads and compiles multiple subscriptions (replaces existing)
|
||||
func (m *Manager) LoadSubscriptions(subs []*Subscription) error {
|
||||
m.mu.Lock()
|
||||
m.subscriptions = make([]*compiledSubscription, 0, len(subs))
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, sub := range subs {
|
||||
if err := m.AddSubscription(sub); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Int("count", len(subs)).Msg("loaded notification subscriptions")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldListenToContainer implements ContainerMatcher interface
|
||||
func (m *Manager) ShouldListenToContainer(c container.Container) bool {
|
||||
// Pass empty host for matching - host fields aren't used in container expressions
|
||||
notificationContainer := FromContainerModel(c, container.Host{})
|
||||
|
||||
shouldListen := false
|
||||
m.subscriptions.Range(func(_ int, sub *Subscription) bool {
|
||||
if sub.Enabled && sub.MatchesContainer(notificationContainer) {
|
||||
shouldListen = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return shouldListen
|
||||
}
|
||||
|
||||
// AddSubscription adds a new subscription with compiled expressions
|
||||
// AddSubscription adds a single subscription
|
||||
func (m *Manager) AddSubscription(sub *Subscription) error {
|
||||
// Auto-increment ID using atomic counter
|
||||
sub.ID = int(m.subscriptionCounter.Add(1))
|
||||
sub.Enabled = true
|
||||
|
||||
// Compile container expression if provided
|
||||
if sub.ContainerExpression != "" {
|
||||
program, err := expr.Compile(sub.ContainerExpression, expr.Env(types.NotificationContainer{}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile container expression: %w", err)
|
||||
}
|
||||
sub.ContainerProgram = program
|
||||
cs, err := m.compileSubscription(sub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Compile log expression if provided
|
||||
if sub.LogExpression != "" {
|
||||
program, err := expr.Compile(sub.LogExpression, expr.Env(types.NotificationLog{}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile log expression: %w", err)
|
||||
}
|
||||
sub.LogProgram = program
|
||||
}
|
||||
|
||||
m.subscriptions.Store(sub.ID, sub)
|
||||
log.Debug().Str("name", sub.Name).Int("id", sub.ID).Msg("Added subscription")
|
||||
|
||||
// Update listener to start/stop streams based on new subscription
|
||||
if m.listener != nil {
|
||||
if err := m.listener.UpdateStreams(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to update listener streams")
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.subscriptions = append(m.subscriptions, cs)
|
||||
m.mu.Unlock()
|
||||
|
||||
log.Info().Str("subscription", sub.Name).Msg("added notification subscription")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSubscription removes a subscription by ID
|
||||
func (m *Manager) RemoveSubscription(id int) {
|
||||
if sub, ok := m.subscriptions.LoadAndDelete(id); ok {
|
||||
log.Debug().Int("id", id).Str("name", sub.Name).Msg("Removed subscription")
|
||||
// compileSubscription compiles expr programs for a subscription
|
||||
func (m *Manager) compileSubscription(sub *Subscription) (*compiledSubscription, error) {
|
||||
if sub.ContainerFilter == "" {
|
||||
return nil, fmt.Errorf("container_filter is required for subscription %q", sub.Name)
|
||||
}
|
||||
if sub.LogFilter == "" {
|
||||
return nil, fmt.Errorf("log_filter is required for subscription %q", sub.Name)
|
||||
}
|
||||
|
||||
// Update listener to stop streams that are no longer needed
|
||||
if m.listener != nil {
|
||||
if err := m.listener.UpdateStreams(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to update listener streams")
|
||||
cs := &compiledSubscription{
|
||||
subscription: sub,
|
||||
}
|
||||
|
||||
// Compile container filter (required)
|
||||
containerProgram, err := expr.Compile(sub.ContainerFilter, expr.Env(Container{}))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("subscription", sub.Name).Msg("failed to compile container filter")
|
||||
return nil, fmt.Errorf("failed to compile container_filter for %q: %w", sub.Name, err)
|
||||
}
|
||||
cs.containerProgram = containerProgram
|
||||
|
||||
// Compile log filter (required)
|
||||
logProgram, err := expr.Compile(sub.LogFilter, expr.Env(Log{}))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("subscription", sub.Name).Msg("failed to compile log filter")
|
||||
return nil, fmt.Errorf("failed to compile log_filter for %q: %w", sub.Name, err)
|
||||
}
|
||||
cs.logProgram = logProgram
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
// Start begins monitoring containers and streaming logs for matching subscriptions
|
||||
// Only starts if there are subscriptions configured
|
||||
func (m *Manager) Start(ctx context.Context) error {
|
||||
m.mu.RLock()
|
||||
hasSubscriptions := len(m.subscriptions) > 0
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !hasSubscriptions {
|
||||
log.Debug().Msg("no subscriptions configured, skipping notification manager start")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subscribe to new containers that match our filters
|
||||
newContainers := make(chan container.Container)
|
||||
m.containerService.SubscribeContainersStarted(ctx, newContainers, func(c *container.Container) bool {
|
||||
matchingSubs := m.getMatchingSubscriptions(c)
|
||||
return len(matchingSubs) > 0
|
||||
})
|
||||
|
||||
// Handle new containers in background
|
||||
go func() {
|
||||
for c := range newContainers {
|
||||
m.startContainerStream(ctx, &c)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get all existing containers and start streaming matching ones
|
||||
containers, errs := m.containerService.ListAllContainers(nil)
|
||||
for _, err := range errs {
|
||||
log.Warn().Err(err).Msg("error listing containers for notifications")
|
||||
}
|
||||
|
||||
// Check each container and start streaming only if it matches any subscription
|
||||
for _, c := range containers {
|
||||
if c.State == "running" {
|
||||
matchingSubs := m.getMatchingSubscriptions(&c)
|
||||
if len(matchingSubs) > 0 {
|
||||
m.startContainerStream(ctx, &c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceSubscription replaces a subscription with new data
|
||||
func (m *Manager) ReplaceSubscription(sub *Subscription) error {
|
||||
// Compile container expression if provided
|
||||
if sub.ContainerExpression != "" {
|
||||
program, err := expr.Compile(sub.ContainerExpression, expr.Env(types.NotificationContainer{}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile container expression: %w", err)
|
||||
}
|
||||
sub.ContainerProgram = program
|
||||
}
|
||||
|
||||
// Compile log expression if provided
|
||||
if sub.LogExpression != "" {
|
||||
program, err := expr.Compile(sub.LogExpression, expr.Env(types.NotificationLog{}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile log expression: %w", err)
|
||||
}
|
||||
sub.LogProgram = program
|
||||
}
|
||||
|
||||
// Preserve enabled state from existing subscription if it exists
|
||||
if existing, ok := m.subscriptions.Load(sub.ID); ok {
|
||||
sub.Enabled = existing.Enabled
|
||||
} else {
|
||||
sub.Enabled = true
|
||||
}
|
||||
|
||||
m.subscriptions.Store(sub.ID, sub)
|
||||
log.Debug().Str("name", sub.Name).Int("id", sub.ID).Msg("Replaced subscription")
|
||||
|
||||
// Update listener to start/stop streams based on new subscription
|
||||
if m.listener != nil {
|
||||
if err := m.listener.UpdateStreams(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to update listener streams")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSubscription updates a subscription with the provided fields
|
||||
func (m *Manager) UpdateSubscription(id int, updates map[string]any) error {
|
||||
var updateErr error
|
||||
_, ok := m.subscriptions.Compute(id, func(sub *Subscription, loaded bool) (*Subscription, xsync.ComputeOp) {
|
||||
if !loaded {
|
||||
updateErr = fmt.Errorf("subscription not found")
|
||||
return nil, xsync.CancelOp
|
||||
}
|
||||
// startContainerStream starts a single log stream for a container
|
||||
// All matching subscriptions will receive logs from this one stream
|
||||
func (m *Manager) startContainerStream(ctx context.Context, c *container.Container) {
|
||||
// Check if already streaming
|
||||
m.mu.RLock()
|
||||
_, exists := m.activeStreams[c.ID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Clone the subscription
|
||||
updated := &Subscription{
|
||||
ID: sub.ID,
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
DispatcherID: sub.DispatcherID,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
ContainerProgram: sub.ContainerProgram,
|
||||
LogExpression: sub.LogExpression,
|
||||
LogProgram: sub.LogProgram,
|
||||
}
|
||||
|
||||
// Apply updates to the clone
|
||||
for key, value := range updates {
|
||||
switch key {
|
||||
case "name":
|
||||
if name, ok := value.(string); ok {
|
||||
updated.Name = name
|
||||
}
|
||||
case "enabled":
|
||||
if enabled, ok := value.(bool); ok {
|
||||
updated.Enabled = enabled
|
||||
}
|
||||
case "dispatcherId":
|
||||
if dispatcherID, ok := value.(int); ok {
|
||||
updated.DispatcherID = dispatcherID
|
||||
}
|
||||
case "containerExpression":
|
||||
if exprStr, ok := value.(string); ok {
|
||||
program, err := expr.Compile(exprStr, expr.Env(types.NotificationContainer{}))
|
||||
if err != nil {
|
||||
updateErr = fmt.Errorf("failed to compile container expression: %w", err)
|
||||
return nil, xsync.CancelOp
|
||||
}
|
||||
updated.ContainerExpression = exprStr
|
||||
updated.ContainerProgram = program
|
||||
}
|
||||
case "logExpression":
|
||||
if exprStr, ok := value.(string); ok {
|
||||
if exprStr != "" {
|
||||
program, err := expr.Compile(exprStr, expr.Env(types.NotificationLog{}))
|
||||
if err != nil {
|
||||
updateErr = fmt.Errorf("failed to compile log expression: %w", err)
|
||||
return nil, xsync.CancelOp
|
||||
}
|
||||
updated.LogExpression = exprStr
|
||||
updated.LogProgram = program
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated, xsync.UpdateOp
|
||||
})
|
||||
|
||||
if updateErr != nil {
|
||||
return updateErr
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("subscription not found")
|
||||
// Find which subscriptions match this container
|
||||
matchingSubs := m.getMatchingSubscriptions(c)
|
||||
if len(matchingSubs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Int("id", id).Interface("updates", updates).Msg("Updated subscription")
|
||||
log.Debug().
|
||||
Str("container", c.Name).
|
||||
Int("subscriptions", len(matchingSubs)).
|
||||
Msg("starting log stream for notification subscriptions")
|
||||
|
||||
// Update listener streams in case expressions changed
|
||||
if m.listener != nil {
|
||||
if err := m.listener.UpdateStreams(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to update listener streams")
|
||||
}
|
||||
// Convert to notification.Container and cache it
|
||||
notifContainer := NewContainer(c)
|
||||
m.mu.Lock()
|
||||
m.containerCache[c.ID] = notifContainer
|
||||
m.mu.Unlock()
|
||||
|
||||
streamCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
stream := &activeStream{
|
||||
container: notifContainer,
|
||||
subscriptions: matchingSubs,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.activeStreams[c.ID] = stream
|
||||
m.mu.Unlock()
|
||||
|
||||
// AddDispatcher adds a dispatcher and returns its auto-generated ID
|
||||
func (m *Manager) AddDispatcher(d dispatcher.Dispatcher) int {
|
||||
id := int(m.dispatcherCounter.Add(1))
|
||||
m.dispatchers.Store(id, d)
|
||||
log.Debug().Int("id", id).Msg("Added dispatcher")
|
||||
return id
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
m.mu.Lock()
|
||||
delete(m.activeStreams, c.ID)
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
|
||||
// UpdateDispatcher updates a dispatcher by ID
|
||||
func (m *Manager) UpdateDispatcher(id int, d dispatcher.Dispatcher) {
|
||||
m.dispatchers.Store(id, d)
|
||||
log.Debug().Int("id", id).Msg("Updated dispatcher")
|
||||
}
|
||||
logs := make(chan *container.LogEvent)
|
||||
|
||||
// RemoveDispatcher removes a dispatcher by ID
|
||||
func (m *Manager) RemoveDispatcher(id int) {
|
||||
if _, ok := m.dispatchers.LoadAndDelete(id); ok {
|
||||
log.Debug().Int("id", id).Msg("Removed dispatcher")
|
||||
}
|
||||
}
|
||||
|
||||
// Subscriptions returns all subscriptions sorted by ID
|
||||
func (m *Manager) Subscriptions() []*Subscription {
|
||||
result := make([]*Subscription, 0)
|
||||
m.subscriptions.Range(func(_ int, sub *Subscription) bool {
|
||||
result = append(result, sub)
|
||||
return true
|
||||
})
|
||||
slices.SortFunc(result, func(a, b *Subscription) int {
|
||||
return a.ID - b.ID
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Dispatchers returns all dispatchers as DispatcherConfig sorted by ID
|
||||
func (m *Manager) Dispatchers() []DispatcherConfig {
|
||||
result := make([]DispatcherConfig, 0)
|
||||
m.dispatchers.Range(func(id int, d dispatcher.Dispatcher) bool {
|
||||
switch v := d.(type) {
|
||||
case *dispatcher.WebhookDispatcher:
|
||||
result = append(result, DispatcherConfig{
|
||||
ID: id,
|
||||
Name: v.Name,
|
||||
Type: "webhook",
|
||||
URL: v.URL,
|
||||
Template: v.TemplateText,
|
||||
})
|
||||
case *dispatcher.CloudDispatcher:
|
||||
result = append(result, DispatcherConfig{
|
||||
ID: id,
|
||||
Name: v.Name,
|
||||
Type: "cloud",
|
||||
APIKey: v.APIKey,
|
||||
Prefix: v.Prefix,
|
||||
ExpiresAt: v.ExpiresAt,
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
slices.SortFunc(result, func(a, b DispatcherConfig) int {
|
||||
return a.ID - b.ID
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// processLogEvents processes log events from the listener channel
|
||||
func (m *Manager) processLogEvents() {
|
||||
for {
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return
|
||||
case logEvent := <-m.listener.LogChannel():
|
||||
if logEvent == nil {
|
||||
// Stream logs in separate goroutine
|
||||
go func() {
|
||||
containerService, err := m.containerService.FindContainer(c.Host, c.ID, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error finding container for notification streaming")
|
||||
return
|
||||
}
|
||||
m.processLogEvent(logEvent)
|
||||
|
||||
err = containerService.StreamLogs(streamCtx, time.Now(), container.STDOUT|container.STDERR, logs)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.Error().Err(err).Msg("error streaming logs for notification")
|
||||
}
|
||||
close(logs)
|
||||
}()
|
||||
|
||||
// Process logs - check against all matching subscriptions
|
||||
for logEvent := range logs {
|
||||
for _, cs := range matchingSubs {
|
||||
m.processLogEvent(logEvent, c.ID, cs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// processLogEvent processes a single log event and sends notifications for matching subscriptions
|
||||
func (m *Manager) processLogEvent(logEvent *container.LogEvent) {
|
||||
// Get container and host from log event's ContainerID
|
||||
ctx, cancel := context.WithTimeout(m.ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
// getMatchingSubscriptions returns subscriptions that match a container
|
||||
func (m *Manager) getMatchingSubscriptions(c *container.Container) []*compiledSubscription {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
c, host, err := m.listener.FindContainerWithHost(ctx, logEvent.ContainerID, nil)
|
||||
containerCtx := NewContainer(c)
|
||||
matching := make([]*compiledSubscription, 0)
|
||||
|
||||
for _, cs := range m.subscriptions {
|
||||
if m.matchesContainer(cs.containerProgram, containerCtx) {
|
||||
matching = append(matching, cs)
|
||||
}
|
||||
}
|
||||
|
||||
return matching
|
||||
}
|
||||
|
||||
// matchesContainer evaluates the container filter expression
|
||||
func (m *Manager) matchesContainer(program *vm.Program, containerCtx Container) bool {
|
||||
result, err := expr.Run(program, containerCtx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("containerID", logEvent.ContainerID).Msg("Failed to find container")
|
||||
log.Error().Err(err).Msg("error evaluating container filter")
|
||||
return false
|
||||
}
|
||||
|
||||
match, ok := result.(bool)
|
||||
if !ok {
|
||||
log.Error().Msg("container filter did not return boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
// processLogEvent evaluates a log event against the subscription's log filter
|
||||
func (m *Manager) processLogEvent(logEvent *container.LogEvent, containerID string, cs *compiledSubscription) {
|
||||
// Lookup notification.Container from cache
|
||||
m.mu.RLock()
|
||||
notifContainer, ok := m.containerCache[containerID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
log.Warn().Str("container", containerID).Msg("container not in cache")
|
||||
return
|
||||
}
|
||||
|
||||
// Skip logs from Dozzle itself to avoid feedback loops
|
||||
if strings.Contains(c.Image, "dozzle") {
|
||||
return
|
||||
}
|
||||
logCtx := NewLog(logEvent, notifContainer)
|
||||
|
||||
notificationContainer := FromContainerModel(c, host)
|
||||
notificationLog := FromLogEvent(*logEvent)
|
||||
|
||||
m.subscriptions.Range(func(_ int, sub *Subscription) bool {
|
||||
// Skip disabled subscriptions
|
||||
if !sub.Enabled {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check container filter
|
||||
if !sub.MatchesContainer(notificationContainer) {
|
||||
return true
|
||||
}
|
||||
|
||||
sub.AddTriggeredContainer(notificationContainer.ID)
|
||||
|
||||
// Check log filter
|
||||
if !sub.MatchesLog(notificationLog) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Update stats
|
||||
sub.TriggerCount.Add(1)
|
||||
now := time.Now()
|
||||
sub.LastTriggeredAt.Store(&now)
|
||||
|
||||
log.Debug().Str("containerID", notificationContainer.ID).Interface("log", notificationLog.Message).Msg("Matched subscription")
|
||||
|
||||
// Create notification
|
||||
notification := types.Notification{
|
||||
ID: fmt.Sprintf("%s-%d", c.ID, time.Now().UnixNano()),
|
||||
Container: notificationContainer,
|
||||
Log: notificationLog,
|
||||
Subscription: types.SubscriptionConfig{
|
||||
ID: sub.ID,
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
DispatcherID: sub.DispatcherID,
|
||||
LogExpression: sub.LogExpression,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Send to the subscription's dispatcher
|
||||
if d, ok := m.dispatchers.Load(sub.DispatcherID); ok {
|
||||
go m.sendNotification(d, notification, sub.DispatcherID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// sendNotification sends a notification using the dispatcher
|
||||
func (m *Manager) sendNotification(d dispatcher.Dispatcher, notification types.Notification, id int) {
|
||||
acquireCtx, acquireCancel := context.WithTimeout(m.ctx, time.Minute)
|
||||
defer acquireCancel()
|
||||
if err := m.sendSem.Acquire(acquireCtx, 1); err != nil {
|
||||
log.Warn().Err(err).Int("subscription", id).Msg("Notification dropped: too many pending")
|
||||
return
|
||||
}
|
||||
defer m.sendSem.Release(1)
|
||||
|
||||
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := d.Send(ctx, notification); err != nil {
|
||||
log.Error().Err(err).Int("subscription", id).Msg("Failed to send notification")
|
||||
// Evaluate log filter (program is never nil)
|
||||
if m.matchesLog(cs.logProgram, logCtx) {
|
||||
m.triggerWebhook(cs.subscription, logCtx)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteConfig writes the current configuration to a writer in YAML format
|
||||
func (m *Manager) WriteConfig(w io.Writer) error {
|
||||
config := Config{
|
||||
Subscriptions: m.Subscriptions(),
|
||||
Dispatchers: m.Dispatchers(),
|
||||
// matchesLog evaluates the log filter expression
|
||||
func (m *Manager) matchesLog(program *vm.Program, logCtx Log) bool {
|
||||
result, err := expr.Run(program, logCtx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error evaluating log filter")
|
||||
return false
|
||||
}
|
||||
|
||||
encoder := yaml.NewEncoder(w)
|
||||
defer encoder.Close()
|
||||
match, ok := result.(bool)
|
||||
if !ok {
|
||||
log.Error().Msg("log filter did not return boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
return encoder.Encode(config)
|
||||
return match
|
||||
}
|
||||
|
||||
// LoadConfig reads configuration from a reader in YAML format and loads it
|
||||
func (m *Manager) LoadConfig(r io.Reader) error {
|
||||
var config Config
|
||||
|
||||
decoder := yaml.NewDecoder(r)
|
||||
if err := decoder.Decode(&config); err != nil {
|
||||
return fmt.Errorf("failed to decode config: %w", err)
|
||||
// triggerWebhook sends the notification
|
||||
func (m *Manager) triggerWebhook(sub *Subscription, logCtx Log) {
|
||||
payload := WebhookPayload{
|
||||
SubscriptionName: sub.Name,
|
||||
Timestamp: time.Now(),
|
||||
Container: logCtx.Container,
|
||||
Log: LogEvent{
|
||||
Message: logCtx.Message,
|
||||
Level: logCtx.Level,
|
||||
Timestamp: logCtx.Timestamp,
|
||||
},
|
||||
}
|
||||
|
||||
// Convert to types for HandleNotificationConfig
|
||||
subscriptions := make([]types.SubscriptionConfig, len(config.Subscriptions))
|
||||
for i, sub := range config.Subscriptions {
|
||||
subscriptions[i] = types.SubscriptionConfig{
|
||||
ID: sub.ID,
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
DispatcherID: sub.DispatcherID,
|
||||
LogExpression: sub.LogExpression,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
}
|
||||
}
|
||||
|
||||
dispatchers := make([]types.DispatcherConfig, len(config.Dispatchers))
|
||||
for i, d := range config.Dispatchers {
|
||||
dispatchers[i] = types.DispatcherConfig{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.URL,
|
||||
Template: d.Template,
|
||||
APIKey: d.APIKey,
|
||||
Prefix: d.Prefix,
|
||||
ExpiresAt: d.ExpiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
return m.HandleNotificationConfig(subscriptions, dispatchers)
|
||||
}
|
||||
|
||||
// HandleNotificationConfig implements agent.NotificationConfigHandler interface
|
||||
// It atomically replaces all subscriptions and dispatchers with new state from the main server
|
||||
func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
|
||||
// Clear existing state (with nil checks for defensive programming)
|
||||
if m.subscriptions != nil {
|
||||
m.subscriptions.Clear()
|
||||
} else {
|
||||
m.subscriptions = xsync.NewMap[int, *Subscription]()
|
||||
}
|
||||
if m.dispatchers != nil {
|
||||
m.dispatchers.Clear()
|
||||
} else {
|
||||
m.dispatchers = xsync.NewMap[int, dispatcher.Dispatcher]()
|
||||
}
|
||||
|
||||
// Find max IDs to initialize counters
|
||||
var maxSubID, maxDispatcherID int
|
||||
for _, sub := range subscriptions {
|
||||
if sub.ID > maxSubID {
|
||||
maxSubID = sub.ID
|
||||
}
|
||||
}
|
||||
for _, d := range dispatchers {
|
||||
if d.ID > maxDispatcherID {
|
||||
maxDispatcherID = d.ID
|
||||
}
|
||||
}
|
||||
m.subscriptionCounter.Store(int32(maxSubID))
|
||||
m.dispatcherCounter.Store(int32(maxDispatcherID))
|
||||
|
||||
// Load subscriptions (convert from types.SubscriptionConfig to Subscription)
|
||||
for _, sub := range subscriptions {
|
||||
s := &Subscription{
|
||||
ID: sub.ID,
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
DispatcherID: sub.DispatcherID,
|
||||
LogExpression: sub.LogExpression,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
}
|
||||
if err := m.loadSubscription(s); err != nil {
|
||||
return fmt.Errorf("failed to load subscription %s: %w", sub.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load dispatchers
|
||||
for _, dc := range dispatchers {
|
||||
d, err := createDispatcher(DispatcherConfig{
|
||||
ID: dc.ID,
|
||||
Name: dc.Name,
|
||||
Type: dc.Type,
|
||||
URL: dc.URL,
|
||||
Template: dc.Template,
|
||||
APIKey: dc.APIKey,
|
||||
Prefix: dc.Prefix,
|
||||
ExpiresAt: dc.ExpiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create dispatcher %s: %w", dc.Name, err)
|
||||
}
|
||||
m.dispatchers.Store(dc.ID, d)
|
||||
log.Debug().Int("id", dc.ID).Msg("Loaded dispatcher from state sync")
|
||||
}
|
||||
|
||||
// Update listener to start/stop streams based on new subscriptions
|
||||
if m.listener != nil {
|
||||
if err := m.listener.UpdateStreams(); err != nil {
|
||||
return fmt.Errorf("failed to update listener streams: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Int("subscriptions", len(subscriptions)).Int("dispatchers", len(dispatchers)).Msg("Replaced notification state")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDispatcher creates a dispatcher from a DispatcherConfig
|
||||
func createDispatcher(config DispatcherConfig) (dispatcher.Dispatcher, error) {
|
||||
switch config.Type {
|
||||
case "webhook":
|
||||
return dispatcher.NewWebhookDispatcher(config.Name, config.URL, config.Template)
|
||||
case "cloud":
|
||||
return dispatcher.NewCloudDispatcher(config.Name, config.APIKey, config.Prefix, config.ExpiresAt)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown dispatcher type: %s", config.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// loadSubscription loads a subscription with its existing ID (used when loading from config)
|
||||
func (m *Manager) loadSubscription(sub *Subscription) error {
|
||||
// Compile container expression if provided
|
||||
if sub.ContainerExpression != "" {
|
||||
program, err := expr.Compile(sub.ContainerExpression, expr.Env(types.NotificationContainer{}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile container expression: %w", err)
|
||||
}
|
||||
sub.ContainerProgram = program
|
||||
}
|
||||
|
||||
// Compile log expression if provided
|
||||
if sub.LogExpression != "" {
|
||||
program, err := expr.Compile(sub.LogExpression, expr.Env(types.NotificationLog{}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile log expression: %w", err)
|
||||
}
|
||||
sub.LogProgram = program
|
||||
}
|
||||
|
||||
m.subscriptions.Store(sub.ID, sub)
|
||||
log.Debug().Str("name", sub.Name).Int("id", sub.ID).Msg("Loaded subscription")
|
||||
return nil
|
||||
m.dispatcher.Dispatch(sub, payload)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config represents the YAML configuration structure
|
||||
type Config struct {
|
||||
Dispatcher DispatcherConfig `yaml:"dispatcher"`
|
||||
Subscriptions []*Subscription `yaml:"subscriptions"`
|
||||
}
|
||||
|
||||
// LoadFromFile loads config and returns a fully configured Manager
|
||||
func LoadFromFile(path string, containerService ContainerService) (*Manager, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil // No config file is fine, return nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read notifications config: %w", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse notifications config: %w", err)
|
||||
}
|
||||
|
||||
// Validate dispatcher config
|
||||
if config.Dispatcher.Type == "" {
|
||||
config.Dispatcher.Type = "simple" // Default to simple
|
||||
}
|
||||
if config.Dispatcher.URL == "" {
|
||||
return nil, fmt.Errorf("dispatcher.url is required")
|
||||
}
|
||||
|
||||
// Validate subscriptions
|
||||
for _, sub := range config.Subscriptions {
|
||||
if sub.Name == "" {
|
||||
return nil, fmt.Errorf("subscription missing name")
|
||||
}
|
||||
if sub.ContainerFilter == "" {
|
||||
return nil, fmt.Errorf("subscription %q missing container_filter", sub.Name)
|
||||
}
|
||||
if sub.LogFilter == "" {
|
||||
return nil, fmt.Errorf("subscription %q missing log_filter", sub.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Create dispatcher based on type
|
||||
var dispatcher Dispatcher
|
||||
switch config.Dispatcher.Type {
|
||||
case "simple":
|
||||
dispatcher = NewWebhookDispatcher(&config.Dispatcher)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported dispatcher type: %s", config.Dispatcher.Type)
|
||||
}
|
||||
|
||||
// Create and configure manager
|
||||
manager := NewManager(containerService, dispatcher)
|
||||
if err := manager.LoadSubscriptions(config.Subscriptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// SaveToFile saves dispatcher config and subscriptions to a YAML file
|
||||
func SaveToFile(path string, dispatcher *DispatcherConfig, subs []*Subscription) error {
|
||||
config := Config{
|
||||
Dispatcher: *dispatcher,
|
||||
Subscriptions: subs,
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notifications config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write notifications config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+87
-147
@@ -2,163 +2,103 @@ package notification
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/expr-lang/expr/vm"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
// FromContainerModel converts internal container.Container to types.NotificationContainer
|
||||
func FromContainerModel(c container.Container, host container.Host) types.NotificationContainer {
|
||||
return types.NotificationContainer{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Image: c.Image,
|
||||
State: c.State,
|
||||
Health: c.Health,
|
||||
HostID: host.ID,
|
||||
HostName: host.Name,
|
||||
Labels: c.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
// FromLogEvent converts container.LogEvent to types.NotificationLog
|
||||
func FromLogEvent(l container.LogEvent) types.NotificationLog {
|
||||
message := extractMessage(l)
|
||||
|
||||
return types.NotificationLog{
|
||||
ID: l.Id,
|
||||
Message: message,
|
||||
Timestamp: l.Timestamp,
|
||||
Level: l.Level,
|
||||
Stream: l.Stream,
|
||||
Type: string(l.Type),
|
||||
}
|
||||
}
|
||||
|
||||
// extractMessage extracts and joins message from LogEvent
|
||||
// For grouped logs (fragments), joins them into a single string
|
||||
// For complex logs (JSON/objects), converts to a regular map for expr compatibility
|
||||
// For simple logs, returns the string as-is
|
||||
func extractMessage(l container.LogEvent) any {
|
||||
switch v := l.Message.(type) {
|
||||
case string:
|
||||
return container.StripANSI(v)
|
||||
case []container.LogFragment:
|
||||
var parts []string
|
||||
for _, fragment := range v {
|
||||
parts = append(parts, container.StripANSI(fragment.Message))
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
case *orderedmap.OrderedMap[string, any]:
|
||||
// Convert OrderedMap to regular map for expr compatibility
|
||||
result := make(map[string]any)
|
||||
for pair := v.Oldest(); pair != nil; pair = pair.Next() {
|
||||
result[pair.Key] = pair.Value
|
||||
}
|
||||
return result
|
||||
case *orderedmap.OrderedMap[string, string]:
|
||||
// Convert OrderedMap[string, string] to regular map for expr compatibility
|
||||
result := make(map[string]any)
|
||||
for pair := v.Oldest(); pair != nil; pair = pair.Next() {
|
||||
result[pair.Key] = pair.Value
|
||||
}
|
||||
return result
|
||||
default:
|
||||
// For other complex objects, keep the original structure
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription represents a subscription to log streams with filtering
|
||||
type Subscription struct {
|
||||
ID int `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||
DispatcherID int `json:"dispatcherId" yaml:"dispatcherId"`
|
||||
LogExpression string `json:"logExpression" yaml:"logExpression"`
|
||||
ContainerExpression string `json:"containerExpression" yaml:"containerExpression"`
|
||||
|
||||
// Compiled log filter expression
|
||||
LogProgram *vm.Program `json:"-" yaml:"-"` // Compiled log filter expression
|
||||
ContainerProgram *vm.Program `json:"-" yaml:"-"` // Compiled container filter expression
|
||||
|
||||
// Runtime stats (not persisted)
|
||||
TriggerCount atomic.Int64 `json:"-" yaml:"-"`
|
||||
LastTriggeredAt atomic.Pointer[time.Time] `json:"-" yaml:"-"`
|
||||
TriggeredContainerIDs *xsync.Map[string, struct{}] `json:"-" yaml:"-"` // unique container IDs that triggered
|
||||
}
|
||||
|
||||
// TriggeredContainersCount returns the number of unique containers that triggered this subscription
|
||||
func (s *Subscription) TriggeredContainersCount() int {
|
||||
if s.TriggeredContainerIDs == nil {
|
||||
return 0
|
||||
}
|
||||
return s.TriggeredContainerIDs.Size()
|
||||
}
|
||||
|
||||
// AddTriggeredContainer adds a container ID to the triggered set
|
||||
func (s *Subscription) AddTriggeredContainer(id string) {
|
||||
if s.TriggeredContainerIDs == nil {
|
||||
s.TriggeredContainerIDs = xsync.NewMap[string, struct{}]()
|
||||
}
|
||||
s.TriggeredContainerIDs.Store(id, struct{}{})
|
||||
}
|
||||
|
||||
// DispatcherConfig represents a dispatcher configuration
|
||||
// DispatcherConfig represents global dispatcher configuration
|
||||
type DispatcherConfig struct {
|
||||
ID int `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"` // "webhook", "cloud"
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Template string `json:"template,omitempty" yaml:"template,omitempty"` // Go template for custom payload format
|
||||
APIKey string `json:"apiKey,omitempty" yaml:"apiKey,omitempty"` // API key for cloud dispatcher
|
||||
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` // API key prefix for cloud dispatcher
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty" yaml:"expiresAt,omitempty"`
|
||||
// Type determines the dispatcher type (webhook, dozzle-service, etc.)
|
||||
Type string `yaml:"type"` // "webhook" is default
|
||||
|
||||
// URL is the endpoint for the dispatcher service
|
||||
URL string `yaml:"url"`
|
||||
|
||||
// APIKey for authenticating with the dispatcher service
|
||||
APIKey string `yaml:"api_key,omitempty"`
|
||||
|
||||
// Headers to include in all requests
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// Config represents the persisted notification configuration
|
||||
type Config struct {
|
||||
Subscriptions []*Subscription `json:"subscriptions" yaml:"subscriptions"`
|
||||
Dispatchers []DispatcherConfig `json:"dispatchers" yaml:"dispatchers"`
|
||||
// Subscription represents a notification rule
|
||||
type Subscription struct {
|
||||
// Name is a human-readable name for this subscription
|
||||
Name string `yaml:"name"`
|
||||
|
||||
// ContainerFilter is an expr expression to match containers (REQUIRED)
|
||||
// Expression receives a Container object with: Name, Labels, Host, State
|
||||
// Example: Name == "nginx" || Labels["app"] == "web"
|
||||
ContainerFilter string `yaml:"container_filter"`
|
||||
|
||||
// LogFilter is an expr expression to match log events (REQUIRED)
|
||||
// Expression receives a Log object with: Message, Level, Timestamp, Container
|
||||
// Example: Level == "error" || Message contains "panic"
|
||||
LogFilter string `yaml:"log_filter"`
|
||||
}
|
||||
|
||||
// MatchesContainer checks if a container matches this subscription's container filter
|
||||
func (s *Subscription) MatchesContainer(c types.NotificationContainer) bool {
|
||||
if s.ContainerProgram == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
result, err := expr.Run(s.ContainerProgram, c)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("expression", s.ContainerExpression).Msg("container expression evaluation error")
|
||||
return false
|
||||
}
|
||||
|
||||
match, ok := result.(bool)
|
||||
return ok && match
|
||||
// Container represents container information for expr evaluation (internal type)
|
||||
type Container struct {
|
||||
Name string `expr:"Name"`
|
||||
Labels map[string]string `expr:"Labels"`
|
||||
Host string `expr:"Host"`
|
||||
State string `expr:"State"`
|
||||
}
|
||||
|
||||
// MatchesLog checks if a log matches this subscription's log filter
|
||||
func (s *Subscription) MatchesLog(l types.NotificationLog) bool {
|
||||
if s.LogProgram == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
result, err := expr.Run(s.LogProgram, l)
|
||||
if err != nil {
|
||||
// Type mismatches are expected when expression doesn't match log type
|
||||
// e.g., "message contains X" on JSON logs or "message.field" on string logs
|
||||
log.Debug().Err(err).Str("expression", s.LogExpression).Msg("log expression evaluation error")
|
||||
return false
|
||||
}
|
||||
|
||||
match, ok := result.(bool)
|
||||
return ok && match
|
||||
// Log represents log event information for expr evaluation
|
||||
type Log struct {
|
||||
Message any `expr:"Message"`
|
||||
Level string `expr:"Level"`
|
||||
Timestamp time.Time `expr:"Timestamp"`
|
||||
Container Container `expr:"Container"`
|
||||
}
|
||||
|
||||
// WebhookPayload is the structure sent to webhook endpoints
|
||||
type WebhookPayload struct {
|
||||
SubscriptionName string `json:"subscription_name"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Container Container `json:"container"`
|
||||
Log LogEvent `json:"log"`
|
||||
}
|
||||
|
||||
// LogEvent represents the log in the webhook payload
|
||||
type LogEvent struct {
|
||||
Message any `json:"message"`
|
||||
Level string `json:"level"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// NewContainer converts container.Container to notification.Container
|
||||
func NewContainer(c *container.Container) Container {
|
||||
return Container{
|
||||
Name: c.Name,
|
||||
Labels: c.Labels,
|
||||
Host: c.Host,
|
||||
State: c.State,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLog creates a Log from container.LogEvent and notification.Container
|
||||
func NewLog(logEvent *container.LogEvent, notifContainer Container) Log {
|
||||
timestamp := time.Unix(0, logEvent.Timestamp)
|
||||
|
||||
// Extract message - keep as any type for expr flexibility
|
||||
var message any = logEvent.Message
|
||||
|
||||
// For grouped logs, join fragments into single string for easier matching
|
||||
if fragments, ok := logEvent.Message.([]container.LogFragment); ok {
|
||||
parts := make([]string, len(fragments))
|
||||
for i, frag := range fragments {
|
||||
parts[i] = frag.Message
|
||||
}
|
||||
message = strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
return Log{
|
||||
Message: message,
|
||||
Level: logEvent.Level,
|
||||
Timestamp: timestamp,
|
||||
Container: notifContainer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
func TestSubscription_MatchesLog(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
log types.NotificationLog
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matches message substring",
|
||||
expression: `message contains "error"`,
|
||||
log: types.NotificationLog{
|
||||
Message: "an error occurred",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "does not match message substring",
|
||||
expression: `message contains "error"`,
|
||||
log: types.NotificationLog{
|
||||
Message: "everything is fine",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "matches log level",
|
||||
expression: `level == "error"`,
|
||||
log: types.NotificationLog{
|
||||
Level: "error",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches stream",
|
||||
expression: `stream == "stderr"`,
|
||||
log: types.NotificationLog{
|
||||
Stream: "stderr",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "complex expression with AND",
|
||||
expression: `level == "error" && message contains "fatal"`,
|
||||
log: types.NotificationLog{
|
||||
Level: "error",
|
||||
Message: "fatal exception",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "complex expression fails AND",
|
||||
expression: `level == "error" && message contains "fatal"`,
|
||||
log: types.NotificationLog{
|
||||
Level: "error",
|
||||
Message: "normal error",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "complex expression with OR",
|
||||
expression: `level == "error" || message contains "warning"`,
|
||||
log: types.NotificationLog{
|
||||
Level: "info",
|
||||
Message: "warning: something happened",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches type",
|
||||
expression: `type == "stdout"`,
|
||||
log: types.NotificationLog{
|
||||
Type: "stdout",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches complex message number field",
|
||||
expression: `message.number == 123`,
|
||||
log: types.NotificationLog{
|
||||
Message: map[string]any{
|
||||
"number": 123,
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches complex message string field",
|
||||
expression: `message.status == "error"`,
|
||||
log: types.NotificationLog{
|
||||
Message: map[string]any{
|
||||
"status": "error",
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches complex message nested field",
|
||||
expression: `message.user.name == "admin"`,
|
||||
log: types.NotificationLog{
|
||||
Message: map[string]any{
|
||||
"user": map[string]any{
|
||||
"name": "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "does not match complex message wrong value",
|
||||
expression: `message.number == 123`,
|
||||
log: types.NotificationLog{
|
||||
Message: map[string]any{
|
||||
"number": 456,
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "complex message with multiple conditions",
|
||||
expression: `message.level == "error" && message.code >= 400`,
|
||||
log: types.NotificationLog{
|
||||
Message: map[string]any{
|
||||
"level": "error",
|
||||
"code": 500,
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
program, err := expr.Compile(tt.expression, expr.Env(types.NotificationLog{}), expr.AsBool())
|
||||
require.NoError(t, err, "failed to compile expression")
|
||||
|
||||
sub := &Subscription{
|
||||
LogProgram: program,
|
||||
}
|
||||
|
||||
got := sub.MatchesLog(tt.log)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscription_MatchesLog_NilProgram(t *testing.T) {
|
||||
sub := &Subscription{
|
||||
LogProgram: nil,
|
||||
}
|
||||
|
||||
log := types.NotificationLog{
|
||||
Message: "test",
|
||||
}
|
||||
|
||||
got := sub.MatchesLog(log)
|
||||
assert.False(t, got, "should return false when LogProgram is nil")
|
||||
}
|
||||
|
||||
func TestSubscription_MatchesLog_InvalidExpression(t *testing.T) {
|
||||
// Create a program that will cause a runtime error by dividing by zero
|
||||
program, err := expr.Compile(`1 / 0 == 1`, expr.AsBool())
|
||||
require.NoError(t, err)
|
||||
|
||||
sub := &Subscription{
|
||||
LogProgram: program,
|
||||
}
|
||||
|
||||
log := types.NotificationLog{
|
||||
Message: "test",
|
||||
}
|
||||
|
||||
got := sub.MatchesLog(log)
|
||||
assert.False(t, got, "should return false when expression evaluation fails")
|
||||
}
|
||||
|
||||
func TestFromLogEvent_OrderedMapConversion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
logEvent container.LogEvent
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "orderedmap string any - matches number field",
|
||||
expression: `message.value > 0`,
|
||||
logEvent: container.LogEvent{
|
||||
Message: orderedmap.New[string, any](
|
||||
orderedmap.WithInitialData(
|
||||
orderedmap.Pair[string, any]{Key: "value", Value: 123},
|
||||
orderedmap.Pair[string, any]{Key: "name", Value: "test"},
|
||||
),
|
||||
),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "orderedmap string any - matches string field",
|
||||
expression: `message.name == "test"`,
|
||||
logEvent: container.LogEvent{
|
||||
Message: orderedmap.New[string, any](
|
||||
orderedmap.WithInitialData(
|
||||
orderedmap.Pair[string, any]{Key: "value", Value: 123},
|
||||
orderedmap.Pair[string, any]{Key: "name", Value: "test"},
|
||||
),
|
||||
),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "orderedmap string string - matches field",
|
||||
expression: `message.level == "error"`,
|
||||
logEvent: container.LogEvent{
|
||||
Message: orderedmap.New[string, string](
|
||||
orderedmap.WithInitialData(
|
||||
orderedmap.Pair[string, string]{Key: "level", Value: "error"},
|
||||
orderedmap.Pair[string, string]{Key: "msg", Value: "something failed"},
|
||||
),
|
||||
),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "orderedmap string string - does not match",
|
||||
expression: `message.level == "error"`,
|
||||
logEvent: container.LogEvent{
|
||||
Message: orderedmap.New[string, string](
|
||||
orderedmap.WithInitialData(
|
||||
orderedmap.Pair[string, string]{Key: "level", Value: "info"},
|
||||
orderedmap.Pair[string, string]{Key: "msg", Value: "all good"},
|
||||
),
|
||||
),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Convert LogEvent to NotificationLog (this is where OrderedMap conversion happens)
|
||||
notificationLog := FromLogEvent(tt.logEvent)
|
||||
|
||||
// Verify the message is now a regular map
|
||||
_, isMap := notificationLog.Message.(map[string]any)
|
||||
assert.True(t, isMap, "message should be converted to map[string]any")
|
||||
|
||||
// Compile and run the expression
|
||||
program, err := expr.Compile(tt.expression, expr.Env(types.NotificationLog{}), expr.AsBool())
|
||||
require.NoError(t, err, "failed to compile expression")
|
||||
|
||||
sub := &Subscription{
|
||||
LogProgram: program,
|
||||
}
|
||||
|
||||
got := sub.MatchesLog(notificationLog)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Dispatcher is the interface for sending notifications
|
||||
type Dispatcher interface {
|
||||
Dispatch(subscription *Subscription, payload WebhookPayload)
|
||||
}
|
||||
|
||||
// WebhookDispatcher handles async webhook delivery
|
||||
type WebhookDispatcher struct {
|
||||
client *http.Client
|
||||
config *DispatcherConfig
|
||||
}
|
||||
|
||||
// NewWebhookDispatcher creates a new webhook dispatcher
|
||||
func NewWebhookDispatcher(config *DispatcherConfig) *WebhookDispatcher {
|
||||
return &WebhookDispatcher{
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch sends a webhook payload asynchronously
|
||||
func (d *WebhookDispatcher) Dispatch(subscription *Subscription, payload WebhookPayload) {
|
||||
url := d.config.URL
|
||||
go func() {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("failed to marshal webhook payload")
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("failed to create webhook request")
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Dozzle-Notifications/1.0")
|
||||
|
||||
// Add API key if configured
|
||||
if d.config.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+d.config.APIKey)
|
||||
}
|
||||
|
||||
// Add custom headers
|
||||
for key, value := range d.config.Headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := d.client.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("failed to send webhook")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
log.Warn().
|
||||
Int("status", resp.StatusCode).
|
||||
Str("url", url).
|
||||
Str("subscription", payload.SubscriptionName).
|
||||
Msg("webhook returned error status")
|
||||
} else {
|
||||
log.Debug().
|
||||
Int("status", resp.StatusCode).
|
||||
Str("url", url).
|
||||
Str("subscription", payload.SubscriptionName).
|
||||
Msg("webhook delivered successfully")
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -12,10 +12,6 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
docker_support "github.com/amir20/dozzle/internal/support/docker"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -23,37 +19,6 @@ type AgentCmd struct {
|
||||
Addr string `arg:"--agent-addr,env:DOZZLE_AGENT_ADDR" default:":7007" help:"sets the host:port to bind for the agent"`
|
||||
}
|
||||
|
||||
// persistingNotificationHandler wraps a notification manager and saves config to disk after updates
|
||||
type persistingNotificationHandler struct {
|
||||
manager *notification.Manager
|
||||
configPath string
|
||||
}
|
||||
|
||||
func (h *persistingNotificationHandler) HandleNotificationConfig(subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
|
||||
// Update the manager
|
||||
if err := h.manager.HandleNotificationConfig(subscriptions, dispatchers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.Create(h.configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := h.manager.WriteConfig(file); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().Str("path", h.configPath).Msg("Saved notification config to disk")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AgentCmd) Run(args Args, embeddedCerts embed.FS) error {
|
||||
if args.Mode != "server" {
|
||||
return fmt.Errorf("agent command is only available in server mode")
|
||||
@@ -78,43 +43,12 @@ func (a *AgentCmd) Run(args Args, embeddedCerts embed.FS) error {
|
||||
io.WriteString(tempFile, listener.Addr().String())
|
||||
log.Debug().Str("file", tempFile.Name()).Msg("Created temp file")
|
||||
go StartEvent(args, "", client, "agent")
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
// Create shared client service (single ContainerStore for both agent server and notifications)
|
||||
clientService := docker_support.NewDockerClientService(client, args.Filter)
|
||||
|
||||
// Create notification manager using the shared client service
|
||||
const notificationConfigPath = "./data/notifications.yml"
|
||||
clients := []container_support.ClientService{clientService}
|
||||
notificationManager := notification.NewManager(notification.NewContainerLogListener(ctx, clients))
|
||||
|
||||
// Load existing notification config if available
|
||||
if file, err := os.Open(notificationConfigPath); err == nil {
|
||||
if err := notificationManager.LoadConfig(file); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to load notification config, starting fresh")
|
||||
} else {
|
||||
log.Info().Str("path", notificationConfigPath).Msg("Loaded notification config from disk")
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
if err := notificationManager.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start notification manager: %w", err)
|
||||
}
|
||||
|
||||
// Create handler that wraps manager and persists config to disk
|
||||
notificationHandler := &persistingNotificationHandler{
|
||||
manager: notificationManager,
|
||||
configPath: notificationConfigPath,
|
||||
}
|
||||
|
||||
// Create agent server using the same shared client service
|
||||
server, err := agent.NewServer(clientService, certs, args.Version(), notificationHandler)
|
||||
server, err := agent.NewServer(client, certs, args.Version(), args.Filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create agent server: %w", err)
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
go func() {
|
||||
log.Info().Msgf("Dozzle agent version %s", args.Version())
|
||||
log.Info().Msgf("Agent listening on %s", listener.Addr().String())
|
||||
|
||||
@@ -2,20 +2,21 @@ package container_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"sync"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type agentService struct {
|
||||
client *agent.Client
|
||||
host atomic.Pointer[container.Host]
|
||||
host container.Host
|
||||
}
|
||||
|
||||
func NewAgentService(client *agent.Client) ClientService {
|
||||
@@ -25,7 +26,7 @@ func NewAgentService(client *agent.Client) ClientService {
|
||||
}
|
||||
|
||||
func (a *agentService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
|
||||
return a.client.FindContainer(ctx, id, labels)
|
||||
return a.client.FindContainer(ctx, id)
|
||||
}
|
||||
|
||||
func (a *agentService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
|
||||
@@ -48,16 +49,13 @@ func (a *agentService) ListContainers(ctx context.Context, labels container.Cont
|
||||
func (a *agentService) Host(ctx context.Context) (container.Host, error) {
|
||||
host, err := a.client.Host(ctx)
|
||||
if err != nil {
|
||||
if cached := a.host.Load(); cached != nil {
|
||||
h := *cached
|
||||
h.Available = false
|
||||
return h, err
|
||||
}
|
||||
return container.Host{Available: false}, err
|
||||
host := a.host
|
||||
host.Available = false
|
||||
return host, err
|
||||
}
|
||||
|
||||
a.host.Store(&host)
|
||||
return host, nil
|
||||
a.host = host
|
||||
return a.host, err
|
||||
}
|
||||
|
||||
func (a *agentService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
@@ -76,14 +74,57 @@ func (a *agentService) ContainerAction(ctx context.Context, container container.
|
||||
return a.client.ContainerAction(ctx, container.ID, action)
|
||||
}
|
||||
|
||||
func (a *agentService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
|
||||
func (a *agentService) Attach(ctx context.Context, container container.Container, stdin io.Reader, stdout io.Writer) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (a *agentService) Exec(ctx context.Context, c container.Container, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
return a.client.Exec(ctx, c.ID, cmd, events, stdout)
|
||||
}
|
||||
func (a *agentService) Exec(ctx context.Context, c container.Container, cmd []string, stdin io.Reader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := a.client.ContainerExec(cancelCtx, c.ID, cmd)
|
||||
|
||||
func (a *agentService) UpdateNotificationConfig(ctx context.Context, subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
|
||||
return a.client.UpdateNotificationConfig(ctx, subscriptions, dispatchers)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Go(func() {
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error decoding event from ws using agent")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
switch event.Type {
|
||||
case "userinput":
|
||||
if _, err := session.Writer.Write([]byte(event.Data)); err != nil {
|
||||
log.Error().Err(err).Msg("error writing to container using agent")
|
||||
break loop
|
||||
}
|
||||
case "resize":
|
||||
if err := session.Resize(event.Width, event.Height); err != nil {
|
||||
log.Error().Err(err).Msg("error resizing terminal using agent")
|
||||
}
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
session.Writer.Close()
|
||||
})
|
||||
|
||||
wg.Go(func() {
|
||||
if _, err := stdcopy.StdCopy(stdout, stdout, session.Reader); err != nil {
|
||||
log.Error().Err(err).Msg("error while writing to ws using agent")
|
||||
}
|
||||
cancel()
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ type ClientService interface {
|
||||
StreamLogs(context.Context, container.Container, time.Time, container.StdType, chan<- *container.LogEvent) error
|
||||
|
||||
// Terminal
|
||||
Attach(context.Context, container.Container, container.ExecEventReader, io.Writer) error
|
||||
Exec(context.Context, container.Container, []string, container.ExecEventReader, io.Writer) error
|
||||
Attach(context.Context, container.Container, io.Reader, io.Writer) error
|
||||
Exec(context.Context, container.Container, []string, io.Reader, io.Writer) error
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ func (c *ContainerService) Action(ctx context.Context, action container.Containe
|
||||
return c.clientService.ContainerAction(ctx, c.Container, action)
|
||||
}
|
||||
|
||||
func (c *ContainerService) Attach(ctx context.Context, events container.ExecEventReader, stdout io.Writer) error {
|
||||
return c.clientService.Attach(ctx, c.Container, events, stdout)
|
||||
func (c *ContainerService) Attach(ctx context.Context, stdin io.Reader, stdout io.Writer) error {
|
||||
return c.clientService.Attach(ctx, c.Container, stdin, stdout)
|
||||
}
|
||||
|
||||
func (c *ContainerService) Exec(ctx context.Context, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
return c.clientService.Exec(ctx, c.Container, cmd, events, stdout)
|
||||
func (c *ContainerService) Exec(ctx context.Context, cmd []string, stdin io.Reader, stdout io.Writer) error {
|
||||
return c.clientService.Exec(ctx, c.Container, cmd, stdin, stdout)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package docker_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -111,7 +112,7 @@ func (d *DockerClientService) SubscribeContainersStarted(ctx context.Context, co
|
||||
d.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
|
||||
func (d *DockerClientService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
|
||||
func (d *DockerClientService) Attach(ctx context.Context, c container.Container, stdin io.Reader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := d.client.ContainerAttach(cancelCtx, c.ID)
|
||||
if err != nil {
|
||||
@@ -122,12 +123,13 @@ func (d *DockerClientService) Attach(ctx context.Context, c container.Container,
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Go(func() {
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error while reading event")
|
||||
log.Error().Err(err).Msg("error while decoding event from ws")
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -168,7 +170,7 @@ func (d *DockerClientService) Attach(ctx context.Context, c container.Container,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerClientService) Exec(ctx context.Context, c container.Container, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
func (d *DockerClientService) Exec(ctx context.Context, c container.Container, cmd []string, stdin io.Reader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := d.client.ContainerExec(cancelCtx, c.ID, cmd)
|
||||
if err != nil {
|
||||
@@ -179,12 +181,13 @@ func (d *DockerClientService) Exec(ctx context.Context, c container.Container, c
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Go(func() {
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error while reading event")
|
||||
log.Error().Err(err).Msg("error while decoding event from ws")
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -208,8 +211,7 @@ func (d *DockerClientService) Exec(ctx context.Context, c container.Container, c
|
||||
})
|
||||
|
||||
wg.Go(func() {
|
||||
// TTY mode outputs raw bytes without Docker's multiplexing headers.
|
||||
if _, err := io.Copy(stdout, session.Reader); err != nil {
|
||||
if _, err := stdcopy.StdCopy(stdout, stdout, session.Reader); err != nil {
|
||||
log.Error().Err(err).Msg("error while writing to ws")
|
||||
}
|
||||
cancel()
|
||||
|
||||
@@ -3,15 +3,10 @@ package docker_support
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
lop "github.com/samber/lo/parallel"
|
||||
)
|
||||
@@ -32,13 +27,11 @@ type ClientManager interface {
|
||||
Subscribe(ctx context.Context, channel chan<- container.Host)
|
||||
Hosts(ctx context.Context) []container.Host
|
||||
LocalClients() []container.Client
|
||||
LocalClientServices() []container_support.ClientService
|
||||
}
|
||||
|
||||
type MultiHostService struct {
|
||||
manager ClientManager
|
||||
timeout time.Duration
|
||||
notificationManager *notification.Manager
|
||||
manager ClientManager
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewMultiHostService(manager ClientManager, timeout time.Duration) *MultiHostService {
|
||||
@@ -129,6 +122,12 @@ func (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events c
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiHostService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
for _, client := range m.manager.List() {
|
||||
client.SubscribeEvents(ctx, events)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
|
||||
newContainers := make(chan container.Container)
|
||||
for _, client := range m.manager.List() {
|
||||
@@ -175,172 +174,6 @@ func (m *MultiHostService) LocalClients() []container.Client {
|
||||
return m.manager.LocalClients()
|
||||
}
|
||||
|
||||
func (m *MultiHostService) LocalClientServices() []container_support.ClientService {
|
||||
return m.manager.LocalClientServices()
|
||||
}
|
||||
|
||||
func (m *MultiHostService) TotalClients() int {
|
||||
return len(m.manager.List())
|
||||
}
|
||||
|
||||
const notificationConfigPath = "./data/notifications.yml"
|
||||
|
||||
// StartNotificationManager initializes and starts the notification manager
|
||||
func (m *MultiHostService) StartNotificationManager(ctx context.Context) error {
|
||||
clients := m.manager.LocalClientServices()
|
||||
listener := notification.NewContainerLogListener(ctx, clients)
|
||||
m.notificationManager = notification.NewManager(listener)
|
||||
|
||||
// Start first so matcher is available for LoadConfig
|
||||
if err := m.notificationManager.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load config if exists
|
||||
if file, err := os.Open(notificationConfigPath); err == nil {
|
||||
defer file.Close()
|
||||
if err := m.notificationManager.LoadConfig(file); err != nil {
|
||||
log.Warn().Err(err).Msg("Could not load notification config")
|
||||
} else {
|
||||
log.Debug().Str("path", notificationConfigPath).Msg("Loaded notification config")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MultiHostService) saveNotificationConfig() {
|
||||
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||
log.Error().Err(err).Msg("Could not create data directory")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Create(notificationConfigPath)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Could not create notification config file")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := m.notificationManager.WriteConfig(file); err != nil {
|
||||
log.Error().Err(err).Msg("Could not write notification config")
|
||||
}
|
||||
|
||||
// Broadcast to all agents
|
||||
m.broadcastNotificationConfig()
|
||||
}
|
||||
|
||||
// NotificationConfigUpdater is an interface for clients that support notification config updates
|
||||
type NotificationConfigUpdater interface {
|
||||
UpdateNotificationConfig(ctx context.Context, subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error
|
||||
}
|
||||
|
||||
// broadcastNotificationConfig sends current notification config to all agent clients
|
||||
func (m *MultiHostService) broadcastNotificationConfig() {
|
||||
notifSubs := m.notificationManager.Subscriptions()
|
||||
notifDispatchers := m.notificationManager.Dispatchers()
|
||||
|
||||
// Convert notification.Subscription to types.SubscriptionConfig
|
||||
subscriptions := make([]types.SubscriptionConfig, len(notifSubs))
|
||||
for i, sub := range notifSubs {
|
||||
subscriptions[i] = types.SubscriptionConfig{
|
||||
ID: sub.ID,
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
DispatcherID: sub.DispatcherID,
|
||||
LogExpression: sub.LogExpression,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert notification.DispatcherConfig to types.DispatcherConfig
|
||||
dispatchers := make([]types.DispatcherConfig, len(notifDispatchers))
|
||||
for i, d := range notifDispatchers {
|
||||
dispatchers[i] = types.DispatcherConfig{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.URL,
|
||||
Template: d.Template,
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, client := range m.manager.List() {
|
||||
// Check if client supports notification config updates (agents do, local docker clients don't)
|
||||
if updater, ok := client.(NotificationConfigUpdater); ok {
|
||||
wg.Go(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
defer cancel()
|
||||
if err := updater.UpdateNotificationConfig(ctx, subscriptions, dispatchers); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to broadcast notification config to agent")
|
||||
} else {
|
||||
log.Debug().Int("subscriptions", len(subscriptions)).Int("dispatchers", len(dispatchers)).Msg("Broadcasted notification config to agent")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// AddSubscription adds a subscription to local manager and broadcasts to agents
|
||||
func (m *MultiHostService) AddSubscription(sub *notification.Subscription) error {
|
||||
if err := m.notificationManager.AddSubscription(sub); err != nil {
|
||||
return err
|
||||
}
|
||||
m.saveNotificationConfig()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSubscription removes a subscription from local manager and broadcasts to agents
|
||||
func (m *MultiHostService) RemoveSubscription(id int) {
|
||||
m.notificationManager.RemoveSubscription(id)
|
||||
m.saveNotificationConfig()
|
||||
}
|
||||
|
||||
// AddDispatcher adds a dispatcher and returns its auto-generated ID
|
||||
func (m *MultiHostService) AddDispatcher(d dispatcher.Dispatcher) int {
|
||||
id := m.notificationManager.AddDispatcher(d)
|
||||
m.saveNotificationConfig()
|
||||
return id
|
||||
}
|
||||
|
||||
// UpdateDispatcher updates a dispatcher by ID
|
||||
func (m *MultiHostService) UpdateDispatcher(id int, d dispatcher.Dispatcher) {
|
||||
m.notificationManager.UpdateDispatcher(id, d)
|
||||
m.saveNotificationConfig()
|
||||
}
|
||||
|
||||
// RemoveDispatcher removes a dispatcher by ID
|
||||
func (m *MultiHostService) RemoveDispatcher(id int) {
|
||||
m.notificationManager.RemoveDispatcher(id)
|
||||
m.saveNotificationConfig()
|
||||
}
|
||||
|
||||
// ReplaceSubscription replaces a subscription with new data
|
||||
func (m *MultiHostService) ReplaceSubscription(sub *notification.Subscription) error {
|
||||
if err := m.notificationManager.ReplaceSubscription(sub); err != nil {
|
||||
return err
|
||||
}
|
||||
m.saveNotificationConfig()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSubscription updates a subscription with the provided fields
|
||||
func (m *MultiHostService) UpdateSubscription(id int, updates map[string]any) error {
|
||||
if err := m.notificationManager.UpdateSubscription(id, updates); err != nil {
|
||||
return err
|
||||
}
|
||||
m.saveNotificationConfig()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subscriptions returns all subscriptions
|
||||
func (m *MultiHostService) Subscriptions() []*notification.Subscription {
|
||||
return m.notificationManager.Subscriptions()
|
||||
}
|
||||
|
||||
// Dispatchers returns all dispatchers
|
||||
func (m *MultiHostService) Dispatchers() []notification.DispatcherConfig {
|
||||
return m.notificationManager.Dispatchers()
|
||||
}
|
||||
|
||||
@@ -195,17 +195,3 @@ func (m *RetriableClientManager) LocalClients() []container.Client {
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (m *RetriableClientManager) LocalClientServices() []container_support.ClientService {
|
||||
services := m.List()
|
||||
|
||||
result := make([]container_support.ClientService, 0)
|
||||
|
||||
for _, service := range services {
|
||||
if _, ok := service.(*DockerClientService); ok {
|
||||
result = append(result, service)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -236,17 +236,3 @@ func (m *SwarmClientManager) String() string {
|
||||
func (m *SwarmClientManager) LocalClients() []container.Client {
|
||||
return []container.Client{m.localClient}
|
||||
}
|
||||
|
||||
func (m *SwarmClientManager) LocalClientServices() []container_support.ClientService {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make([]container_support.ClientService, 0)
|
||||
for _, service := range m.clients {
|
||||
if _, ok := service.(*DockerClientService); ok {
|
||||
result = append(result, service)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/k8s"
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
@@ -98,6 +96,10 @@ func (m *K8sClusterService) SubscribeEventsAndStats(ctx context.Context, events
|
||||
m.client.SubscribeStats(ctx, stats)
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
m.client.SubscribeEvents(ctx, events)
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
|
||||
newContainers := make(chan container.Container)
|
||||
m.client.SubscribeContainersStarted(ctx, newContainers)
|
||||
@@ -133,43 +135,3 @@ func (m *K8sClusterService) SubscribeAvailableHosts(ctx context.Context, hosts c
|
||||
func (m *K8sClusterService) LocalClients() []container.Client {
|
||||
return []container.Client{m.client.client}
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) LocalClientServices() []container_support.ClientService {
|
||||
return []container_support.ClientService{m.client}
|
||||
}
|
||||
|
||||
// Notification methods - not yet implemented for k8s
|
||||
func (m *K8sClusterService) AddSubscription(sub *notification.Subscription) error {
|
||||
|
||||
// TODO Implement notification subscription for k8s mode
|
||||
return fmt.Errorf("notifications not supported in k8s mode")
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) RemoveSubscription(id int) {
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) ReplaceSubscription(sub *notification.Subscription) error {
|
||||
return fmt.Errorf("notifications not supported in k8s mode")
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) UpdateSubscription(id int, updates map[string]any) error {
|
||||
return fmt.Errorf("notifications not supported in k8s mode")
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) Subscriptions() []*notification.Subscription {
|
||||
return []*notification.Subscription{}
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) AddDispatcher(d dispatcher.Dispatcher) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) UpdateDispatcher(id int, d dispatcher.Dispatcher) {
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) RemoveDispatcher(id int) {
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) Dispatchers() []notification.DispatcherConfig {
|
||||
return []notification.DispatcherConfig{}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package k8s_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
@@ -92,7 +93,7 @@ func (k *K8sClientService) SubscribeContainersStarted(ctx context.Context, conta
|
||||
k.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
|
||||
func (k *K8sClientService) Attach(ctx context.Context, c container.Container, stdin io.Reader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := k.client.ContainerAttach(cancelCtx, c.ID)
|
||||
if err != nil {
|
||||
@@ -106,12 +107,13 @@ func (k *K8sClientService) Attach(ctx context.Context, c container.Container, ev
|
||||
defer session.Writer.Close()
|
||||
defer cancel()
|
||||
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error reading event")
|
||||
log.Error().Err(err).Msg("error decoding event")
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -141,7 +143,7 @@ func (k *K8sClientService) Attach(ctx context.Context, c container.Container, ev
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *K8sClientService) Exec(ctx context.Context, c container.Container, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
func (k *K8sClientService) Exec(ctx context.Context, c container.Container, cmd []string, stdin io.Reader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := k.client.ContainerExec(cancelCtx, c.ID, cmd)
|
||||
if err != nil {
|
||||
@@ -155,12 +157,13 @@ func (k *K8sClientService) Exec(ctx context.Context, c container.Container, cmd
|
||||
defer session.Writer.Close()
|
||||
defer cancel()
|
||||
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error reading event")
|
||||
log.Error().Err(err).Msg("error decoding event")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func Test_createRoutes_simple_redirect(t *testing.T) {
|
||||
Users: map[string]*auth.User{
|
||||
"amir": {
|
||||
Username: "amir",
|
||||
Password: "$2a$10$4Tvzu0ms9shlv4B8pIfqI.TM9CoqsamsAznP91A1NGuwg/68SGS1m",
|
||||
Password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
|
||||
},
|
||||
},
|
||||
}, time.Second*100),
|
||||
@@ -55,7 +55,7 @@ func Test_createRoutes_simple_valid_token(t *testing.T) {
|
||||
Users: map[string]*auth.User{
|
||||
"amir": {
|
||||
Username: "amir",
|
||||
Password: "$2a$10$4Tvzu0ms9shlv4B8pIfqI.TM9CoqsamsAznP91A1NGuwg/68SGS1m",
|
||||
Password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
|
||||
},
|
||||
},
|
||||
}, time.Second*100),
|
||||
@@ -100,7 +100,7 @@ func Test_createRoutes_simple_bad_password(t *testing.T) {
|
||||
Users: map[string]*auth.User{
|
||||
"amir": {
|
||||
Username: "amir",
|
||||
Password: "$2a$10$4Tvzu0ms9shlv4B8pIfqI.TM9CoqsamsAznP91A1NGuwg/68SGS1m",
|
||||
Password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
|
||||
},
|
||||
},
|
||||
}, time.Second*100),
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type exchangeTokenResponse struct {
|
||||
Key string `json:"key"`
|
||||
Prefix string `json:"prefix"`
|
||||
ExpiresAt *string `json:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
func (h *handler) cloudCallback(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
http.Error(w, "missing token parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cloudURL := os.Getenv("DOLIGENCE_URL")
|
||||
if cloudURL == "" {
|
||||
cloudURL = "https://doligence.dozzle.dev"
|
||||
}
|
||||
|
||||
exchangeURL := fmt.Sprintf("%s/api/exchange-token", cloudURL)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, exchangeURL, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create request")
|
||||
http.Error(w, "failed to create request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
q := req.URL.Query()
|
||||
q.Set("token", token)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to exchange token")
|
||||
http.Error(w, "failed to exchange token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
log.Error().Int("status", resp.StatusCode).Str("body", string(body)).Msg("Token exchange failed")
|
||||
http.Error(w, "token exchange failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var tokenResp exchangeTokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to decode token response")
|
||||
http.Error(w, "failed to decode token response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenResp.Key == "" {
|
||||
log.Error().Msg("Empty key received")
|
||||
http.Error(w, "empty key received", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var expiresAt *time.Time
|
||||
if tokenResp.ExpiresAt != nil {
|
||||
parsed, err := time.Parse(time.RFC3339, *tokenResp.ExpiresAt)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("expiresAt", *tokenResp.ExpiresAt).Msg("Failed to parse expiresAt, ignoring")
|
||||
} else {
|
||||
expiresAt = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
name := "Dozzle Cloud"
|
||||
|
||||
cloudDispatcher, err := dispatcher.NewCloudDispatcher(name, tokenResp.Key, tokenResp.Prefix, expiresAt)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create cloud dispatcher")
|
||||
http.Error(w, "failed to create cloud dispatcher", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id := h.hostService.AddDispatcher(cloudDispatcher)
|
||||
|
||||
base := h.config.Base
|
||||
if base == "/" {
|
||||
base = ""
|
||||
}
|
||||
redirectURL := fmt.Sprintf("%s/notifications#cloudLinkSuccess=%d", base, id)
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *handler) cloudStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// Find the cloud dispatcher to get the API key
|
||||
var apiKey string
|
||||
for _, d := range h.hostService.Dispatchers() {
|
||||
if d.Type == "cloud" && d.APIKey != "" {
|
||||
apiKey = d.APIKey
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if apiKey == "" {
|
||||
writeError(w, http.StatusNotFound, "no cloud dispatcher configured")
|
||||
return
|
||||
}
|
||||
|
||||
cloudURL := os.Getenv("DOLIGENCE_URL")
|
||||
if cloudURL == "" {
|
||||
cloudURL = "https://doligence.dozzle.dev"
|
||||
}
|
||||
|
||||
statusURL := fmt.Sprintf("%s/api/status", cloudURL)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, statusURL, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create cloud status request")
|
||||
writeError(w, http.StatusInternalServerError, "failed to create request")
|
||||
return
|
||||
}
|
||||
req.Header.Set("X-API-Key", apiKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to fetch cloud status")
|
||||
writeError(w, http.StatusBadGateway, "failed to fetch cloud status")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
log.Warn().Int("status", resp.StatusCode).Str("body", string(body)).Msg("Cloud status check failed")
|
||||
writeError(w, resp.StatusCode, "cloud API key is invalid or expired")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user