mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 16:21:56 +00:00
Compare commits
67 Commits
notification
...
v10.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| cc82f9f993 | |||
| e48aa3139c | |||
| aef834ba06 | |||
| 3678332ab2 | |||
| 30614269c7 | |||
| 6b2af1cf3a | |||
| 80220f2388 | |||
| 98ab88aa71 | |||
| c5af5a6f5e | |||
| b8f224c13d | |||
| 40f335265f | |||
| 9ac5fcc2c4 | |||
| 8d38034c86 | |||
| 8c231d7ba7 | |||
| 0a1d854168 | |||
| 425d9e4fdd | |||
| 21676d9917 | |||
| c923980ae6 | |||
| 87ea90966e | |||
| cd15f91447 | |||
| 8800bc8c37 | |||
| ecdcd5fb26 | |||
| ff5867eaca | |||
| e5422513f7 | |||
| cd164e62c5 | |||
| 03d6c6a41a | |||
| 7131a6bdf8 | |||
| 4c523d5185 | |||
| 69b4d8b651 | |||
| dc41e018ca | |||
| ca3cd93e9a | |||
| 9224a4d1f9 | |||
| c826564e3f | |||
| 8b4f4d1b00 | |||
| 463fea4634 | |||
| 4db74360e6 | |||
| 58caca2d68 | |||
| 98a4901256 | |||
| 4a6461ce2a | |||
| 620e59aa24 | |||
| c13aca6d2c | |||
| f3afbf843c | |||
| 10e4dfd17f | |||
| efcadfb98a | |||
| f831dc6ecd | |||
| 0ff0d486b1 | |||
| a7405ac2d6 | |||
| 3f4baea664 | |||
| 0d71261358 | |||
| 1411c70c9b | |||
| 6c2c58345c | |||
| af0407157b | |||
| 9f6d24523c | |||
| abeb86d285 | |||
| 4954a020f9 | |||
| 5b96267f1f | |||
| e2109257e4 | |||
| 9ac4762f1d | |||
| 321bf43f1c | |||
| bca061fa7b | |||
| 8599b602dc | |||
| d60a119f9e | |||
| 4b3e44fcdc | |||
| 51fce48137 | |||
| c1432c33a2 | |||
| 22d659f509 | |||
| 9c529af633 |
@@ -14,12 +14,12 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.5
|
||||
go-version: 1.25.7
|
||||
check-latest: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
@@ -56,12 +56,12 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -96,12 +96,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.6.0
|
||||
uses: docker/login-action@v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3.6.0
|
||||
uses: docker/login-action@v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -15,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.6.0
|
||||
uses: docker/login-action@v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3.6.0
|
||||
uses: docker/login-action@v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
cache: pnpm # or pnpm / yarn
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.5"
|
||||
go-version: "1.25.7"
|
||||
check-latest: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.5"
|
||||
go-version: "1.25.7"
|
||||
check-latest: true
|
||||
- name: Generate dependencies
|
||||
run: make fake_assets shared_key.pem shared_cert.pem
|
||||
@@ -96,12 +96,12 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -4,3 +4,4 @@ typed-router.d.ts
|
||||
docs/.vitepress/cache
|
||||
docs/.vitepress/dist
|
||||
dist
|
||||
assets/types/graphql.ts
|
||||
|
||||
@@ -31,9 +31,6 @@ 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
|
||||
```
|
||||
@@ -130,8 +127,18 @@ 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)
|
||||
|
||||
- **`main.go`** - Application entry point with mode switching (server/swarm/k8s)
|
||||
- **`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)
|
||||
|
||||
### Frontend (Vue 3)
|
||||
|
||||
@@ -161,17 +168,23 @@ 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
|
||||
- `container.ts`: Container state management
|
||||
- `config.ts`: App configuration and feature flags (injected from backend HTML, frozen immutable)
|
||||
- `container.ts`: Container state management with EventSource streaming (`/api/events/stream`)
|
||||
- `hosts.ts`: Multi-host state
|
||||
- `settings.ts`: User preferences
|
||||
- `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
|
||||
|
||||
- **`assets/composable/`** - Vue composables (auto-imported)
|
||||
- `eventStreams.ts`: SSE connection management
|
||||
- `eventStreams.ts`: SSE connection management with buffer-based flushing (250ms debounce)
|
||||
- `historicalLogs.ts`: Historical log fetching
|
||||
- `logContext.ts`: Log filtering and search context
|
||||
- `storage.ts`: LocalStorage abstractions
|
||||
- `logContext.ts`: Log filtering and search context (provide/inject pattern)
|
||||
- `scrollContext.ts`: Scroll state management (paused, progress, currentDate)
|
||||
- `storage.ts`: LocalStorage abstractions with reactivity
|
||||
- `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
|
||||
@@ -185,6 +198,7 @@ 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
|
||||
|
||||
@@ -206,6 +220,8 @@ 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
|
||||
@@ -217,6 +233,13 @@ 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
|
||||
|
||||
@@ -247,7 +270,168 @@ The frontend uses file-based routing with these conventions:
|
||||
|
||||
### Deployment Modes
|
||||
|
||||
- **Server mode**: Single or multi-host Docker monitoring
|
||||
- **Server mode** (default): Single or multi-host Docker monitoring
|
||||
- Uses `RetriableClientManager` with local + remote agent clients
|
||||
- **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
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine AS node
|
||||
FROM --platform=$BUILDPLATFORM node:25.6.0-alpine AS node
|
||||
|
||||
RUN corepack enable
|
||||
RUN npm install -g --force corepack && corepack enable
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -25,7 +25,7 @@ RUN pnpm build
|
||||
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
|
||||
RUN go generate
|
||||
# Generate protos and graphql
|
||||
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,7 +1,4 @@
|
||||
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:
|
||||
@@ -30,9 +27,11 @@ build: dist generate
|
||||
|
||||
.PHONY: docker
|
||||
docker: shared_key.pem shared_cert.pem
|
||||
@docker build --build-arg TAG=local -t amir20/dozzle .
|
||||
@docker build --build-arg TAG=local -t amir20/dozzle:local .
|
||||
|
||||
generate: shared_key.pem shared_cert.pem $(GEN_FILES)
|
||||
.PHONY: generate
|
||||
generate: shared_key.pem shared_cert.pem
|
||||
@go generate ./...
|
||||
|
||||
.PHONY: dev
|
||||
dev: generate fake_assets
|
||||
@@ -50,21 +49,24 @@ 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:latest
|
||||
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:local
|
||||
|
||||
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 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.
|
||||
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.
|
||||
|
||||
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) which is a top like application for monitoring Docker containers. It integrates with Dozzle to allow for linking directly to container logs.
|
||||
> 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.
|
||||
|
||||
## 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 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 🌙
|
||||
- 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
|
||||
|
||||
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
|
||||
Dozzle 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.
|
||||
|
||||
## 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. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
|
||||
The simplest way to use Dozzle is to run the Docker container. 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 the Docker Compose file:
|
||||
Here is a Docker Compose example:
|
||||
|
||||
services:
|
||||
dozzle:
|
||||
@@ -56,11 +56,11 @@ Here is the Docker Compose file:
|
||||
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 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 the documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
|
||||
|
||||
## Swarm Mode
|
||||
|
||||
Dozzle works with Docker Swarm mode. You can run Dozzle as a global service with:
|
||||
Dozzle works with Docker Swarm. You can run Dozzle as a global service:
|
||||
|
||||
$ 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 be used to monitor multiple Docker hosts. You can run Dozzle in agent mode with:
|
||||
Dozzle can monitor multiple Docker hosts. 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 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/).
|
||||
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/).
|
||||
|
||||
### Installation on podman
|
||||
### Installation on Podman
|
||||
|
||||
By default Podman doesn't have a background process but you can enable this for Dozzle to work.
|
||||
By default, Podman doesn't have a background process, but you can enable the remote socket for Dozzle to work.
|
||||
|
||||
Verify first if your podman installation has enabled remote socket:
|
||||
First, verify if your Podman installation has the remote socket enabled:
|
||||
|
||||
```
|
||||
podman info
|
||||
```
|
||||
|
||||
When you get under the key remote socket output like this, its already enabled:
|
||||
If you see output like this under the remote socket key, it's already enabled:
|
||||
|
||||
```
|
||||
remoteSocket:
|
||||
@@ -96,40 +96,40 @@ When you get under the key remote socket output like this, its already enabled:
|
||||
path: /run/user/1000/podman/podman.sock
|
||||
```
|
||||
|
||||
If it's not enabled please follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
|
||||
If it's not enabled, follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
|
||||
|
||||
Once you have the podman remote socket you can run Dozzle on podman.
|
||||
Once the Podman remote socket is enabled, you can run Dozzle:
|
||||
|
||||
```
|
||||
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 docker.io/amir20/dozzle:latest
|
||||
```
|
||||
|
||||
Additionally you have to create a fake engine-id to prevent `host not found` errors. Podman doesn't generate an engine-id like Docker by itself due to its daemonless architecture.
|
||||
Additionally, you 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.
|
||||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
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)
|
||||
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).
|
||||
|
||||
## Security
|
||||
|
||||
Dozzle supports file based authentication and forward proxy like [Authelia](https://www.authelia.com/). These are documented at https://dozzle.dev/guide/authentication.
|
||||
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.
|
||||
|
||||
## Analytics collected
|
||||
## Analytics
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
If you do not want to be tracked at all, see the `--no-analytics` flag below.
|
||||
To disable analytics, use the `--no-analytics` flag.
|
||||
|
||||
## Environment variables and configuration
|
||||
## Environment Variables and Configuration
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Support
|
||||
|
||||
There are many ways you can support Dozzle:
|
||||
There are many ways to 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 are good at writing send PRs to improve the documentation at [dozzle.dev](https://dozzle.dev/)
|
||||
- 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/).
|
||||
- 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,8 +142,9 @@ There are many ways you can support Dozzle:
|
||||
|
||||
To build and test locally:
|
||||
|
||||
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
|
||||
1. Install [Node.js](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
|
||||
2. Install [Go](https://go.dev/doc/install).
|
||||
3. Install tools with `make tools`.
|
||||
4. Install node modules `pnpm install`.
|
||||
5. Run `make dev` to start a development server with hot reload.
|
||||
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.
|
||||
|
||||
Vendored
+22
-6
@@ -27,15 +27,19 @@ 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
|
||||
@@ -78,7 +82,6 @@ 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
|
||||
@@ -131,7 +134,6 @@ 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
|
||||
@@ -206,6 +208,7 @@ 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
|
||||
@@ -262,6 +265,7 @@ 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
|
||||
@@ -305,8 +309,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').useRoute
|
||||
const useRouter: typeof import('vue-router').useRouter
|
||||
const useRoute: typeof import('vue-router/auto').useRoute
|
||||
const useRouter: typeof import('vue-router/auto').useRouter
|
||||
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
|
||||
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
|
||||
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
|
||||
@@ -397,6 +401,12 @@ 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
|
||||
@@ -436,15 +446,19 @@ 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']>
|
||||
@@ -613,6 +627,7 @@ 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']>
|
||||
@@ -669,6 +684,7 @@ 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']>
|
||||
@@ -712,8 +728,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')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['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
+15
@@ -11,6 +11,8 @@ 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']
|
||||
@@ -41,6 +43,8 @@ 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']
|
||||
@@ -80,15 +84,21 @@ 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: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']
|
||||
@@ -98,8 +108,13 @@ 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']
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
|
||||
<div class="@container flex min-w-0 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 items-center text-sm">
|
||||
<div class="breadcrumbs overflow-x-visible p-0 font-mono">
|
||||
<div class="inline-flex min-w-0 items-center text-sm">
|
||||
<div class="breadcrumbs min-w-0 overflow-x-visible p-0 font-mono">
|
||||
<ul>
|
||||
<li v-if="config.hosts.length > 1" class="font-thin max-md:hidden">
|
||||
{{ container.hostLabel }}
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
<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,6 +73,12 @@
|
||||
{{ $t("action.show-details") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a @click="createAlert()">
|
||||
<mdi:bell />
|
||||
{{ $t("action.create-alert") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
@@ -82,6 +88,7 @@ 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>;
|
||||
@@ -147,6 +154,22 @@ 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 JSONObject, LogEntry } from "@/models/LogEntry";
|
||||
import type { LogEntry, LogMessage } from "@/models/LogEntry";
|
||||
|
||||
const { progress, currentDate } = useScrollContext();
|
||||
|
||||
const { messages } = defineProps<{
|
||||
messages: LogEntry<string | string[] | JSONObject>[];
|
||||
messages: LogEntry<LogMessage>[];
|
||||
}>();
|
||||
|
||||
const { containers } = useLoggingContext();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="flex gap-1 md:gap-4">
|
||||
<div class="grid min-w-15 grid-cols-[auto_1fr] items-center gap-0.5 text-xs leading-none max-md:hidden">
|
||||
<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]"
|
||||
>
|
||||
<PhArrowUp class="text-primary" />
|
||||
<span class="tabular-nums">{{ formatBytes(networkRate.tx, { short: true, decimals: 1 }) }}/s</span>
|
||||
<PhArrowDown class="text-secondary" />
|
||||
@@ -115,7 +117,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 + 1, 0);
|
||||
const sumCpu = hostContainers.reduce((sum, c) => sum + (c.cpuLimit || 0), 0);
|
||||
totalCpu += Math.min(sumCpu, hostTotalCpu);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ 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-->
|
||||
@@ -59,6 +62,9 @@ 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-->
|
||||
@@ -97,6 +103,9 @@ 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-->
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<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>
|
||||
@@ -0,0 +1,343 @@
|
||||
<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>
|
||||
@@ -0,0 +1,68 @@
|
||||
<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>
|
||||
@@ -0,0 +1,364 @@
|
||||
<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>
|
||||
|
||||
<!-- Name (only for webhook type) -->
|
||||
<fieldset v-if="type === 'webhook'" 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>
|
||||
|
||||
<!-- Cloud linked success (when editing cloud with prefix) -->
|
||||
<fieldset v-if="type === 'cloud' && 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 input-success w-full font-mono"
|
||||
/>
|
||||
<span class="join-item btn btn-success pointer-events-none">
|
||||
<mdi:check class="text-lg" />
|
||||
</span>
|
||||
</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 (only for cloud type, when creating or not linked) -->
|
||||
<div v-else-if="type === 'cloud'" 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>
|
||||
|
||||
<!-- Webhook URL (only for webhook type) -->
|
||||
<fieldset v-if="type === 'webhook'" 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 (only for webhook type) -->
|
||||
<fieldset v-if="type === 'webhook' && !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 (only for webhook type) -->
|
||||
<fieldset v-if="type === 'webhook'" 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
|
||||
v-if="type === 'webhook'"
|
||||
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" :class="{ 'btn-primary': type === 'cloud' }" @click="close?.()">
|
||||
{{
|
||||
type === "cloud" ? $t("notifications.destination-form.close") : $t("notifications.destination-form.cancel")
|
||||
}}
|
||||
</button>
|
||||
<button v-if="type === 'webhook'" 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,
|
||||
existingDispatchers = [],
|
||||
showLinkSuccess = false,
|
||||
} = defineProps<{
|
||||
close?: () => void;
|
||||
onCreated?: () => void;
|
||||
destination?: Dispatcher;
|
||||
existingDispatchers?: Dispatcher[];
|
||||
showLinkSuccess?: boolean;
|
||||
}>();
|
||||
|
||||
const hasExistingCloudDestination = computed(() => {
|
||||
// When editing, exclude the current destination from the check
|
||||
const others = isEditing ? existingDispatchers.filter((d) => d.id !== destination!.id) : existingDispatchers;
|
||||
return others.some((d) => d.type === "cloud");
|
||||
});
|
||||
|
||||
const isEditing = !!destination;
|
||||
|
||||
const nameInput = ref<HTMLInputElement>();
|
||||
const templateEditorRef = ref<HTMLElement>();
|
||||
const name = ref(destination?.name ?? "");
|
||||
useFocus(nameInput, { initialValue: true });
|
||||
const type = ref<"webhook" | "cloud">((destination?.type as "webhook" | "cloud") ?? "webhook");
|
||||
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);
|
||||
|
||||
const callbackUrl = `${window.location.origin}${withBase("/")}`;
|
||||
const cloudLinkUrl = `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
const cloudSettingsUrl = `${__CLOUD_URL__}/settings`;
|
||||
|
||||
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(() => {
|
||||
if (type.value === "webhook") {
|
||||
return webhookUrl.value.trim().length > 0;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const isValidUrl = computed(() => {
|
||||
try {
|
||||
new URL(webhookUrl.value.trim());
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const canSave = computed(() => {
|
||||
if (isSaving.value) return false;
|
||||
if (type.value === "cloud") return false;
|
||||
if (type.value === "webhook") {
|
||||
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: type.value,
|
||||
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>
|
||||
@@ -0,0 +1,64 @@
|
||||
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,
|
||||
),
|
||||
};
|
||||
@@ -11,7 +11,7 @@
|
||||
<mdi:close class="swap-on" />
|
||||
</button>
|
||||
</form>
|
||||
<slot v-if="open"></slot>
|
||||
<slot v-if="open" :close="close"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
@@ -28,11 +28,16 @@ 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));
|
||||
|
||||
@@ -260,7 +260,12 @@ export async function loadBetween(
|
||||
params: Ref<URLSearchParams>,
|
||||
from: Date,
|
||||
to: Date,
|
||||
{ lastSeenId, min, maxStart }: { lastSeenId?: number; min?: number; maxStart?: number } = {},
|
||||
{
|
||||
lastSeenId,
|
||||
startId,
|
||||
min,
|
||||
maxStart,
|
||||
}: { lastSeenId?: number; startId?: number; min?: number; maxStart?: number } = {},
|
||||
) {
|
||||
const url = computed(() => `/api/hosts/${container.value.host}/containers/${container.value.id}/logs`);
|
||||
const abortController = new AbortController();
|
||||
@@ -279,6 +284,9 @@ 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"));
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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,6 +96,7 @@ 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) {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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: "success" | "error" | "warning" | "info";
|
||||
type: "error" | "warning" | "info";
|
||||
action?: {
|
||||
label: string;
|
||||
handler: () => void;
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<SideDrawer ref="drawer" :width="drawerWidth">
|
||||
<SideDrawer ref="drawer" :width="drawerWidth" v-slot="{ close }">
|
||||
<Suspense :timeout="0">
|
||||
<component :is="drawerComponent" v-bind="drawerProperties" />
|
||||
<template #fallback> Loading dependencies... </template>
|
||||
<component :is="drawerComponent" v-bind="drawerProperties" :close="close" />
|
||||
<template #fallback> <span class="loading loading-spinner loading-sm"></span></template>
|
||||
</Suspense>
|
||||
</SideDrawer>
|
||||
<ToastModal />
|
||||
|
||||
@@ -151,3 +151,44 @@ 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;
|
||||
}
|
||||
|
||||
+55
-11
@@ -1,5 +1,4 @@
|
||||
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
|
||||
import { useExponentialMovingAverage, useSimpleRefHistory } from "@/utils";
|
||||
import type { ContainerHealth, ContainerJson, ContainerStat, ContainerState } from "@/types/Container";
|
||||
import { Ref } from "vue";
|
||||
|
||||
export type Stat = Omit<ContainerStat, "id">;
|
||||
@@ -51,13 +50,12 @@ export class Container {
|
||||
public readonly group?: string,
|
||||
public health?: ContainerHealth,
|
||||
) {
|
||||
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;
|
||||
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._name = name;
|
||||
}
|
||||
@@ -85,6 +83,7 @@ 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"]
|
||||
);
|
||||
@@ -115,11 +114,56 @@ 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 {
|
||||
// @ts-ignore
|
||||
this._stat = stat;
|
||||
(this._stat as unknown as 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<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,3 +4,5 @@ declare module "*.vue" {
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare const __CLOUD_URL__: string;
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
type Announcement = {
|
||||
interface Release {
|
||||
name: string;
|
||||
announcement: boolean;
|
||||
createdAt: Date;
|
||||
body: string;
|
||||
mentionsCount: number;
|
||||
tag: string;
|
||||
body: string;
|
||||
createdAt: 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[]>([]);
|
||||
@@ -20,8 +24,14 @@ async function fetchReleases() {
|
||||
fetched = true;
|
||||
|
||||
try {
|
||||
const { data } = await useFetch(withBase("/api/releases")).get().json<Announcement[]>();
|
||||
releases.value = data.value || [];
|
||||
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,
|
||||
})) || [];
|
||||
} catch (error) {
|
||||
console.error("Error while fetching releases:\n", error);
|
||||
fetched = false;
|
||||
@@ -35,10 +45,7 @@ if (config.releaseCheckMode === "automatic") {
|
||||
const otherAnnouncements = [] as Announcement[];
|
||||
|
||||
const announcements = computed(() => {
|
||||
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());
|
||||
return [...releases.value, ...otherAnnouncements].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
});
|
||||
|
||||
const mostRecent = computed(() => announcements.value?.[0]);
|
||||
|
||||
@@ -140,28 +140,7 @@ export const useContainerStore = defineStore("container", () => {
|
||||
existing.name = c.name;
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
}),
|
||||
];
|
||||
containers.value = [...containers.value, ...newContainers.map(Container.fromJSON)];
|
||||
};
|
||||
|
||||
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
|
||||
|
||||
Vendored
+31
-13
@@ -1,26 +1,29 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// oxfmt-ignore
|
||||
// @ts-nocheck
|
||||
// noinspection ES6UnusedImports
|
||||
// Generated by unplugin-vue-router. !! DO NOT MODIFY THIS FILE !!
|
||||
// Generated by 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.
|
||||
|
||||
declare module 'vue-router/auto-resolver' {
|
||||
export type ParamParserCustom = never
|
||||
import type {
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'vue-router'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface TypesConfig {
|
||||
ParamParsers: never
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue-router/auto-routes' {
|
||||
import type {
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'vue-router'
|
||||
|
||||
/**
|
||||
* Route name map generated by unplugin-vue-router
|
||||
* Route name map generated by vue-router
|
||||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<
|
||||
@@ -86,6 +89,13 @@ 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',
|
||||
@@ -124,7 +134,7 @@ declare module 'vue-router/auto-routes' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Route file to route info map by unplugin-vue-router.
|
||||
* Route file to route info map by 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:
|
||||
@@ -188,6 +198,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'assets/pages/notifications.vue': {
|
||||
routes:
|
||||
| '/notifications'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'assets/pages/owner/[name].vue': {
|
||||
routes:
|
||||
| '/owner/[name]'
|
||||
@@ -231,3 +247,5 @@ declare module 'vue-router/auto-routes' {
|
||||
? Info['routes']
|
||||
: keyof RouteNamedMap
|
||||
}
|
||||
|
||||
export {}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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.57.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.58.2-jammy
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
@@ -27,3 +27,12 @@ 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,14 +1,39 @@
|
||||
#!/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="${1:-dozzle-agent}"
|
||||
DISTRO="${2:-ubuntu}"
|
||||
AGENT_PORT="${3:-7007}"
|
||||
VM_NAME="${POSITIONAL_ARGS[0]:-dozzle-agent}"
|
||||
DISTRO="${POSITIONAL_ARGS[1]:-ubuntu}"
|
||||
AGENT_PORT="${POSITIONAL_ARGS[2]:-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
|
||||
@@ -40,10 +65,7 @@ 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
|
||||
@@ -62,17 +84,44 @@ cat "$SHARED_KEY" | orb exec -m "$VM_NAME" bash -c 'cat > ~/dozzle-certs/shared_
|
||||
|
||||
echo "✅ Certificates copied"
|
||||
|
||||
# Step 4: Start Dozzle agent
|
||||
# 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
|
||||
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 \
|
||||
amir20/dozzle:latest agent \
|
||||
-e DOZZLE_LEVEL=debug \
|
||||
$DOZZLE_IMAGE agent \
|
||||
--cert /certs/shared_cert.pem \
|
||||
--key /certs/shared_key.pem
|
||||
"; then
|
||||
@@ -82,11 +131,11 @@ fi
|
||||
|
||||
echo "✅ Dozzle agent started"
|
||||
|
||||
# Step 5: Wait for agent to be ready
|
||||
# Step 6: Wait for agent to be ready
|
||||
echo "⏳ Waiting for agent to be ready..."
|
||||
sleep 3
|
||||
|
||||
# Step 6: Verify agent is running
|
||||
# Step 7: 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,7 +8,6 @@ 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
|
||||
@@ -17,24 +16,26 @@ 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.47.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.41.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/go-chi/chi/v5 v5.2.3
|
||||
github.com/expr-lang/expr v1.17.7
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
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.2.0
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.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
|
||||
golang.org/x/crypto v0.46.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/sync v0.19.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
@@ -45,9 +46,13 @@ require (
|
||||
)
|
||||
|
||||
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
|
||||
@@ -57,19 +62,21 @@ 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/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/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gohugoio/hugo v0.149.1 // 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
|
||||
@@ -88,24 +95,28 @@ 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.59.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.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.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.10.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
|
||||
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
|
||||
@@ -114,9 +125,14 @@ 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.25.5
|
||||
go 1.25.7
|
||||
|
||||
tool (
|
||||
github.com/air-verse/air
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc
|
||||
google.golang.org/protobuf/cmd/protoc-gen-go
|
||||
)
|
||||
|
||||
@@ -1,35 +1,74 @@
|
||||
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/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/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.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=
|
||||
@@ -44,28 +83,12 @@ 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/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/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.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=
|
||||
@@ -74,35 +97,36 @@ 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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
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/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/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=
|
||||
@@ -115,43 +139,62 @@ 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
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/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/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/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/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/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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.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=
|
||||
@@ -159,6 +202,8 @@ 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=
|
||||
@@ -173,6 +218,10 @@ 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=
|
||||
@@ -180,6 +229,12 @@ 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=
|
||||
@@ -193,47 +248,62 @@ 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/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/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/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.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/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/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=
|
||||
@@ -242,13 +312,11 @@ 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=
|
||||
@@ -259,125 +327,76 @@ 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=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
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.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.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/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/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.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/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/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.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/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/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=
|
||||
@@ -386,57 +405,23 @@ 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.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/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/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=
|
||||
@@ -449,22 +434,10 @@ 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.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/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/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=
|
||||
@@ -474,22 +447,10 @@ 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.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/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/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=
|
||||
@@ -499,102 +460,47 @@ 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.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/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/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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/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=
|
||||
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=
|
||||
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/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/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.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=
|
||||
@@ -602,124 +508,27 @@ 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/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/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/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/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=
|
||||
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=
|
||||
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-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
@@ -0,0 +1,177 @@
|
||||
# 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
|
||||
+100
-53
@@ -8,12 +8,14 @@ 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"
|
||||
@@ -274,8 +276,17 @@ func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- cont
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) FindContainer(ctx context.Context, containerID string) (container.Container, error) {
|
||||
response, err := c.client.FindContainer(ctx, &pb.FindContainerRequest{ContainerId: containerID})
|
||||
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)
|
||||
if err != nil {
|
||||
return container.Container{}, err
|
||||
}
|
||||
@@ -365,30 +376,41 @@ func (c *Client) ContainerAttach(ctx context.Context, containerId string) (*cont
|
||||
defer stdoutWriter.Close()
|
||||
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
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 {
|
||||
n, err := stdinReader.Read(buffer)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -412,76 +434,101 @@ func (c *Client) ContainerAttach(ctx context.Context, containerId string) (*cont
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) ContainerExec(ctx context.Context, containerId string, cmd []string) (*container.ExecSession, error) {
|
||||
func (c *Client) Exec(ctx context.Context, containerId string, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
stream, err := c.client.ContainerExec(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
if err = stream.Send(&pb.ContainerExecRequest{
|
||||
ContainerId: containerId,
|
||||
Command: cmd,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
stdinReader, stdinWriter := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer stdoutWriter.Close()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Read from gRPC stream and write to stdout
|
||||
wg.Go(func() {
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stdoutWriter.Write(msg.Stdout)
|
||||
stdout.Write(msg.Stdout)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
buffer := make([]byte, 1024)
|
||||
})
|
||||
|
||||
// Read events and convert to gRPC messages
|
||||
wg.Go(func() {
|
||||
for {
|
||||
n, err := stdinReader.Read(buffer)
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := stream.Send(&pb.ContainerExecRequest{
|
||||
Payload: &pb.ContainerExecRequest_Stdin{
|
||||
Stdin: buffer[:n],
|
||||
},
|
||||
}); 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),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
// 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
|
||||
wg.Wait()
|
||||
return 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,7 +13,6 @@ 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"
|
||||
@@ -27,58 +26,67 @@ const bufSize = 1024 * 1024
|
||||
|
||||
var lis *bufconn.Listener
|
||||
var certs tls.Certificate
|
||||
var client *MockedClient
|
||||
var mockService *MockedClientService
|
||||
|
||||
type MockedClient struct {
|
||||
type MockedClientService struct {
|
||||
mock.Mock
|
||||
container.Client
|
||||
}
|
||||
|
||||
func (m *MockedClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
|
||||
args := m.Called(ctx, id)
|
||||
func (m *MockedClientService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
|
||||
args := m.Called(ctx, id, labels)
|
||||
return args.Get(0).(container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
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) {
|
||||
func (m *MockedClientService) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
|
||||
args := m.Called(ctx, filter)
|
||||
return args.Get(0).([]container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, id, since, stdType)
|
||||
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)
|
||||
return args.Get(0).(io.ReadCloser), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerStats(context.Context, string, chan<- container.ContainerStat) error {
|
||||
return nil
|
||||
func (m *MockedClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
m.Called(ctx, stats)
|
||||
}
|
||||
|
||||
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) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
m.Called(ctx, events)
|
||||
}
|
||||
|
||||
func (m *MockedClient) Host() container.Host {
|
||||
args := m.Called()
|
||||
return args.Get(0).(container.Host)
|
||||
func (m *MockedClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
|
||||
m.Called(ctx, containers)
|
||||
}
|
||||
|
||||
func (m *MockedClient) IsSwarmMode() bool {
|
||||
return false
|
||||
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) SystemInfo() system.Info {
|
||||
return system.Info{ID: "123"}
|
||||
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)
|
||||
}
|
||||
|
||||
var wantedContainer = container.Container{}
|
||||
@@ -103,28 +111,30 @@ func init() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client = &MockedClient{}
|
||||
client.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
|
||||
{
|
||||
ID: "123456",
|
||||
Name: "test",
|
||||
Host: "localhost",
|
||||
State: "running",
|
||||
},
|
||||
mockService = &MockedClientService{}
|
||||
mockService.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
|
||||
wantedContainer,
|
||||
}, nil)
|
||||
|
||||
client.On("Host").Return(container.Host{
|
||||
mockService.On("Host", mock.Anything).Return(container.Host{
|
||||
ID: "localhost",
|
||||
Endpoint: "local",
|
||||
Name: "local",
|
||||
})
|
||||
}, nil)
|
||||
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
|
||||
mockService.On("SubscribeEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return().Run(func(args mock.Arguments) {
|
||||
time.Sleep(5 * time.Second)
|
||||
})
|
||||
|
||||
client.On("FindContainer", mock.Anything, "123456").Return(wantedContainer, nil)
|
||||
server, _ := NewServer(client, certs, "test", container.ContainerLabels{})
|
||||
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)
|
||||
go server.Serve(lis)
|
||||
}
|
||||
|
||||
@@ -138,7 +148,7 @@ func TestFindContainer(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c, _ := rpc.FindContainer(context.Background(), "123456")
|
||||
c, _ := rpc.FindContainer(context.Background(), "123456", container.ContainerLabels{})
|
||||
|
||||
assert.Equal(t, wantedContainer, c)
|
||||
}
|
||||
|
||||
+187
-86
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.3
|
||||
// protoc v6.33.4
|
||||
// source: rpc.proto
|
||||
|
||||
package pb
|
||||
@@ -1270,11 +1270,99 @@ 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\vtypes.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb1\x01\n" +
|
||||
"\trpc.proto\x12\bprotobuf\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\vtypes.proto\"\xb1\x01\n" +
|
||||
"\x15ListContainersRequest\x12C\n" +
|
||||
"\x06filter\x18\x01 \x03(\v2+.protobuf.ListContainersRequest.FilterEntryR\x06filter\x1aS\n" +
|
||||
"\vFilterEntry\x12\x10\n" +
|
||||
@@ -1345,7 +1433,11 @@ 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\x06stdout2\xa1\b\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" +
|
||||
"\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" +
|
||||
@@ -1359,7 +1451,8 @@ 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\x01B\x13Z\x11internal/agent/pbb\x06proto3"
|
||||
"\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"
|
||||
|
||||
var (
|
||||
file_rpc_proto_rawDescOnce sync.Once
|
||||
@@ -1373,92 +1466,100 @@ func file_rpc_proto_rawDescGZIP() []byte {
|
||||
return file_rpc_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
|
||||
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
|
||||
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
|
||||
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
|
||||
(*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
|
||||
}
|
||||
var file_rpc_proto_depIdxs = []int32{
|
||||
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
|
||||
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
|
||||
21, // 15: protobuf.ContainerExecRequest.resize:type_name -> protobuf.ResizePayload
|
||||
21, // 16: protobuf.ContainerAttachRequest.resize:type_name -> protobuf.ResizePayload
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
func init() { file_rpc_proto_init() }
|
||||
@@ -1481,7 +1582,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: 27,
|
||||
NumMessages: 29,
|
||||
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.3
|
||||
// - protoc v6.33.4
|
||||
// source: rpc.proto
|
||||
|
||||
package pb
|
||||
@@ -19,18 +19,19 @@ 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_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"
|
||||
)
|
||||
|
||||
// AgentServiceClient is the client API for AgentService service.
|
||||
@@ -49,6 +50,7 @@ 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 {
|
||||
@@ -239,6 +241,16 @@ 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.
|
||||
@@ -255,6 +267,7 @@ 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()
|
||||
}
|
||||
|
||||
@@ -301,6 +314,9 @@ 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() {}
|
||||
|
||||
@@ -474,6 +490,24 @@ 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)
|
||||
@@ -497,6 +531,10 @@ var AgentService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ContainerAction",
|
||||
Handler: _AgentService_ContainerAction_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateNotificationConfig",
|
||||
Handler: _AgentService_UpdateNotificationConfig_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
+201
-26
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.3
|
||||
// protoc v6.33.4
|
||||
// source: types.proto
|
||||
|
||||
package pb
|
||||
@@ -820,6 +820,166 @@ 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 = "" +
|
||||
@@ -898,7 +1058,20 @@ 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*3\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" +
|
||||
"\x0fContainerAction\x12\t\n" +
|
||||
"\x05Start\x10\x00\x12\b\n" +
|
||||
"\x04Stop\x10\x01\x12\v\n" +
|
||||
@@ -917,34 +1090,36 @@ func file_types_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_types_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
||||
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
|
||||
nil, // 10: protobuf.Container.LabelsEntry
|
||||
nil, // 11: protobuf.Host.LabelsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 13: 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
|
||||
(*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
|
||||
}
|
||||
var file_types_proto_depIdxs = []int32{
|
||||
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
|
||||
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
|
||||
2, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
|
||||
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
|
||||
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
|
||||
3, // 7: protobuf.GroupMessage.fragments:type_name -> protobuf.LogFragment
|
||||
12, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
11, // 9: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
|
||||
14, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
13, // 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
|
||||
@@ -963,7 +1138,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: 11,
|
||||
NumMessages: 13,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
+176
-142
@@ -6,7 +6,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"sync"
|
||||
"io"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent/pb"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
"google.golang.org/grpc"
|
||||
@@ -26,21 +26,40 @@ 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 {
|
||||
client container.Client
|
||||
store *container.ContainerStore
|
||||
version string
|
||||
service ClientService
|
||||
version string
|
||||
notificationConfigHandler NotificationConfigHandler
|
||||
|
||||
pb.UnimplementedAgentServiceServer
|
||||
}
|
||||
|
||||
func newServer(client container.Client, dozzleVersion string, labels container.ContainerLabels) pb.AgentServiceServer {
|
||||
statsCollector := docker.NewDockerStatsCollector(client, labels)
|
||||
func newServer(service ClientService, dozzleVersion string, notificationHandler NotificationConfigHandler) pb.AgentServiceServer {
|
||||
return &server{
|
||||
client: client,
|
||||
version: dozzleVersion,
|
||||
|
||||
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
|
||||
service: service,
|
||||
version: dozzleVersion,
|
||||
notificationConfigHandler: notificationHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,20 +69,18 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
since = in.Since.AsTime()
|
||||
}
|
||||
|
||||
c, err := s.store.FindContainer(in.ContainerId, container.ContainerLabels{})
|
||||
c, err := s.service.FindContainer(out.Context(), in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader, err := s.client.ContainerLogs(out.Context(), in.ContainerId, since, container.StdType(in.StreamTypes))
|
||||
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)
|
||||
}()
|
||||
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(out.Context(), dockerReader, c)
|
||||
|
||||
for event := range g.Events {
|
||||
for event := range events {
|
||||
if event != nil {
|
||||
out.Send(&pb.StreamLogsResponse{
|
||||
Event: logEventToPb(event),
|
||||
@@ -71,31 +88,23 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case e := <-g.Errors:
|
||||
return e
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentService_LogsBetweenDatesServer) error {
|
||||
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
c, err := s.service.FindContainer(out.Context(), in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := s.client.FindContainer(out.Context(), in.ContainerId)
|
||||
events, err := s.service.LogsBetweenDates(out.Context(), c, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(out.Context(), dockerReader, c)
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-g.Events:
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
// Channel closed, exit cleanly
|
||||
return nil
|
||||
@@ -103,8 +112,6 @@ 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
|
||||
}
|
||||
@@ -112,16 +119,24 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
|
||||
}
|
||||
|
||||
func (s *server) StreamRawBytes(in *pb.StreamRawBytesRequest, out pb.AgentService_StreamRawBytesServer) error {
|
||||
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
|
||||
c, err := s.service.FindContainer(out.Context(), in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -142,7 +157,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.store.SubscribeEvents(out.Context(), events)
|
||||
s.service.SubscribeEvents(out.Context(), events)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -164,7 +179,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.store.SubscribeStats(out.Context(), stats)
|
||||
s.service.SubscribeStats(out.Context(), stats)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -193,13 +208,13 @@ func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest)
|
||||
}
|
||||
}
|
||||
|
||||
container, err := s.store.FindContainer(in.ContainerId, labels)
|
||||
c, err := s.service.FindContainer(ctx, in.ContainerId, labels)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
c := container.ToProto()
|
||||
proto := c.ToProto()
|
||||
return &pb.FindContainerResponse{
|
||||
Container: &c,
|
||||
Container: &proto,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -211,15 +226,15 @@ func (s *server) ListContainers(ctx context.Context, in *pb.ListContainersReques
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := s.store.ListContainers(labels)
|
||||
containers, err := s.service.ListContainers(ctx, labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pbContainers []*pb.Container
|
||||
for _, container := range containers {
|
||||
c := container.ToProto()
|
||||
pbContainers = append(pbContainers, &c)
|
||||
for _, c := range containers {
|
||||
proto := c.ToProto()
|
||||
pbContainers = append(pbContainers, &proto)
|
||||
}
|
||||
|
||||
return &pb.ListContainersResponse{
|
||||
@@ -228,7 +243,10 @@ func (s *server) ListContainers(ctx context.Context, in *pb.ListContainersReques
|
||||
}
|
||||
|
||||
func (s *server) HostInfo(ctx context.Context, in *pb.HostInfoRequest) (*pb.HostInfoResponse, error) {
|
||||
host := s.client.Host()
|
||||
host, err := s.service.Host(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.HostInfoResponse{
|
||||
Host: &pb.Host{
|
||||
Id: host.ID,
|
||||
@@ -244,7 +262,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.store.SubscribeNewContainers(out.Context(), containers)
|
||||
go s.service.SubscribeContainersStarted(out.Context(), containers)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -275,8 +293,12 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid action")
|
||||
}
|
||||
|
||||
err := s.client.ContainerActions(ctx, action, in.ContainerId)
|
||||
c, err := s.service.FindContainer(ctx, in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
err = s.service.ContainerAction(ctx, c, action)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
@@ -284,62 +306,62 @@ 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())
|
||||
}
|
||||
|
||||
cancelCtx, cancel := context.WithCancel(stream.Context())
|
||||
session, err := s.client.ContainerExec(cancelCtx, request.ContainerId, request.Command)
|
||||
c, err := s.service.FindContainer(stream.Context(), request.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
reader := &protoEventReader{recv: func() (terminalMessage, error) { return stream.Recv() }}
|
||||
writer := &terminalStreamWriter{send: func(p []byte) error { return stream.Send(&pb.ContainerExecResponse{Stdout: p}) }}
|
||||
|
||||
// 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()
|
||||
if err := s.service.Exec(stream.Context(), c, request.Command, reader, writer); err != nil {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -350,61 +372,73 @@ func (s *server) ContainerAttach(stream pb.AgentService_ContainerAttachServer) e
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
cancelCtx, cancel := context.WithCancel(stream.Context())
|
||||
session, err := s.client.ContainerAttach(cancelCtx, request.ContainerId)
|
||||
c, err := s.service.FindContainer(stream.Context(), request.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
reader := &protoEventReader{recv: func() (terminalMessage, error) { return stream.Recv() }}
|
||||
writer := &terminalStreamWriter{send: func(p []byte) error { return stream.Send(&pb.ContainerAttachResponse{Stdout: p}) }}
|
||||
|
||||
// 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()
|
||||
if err := s.service.Attach(stream.Context(), c, reader, writer); err != nil {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewServer(client container.Client, certificates tls.Certificate, dozzleVersion string, labels container.ContainerLabels) (*grpc.Server, error) {
|
||||
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) {
|
||||
caCertPool := x509.NewCertPool()
|
||||
c, err := x509.ParseCertificate(certificates.Certificate[0])
|
||||
if err != nil {
|
||||
@@ -423,7 +457,7 @@ func NewServer(client container.Client, certificates tls.Certificate, dozzleVers
|
||||
creds := credentials.NewTLS(tlsConfig)
|
||||
|
||||
grpcServer := grpc.NewServer(grpc.Creds(creds))
|
||||
pb.RegisterAgentServiceServer(grpcServer, newServer(client, dozzleVersion, labels))
|
||||
pb.RegisterAgentServiceServer(grpcServer, newServer(service, dozzleVersion, notificationHandler))
|
||||
|
||||
return grpcServer, nil
|
||||
}
|
||||
|
||||
@@ -46,7 +46,9 @@ 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.Fatal().Str("filter", r.Header.Get(p.headerFilter)).Msg("Failed to parse container filter")
|
||||
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
|
||||
}
|
||||
userRoles := All
|
||||
if strings.TrimSpace(r.Header.Get(p.headerRoles)) != "" {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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)
|
||||
}
|
||||
@@ -212,7 +212,8 @@ func UserFromContext(ctx context.Context) *User {
|
||||
if filter, ok := claims["filter"].(string); ok {
|
||||
containerFilter, err = container.ParseContainerFilter(filter)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Str("filter", filter).Msg("Failed to parse container filter")
|
||||
log.Warn().Err(err).Str("filter", filter).Msg("Failed to parse container filter")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
roles := None
|
||||
|
||||
@@ -41,6 +41,11 @@ 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,47 +171,52 @@ func (s *ContainerStore) FindContainer(id string, labels ContainerLabels) (Conta
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
var updated bool
|
||||
container, found := s.containers.Compute(id, func(c *Container, loaded bool) (*Container, xsync.ComputeOp) {
|
||||
if !loaded {
|
||||
return nil, xsync.CancelOp
|
||||
}
|
||||
return *container, nil
|
||||
} else {
|
||||
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 !found {
|
||||
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"},
|
||||
{"trace", "verbose", "ver", "vbs"},
|
||||
{"fatal", "sev", "severe", "crit", "critical"},
|
||||
}
|
||||
|
||||
|
||||
@@ -386,6 +386,8 @@ 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], "/")
|
||||
}
|
||||
@@ -393,6 +395,8 @@ 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],
|
||||
@@ -412,6 +416,8 @@ 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, "/")
|
||||
}
|
||||
@@ -419,6 +425,8 @@ 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,3 +260,105 @@ 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
// Manager manages notification subscriptions and dispatches notifications
|
||||
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
|
||||
}
|
||||
|
||||
// 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),
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 !ok {
|
||||
return fmt.Errorf("subscription not found")
|
||||
}
|
||||
|
||||
log.Debug().Int("id", id).Interface("updates", updates).Msg("Updated subscription")
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
m.processLogEvent(logEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
c, host, err := m.listener.FindContainerWithHost(ctx, logEvent.ContainerID, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("containerID", logEvent.ContainerID).Msg("Failed to find container")
|
||||
return
|
||||
}
|
||||
|
||||
// Skip logs from Dozzle itself to avoid feedback loops
|
||||
if strings.Contains(c.Image, "dozzle") {
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// 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(),
|
||||
}
|
||||
|
||||
encoder := yaml.NewEncoder(w)
|
||||
defer encoder.Close()
|
||||
|
||||
return encoder.Encode(config)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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 v
|
||||
case []container.LogFragment:
|
||||
var parts []string
|
||||
for _, fragment := range v {
|
||||
parts = append(parts, 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
|
||||
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"`
|
||||
}
|
||||
|
||||
// Config represents the persisted notification configuration
|
||||
type Config struct {
|
||||
Subscriptions []*Subscription `json:"subscriptions" yaml:"subscriptions"`
|
||||
Dispatchers []DispatcherConfig `json:"dispatchers" yaml:"dispatchers"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,10 @@ 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"
|
||||
)
|
||||
|
||||
@@ -19,6 +23,37 @@ 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")
|
||||
@@ -43,12 +78,43 @@ 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")
|
||||
server, err := agent.NewServer(client, certs, args.Version(), args.Filter)
|
||||
|
||||
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)
|
||||
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,21 +2,20 @@ package container_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type agentService struct {
|
||||
client *agent.Client
|
||||
host container.Host
|
||||
host atomic.Pointer[container.Host]
|
||||
}
|
||||
|
||||
func NewAgentService(client *agent.Client) ClientService {
|
||||
@@ -26,7 +25,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)
|
||||
return a.client.FindContainer(ctx, id, labels)
|
||||
}
|
||||
|
||||
func (a *agentService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
|
||||
@@ -49,13 +48,16 @@ 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 {
|
||||
host := a.host
|
||||
host.Available = false
|
||||
return host, err
|
||||
if cached := a.host.Load(); cached != nil {
|
||||
h := *cached
|
||||
h.Available = false
|
||||
return h, err
|
||||
}
|
||||
return container.Host{Available: false}, err
|
||||
}
|
||||
|
||||
a.host = host
|
||||
return a.host, err
|
||||
a.host.Store(&host)
|
||||
return host, nil
|
||||
}
|
||||
|
||||
func (a *agentService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
@@ -74,57 +76,14 @@ func (a *agentService) ContainerAction(ctx context.Context, container container.
|
||||
return a.client.ContainerAction(ctx, container.ID, action)
|
||||
}
|
||||
|
||||
func (a *agentService) Attach(ctx context.Context, container container.Container, stdin io.Reader, stdout io.Writer) error {
|
||||
func (a *agentService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
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) UpdateNotificationConfig(ctx context.Context, subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
|
||||
return a.client.UpdateNotificationConfig(ctx, subscriptions, dispatchers)
|
||||
}
|
||||
|
||||
@@ -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, io.Reader, io.Writer) error
|
||||
Exec(context.Context, container.Container, []string, io.Reader, io.Writer) error
|
||||
Attach(context.Context, container.Container, container.ExecEventReader, io.Writer) error
|
||||
Exec(context.Context, container.Container, []string, container.ExecEventReader, 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, stdin io.Reader, stdout io.Writer) error {
|
||||
return c.clientService.Attach(ctx, c.Container, stdin, stdout)
|
||||
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) Exec(ctx context.Context, cmd []string, stdin io.Reader, stdout io.Writer) error {
|
||||
return c.clientService.Exec(ctx, c.Container, cmd, 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)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package docker_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -112,7 +111,7 @@ func (d *DockerClientService) SubscribeContainersStarted(ctx context.Context, co
|
||||
d.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
|
||||
func (d *DockerClientService) Attach(ctx context.Context, c container.Container, stdin io.Reader, stdout io.Writer) error {
|
||||
func (d *DockerClientService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := d.client.ContainerAttach(cancelCtx, c.ID)
|
||||
if err != nil {
|
||||
@@ -123,13 +122,12 @@ func (d *DockerClientService) Attach(ctx context.Context, c container.Container,
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Go(func() {
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error while decoding event from ws")
|
||||
log.Error().Err(err).Msg("error while reading event")
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -170,7 +168,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, stdin io.Reader, stdout io.Writer) error {
|
||||
func (d *DockerClientService) Exec(ctx context.Context, c container.Container, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := d.client.ContainerExec(cancelCtx, c.ID, cmd)
|
||||
if err != nil {
|
||||
@@ -181,13 +179,12 @@ func (d *DockerClientService) Exec(ctx context.Context, c container.Container, c
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Go(func() {
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error while decoding event from ws")
|
||||
log.Error().Err(err).Msg("error while reading event")
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -211,7 +208,8 @@ func (d *DockerClientService) Exec(ctx context.Context, c container.Container, c
|
||||
})
|
||||
|
||||
wg.Go(func() {
|
||||
if _, err := stdcopy.StdCopy(stdout, stdout, session.Reader); err != nil {
|
||||
// TTY mode outputs raw bytes without Docker's multiplexing headers.
|
||||
if _, err := io.Copy(stdout, session.Reader); err != nil {
|
||||
log.Error().Err(err).Msg("error while writing to ws")
|
||||
}
|
||||
cancel()
|
||||
|
||||
@@ -3,10 +3,15 @@ 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"
|
||||
)
|
||||
@@ -27,11 +32,13 @@ 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
|
||||
manager ClientManager
|
||||
timeout time.Duration
|
||||
notificationManager *notification.Manager
|
||||
}
|
||||
|
||||
func NewMultiHostService(manager ClientManager, timeout time.Duration) *MultiHostService {
|
||||
@@ -168,6 +175,172 @@ 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,3 +195,17 @@ 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,3 +236,17 @@ 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,6 +7,8 @@ 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"
|
||||
)
|
||||
@@ -131,3 +133,43 @@ 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,7 +2,6 @@ package k8s_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
@@ -93,7 +92,7 @@ func (k *K8sClientService) SubscribeContainersStarted(ctx context.Context, conta
|
||||
k.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) Attach(ctx context.Context, c container.Container, stdin io.Reader, stdout io.Writer) error {
|
||||
func (k *K8sClientService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := k.client.ContainerAttach(cancelCtx, c.ID)
|
||||
if err != nil {
|
||||
@@ -107,13 +106,12 @@ func (k *K8sClientService) Attach(ctx context.Context, c container.Container, st
|
||||
defer session.Writer.Close()
|
||||
defer cancel()
|
||||
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error decoding event")
|
||||
log.Error().Err(err).Msg("error reading event")
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -143,7 +141,7 @@ func (k *K8sClientService) Attach(ctx context.Context, c container.Container, st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *K8sClientService) Exec(ctx context.Context, c container.Container, cmd []string, stdin io.Reader, stdout io.Writer) error {
|
||||
func (k *K8sClientService) Exec(ctx context.Context, c container.Container, cmd []string, events container.ExecEventReader, stdout io.Writer) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
session, err := k.client.ContainerExec(cancelCtx, c.ID, cmd)
|
||||
if err != nil {
|
||||
@@ -157,13 +155,12 @@ func (k *K8sClientService) Exec(ctx context.Context, c container.Container, cmd
|
||||
defer session.Writer.Close()
|
||||
defer cancel()
|
||||
|
||||
decoder := json.NewDecoder(stdin)
|
||||
loop:
|
||||
for {
|
||||
var event container.ExecEvent
|
||||
if err := decoder.Decode(&event); err != nil {
|
||||
event, err := events.ReadEvent()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error().Err(err).Msg("error decoding event")
|
||||
log.Error().Err(err).Msg("error reading event")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
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)
|
||||
}
|
||||
@@ -127,6 +127,17 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
lastSeenId = uint32(num)
|
||||
}
|
||||
|
||||
startId := uint32(0)
|
||||
if r.URL.Query().Has("startId") {
|
||||
from = from.Add(-50 * time.Millisecond) // Add a little buffer to ensure we find the start event
|
||||
num, err := strconv.ParseUint(r.URL.Query().Get("startId"), 10, 32)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
startId = uint32(num)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
@@ -135,6 +146,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
encoder = json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
startIdFound := startId == 0 // If no startId, consider it already found
|
||||
for {
|
||||
if minimum > 0 && buffer.Len() >= minimum {
|
||||
break
|
||||
@@ -168,6 +180,14 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
continue
|
||||
}
|
||||
|
||||
if !startIdFound {
|
||||
if event.Id == startId {
|
||||
log.Debug().Uint32("startId", startId).Msg("found start id, will include subsequent events")
|
||||
startIdFound = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if lastSeenId != 0 && event.Id == lastSeenId {
|
||||
log.Debug().Uint32("lastSeenId", lastSeenId).Msg("found last seen id")
|
||||
break
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/cache"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/notification"
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
"github.com/amir20/dozzle/internal/releases"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Response types for JSON serialization
|
||||
type NotificationRuleResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ContainerExpression string `json:"containerExpression"`
|
||||
LogExpression string `json:"logExpression"`
|
||||
TriggerCount int64 `json:"triggerCount"`
|
||||
TriggeredContainers int `json:"triggeredContainers"`
|
||||
LastTriggeredAt *time.Time `json:"lastTriggeredAt"`
|
||||
Dispatcher *DispatcherResponse `json:"dispatcher"`
|
||||
}
|
||||
|
||||
type DispatcherResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
Prefix *string `json:"prefix,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
type NotificationRuleInput struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DispatcherID int `json:"dispatcherId"`
|
||||
LogExpression string `json:"logExpression"`
|
||||
ContainerExpression string `json:"containerExpression"`
|
||||
}
|
||||
|
||||
type NotificationRuleUpdateInput struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
DispatcherID *int `json:"dispatcherId,omitempty"`
|
||||
LogExpression *string `json:"logExpression,omitempty"`
|
||||
ContainerExpression *string `json:"containerExpression,omitempty"`
|
||||
}
|
||||
|
||||
type DispatcherInput struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
type PreviewInput struct {
|
||||
ContainerExpression string `json:"containerExpression"`
|
||||
LogExpression *string `json:"logExpression,omitempty"`
|
||||
}
|
||||
|
||||
type PreviewResult struct {
|
||||
ContainerError *string `json:"containerError,omitempty"`
|
||||
LogError *string `json:"logError,omitempty"`
|
||||
MatchedContainers []container.Container `json:"matchedContainers"`
|
||||
MatchedLogs []container.LogEvent `json:"matchedLogs"`
|
||||
TotalLogs int `json:"totalLogs"`
|
||||
}
|
||||
|
||||
type TestWebhookInput struct {
|
||||
URL string `json:"url"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
type TestWebhookResult struct {
|
||||
Success bool `json:"success"`
|
||||
StatusCode *int `json:"statusCode,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func subscriptionToResponse(sub *notification.Subscription, dispatchers []notification.DispatcherConfig) *NotificationRuleResponse {
|
||||
var lastTriggeredAt *time.Time
|
||||
if t := sub.LastTriggeredAt.Load(); t != nil && !t.IsZero() {
|
||||
lastTriggeredAt = t
|
||||
}
|
||||
|
||||
var disp *DispatcherResponse
|
||||
for _, d := range dispatchers {
|
||||
if d.ID == sub.DispatcherID {
|
||||
disp = dispatcherConfigToResponse(&d)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &NotificationRuleResponse{
|
||||
ID: sub.ID,
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
Dispatcher: disp,
|
||||
LogExpression: sub.LogExpression,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
TriggerCount: sub.TriggerCount.Load(),
|
||||
LastTriggeredAt: lastTriggeredAt,
|
||||
TriggeredContainers: sub.TriggeredContainersCount(),
|
||||
}
|
||||
}
|
||||
|
||||
func dispatcherConfigToResponse(d *notification.DispatcherConfig) *DispatcherResponse {
|
||||
var url *string
|
||||
if d.URL != "" {
|
||||
url = &d.URL
|
||||
}
|
||||
var template *string
|
||||
if d.Template != "" {
|
||||
template = &d.Template
|
||||
}
|
||||
var prefix *string
|
||||
if d.Prefix != "" {
|
||||
prefix = &d.Prefix
|
||||
}
|
||||
return &DispatcherResponse{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: url,
|
||||
Template: template,
|
||||
Prefix: prefix,
|
||||
ExpiresAt: d.ExpiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
log.Error().Err(err).Msg("failed to encode JSON response")
|
||||
}
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
|
||||
// Notification Rules handlers
|
||||
func (h *handler) listNotificationRules(w http.ResponseWriter, r *http.Request) {
|
||||
subscriptions := h.hostService.Subscriptions()
|
||||
dispatchers := h.hostService.Dispatchers()
|
||||
rules := make([]*NotificationRuleResponse, len(subscriptions))
|
||||
for i, sub := range subscriptions {
|
||||
rules[i] = subscriptionToResponse(sub, dispatchers)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, rules)
|
||||
}
|
||||
|
||||
func (h *handler) getNotificationRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
dispatchers := h.hostService.Dispatchers()
|
||||
for _, sub := range h.hostService.Subscriptions() {
|
||||
if sub.ID == id {
|
||||
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, dispatchers))
|
||||
return
|
||||
}
|
||||
}
|
||||
writeError(w, http.StatusNotFound, "notification rule not found")
|
||||
}
|
||||
|
||||
func (h *handler) createNotificationRule(w http.ResponseWriter, r *http.Request) {
|
||||
var input NotificationRuleInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
sub := ¬ification.Subscription{
|
||||
Name: input.Name,
|
||||
Enabled: input.Enabled,
|
||||
DispatcherID: input.DispatcherID,
|
||||
LogExpression: input.LogExpression,
|
||||
ContainerExpression: input.ContainerExpression,
|
||||
}
|
||||
|
||||
if err := h.hostService.AddSubscription(sub); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, subscriptionToResponse(sub, h.hostService.Dispatchers()))
|
||||
}
|
||||
|
||||
func (h *handler) replaceNotificationRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
var input NotificationRuleInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
sub := ¬ification.Subscription{
|
||||
ID: id,
|
||||
Name: input.Name,
|
||||
Enabled: input.Enabled,
|
||||
DispatcherID: input.DispatcherID,
|
||||
LogExpression: input.LogExpression,
|
||||
ContainerExpression: input.ContainerExpression,
|
||||
}
|
||||
|
||||
if err := h.hostService.ReplaceSubscription(sub); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, h.hostService.Dispatchers()))
|
||||
}
|
||||
|
||||
func (h *handler) updateNotificationRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
var input NotificationRuleUpdateInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]any)
|
||||
if input.Name != nil {
|
||||
updates["name"] = *input.Name
|
||||
}
|
||||
if input.Enabled != nil {
|
||||
updates["enabled"] = *input.Enabled
|
||||
}
|
||||
if input.DispatcherID != nil {
|
||||
updates["dispatcherId"] = *input.DispatcherID
|
||||
}
|
||||
if input.LogExpression != nil {
|
||||
updates["logExpression"] = *input.LogExpression
|
||||
}
|
||||
if input.ContainerExpression != nil {
|
||||
updates["containerExpression"] = *input.ContainerExpression
|
||||
}
|
||||
|
||||
if err := h.hostService.UpdateSubscription(id, updates); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch the updated subscription
|
||||
dispatchers := h.hostService.Dispatchers()
|
||||
for _, sub := range h.hostService.Subscriptions() {
|
||||
if sub.ID == id {
|
||||
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, dispatchers))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
writeError(w, http.StatusNotFound, "notification rule not found")
|
||||
}
|
||||
|
||||
func (h *handler) deleteNotificationRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
h.hostService.RemoveSubscription(id)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Dispatcher handlers
|
||||
func (h *handler) listDispatchers(w http.ResponseWriter, r *http.Request) {
|
||||
dispatchers := h.hostService.Dispatchers()
|
||||
result := make([]*DispatcherResponse, len(dispatchers))
|
||||
for i, d := range dispatchers {
|
||||
result[i] = dispatcherConfigToResponse(&d)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) getDispatcher(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
for _, d := range h.hostService.Dispatchers() {
|
||||
if d.ID == id {
|
||||
writeJSON(w, http.StatusOK, dispatcherConfigToResponse(&d))
|
||||
return
|
||||
}
|
||||
}
|
||||
writeError(w, http.StatusNotFound, "dispatcher not found")
|
||||
}
|
||||
|
||||
func (h *handler) createDispatcher(w http.ResponseWriter, r *http.Request) {
|
||||
var input DispatcherInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
var d dispatcher.Dispatcher
|
||||
switch input.Type {
|
||||
case "webhook":
|
||||
url := ""
|
||||
if input.URL != nil {
|
||||
url = *input.URL
|
||||
}
|
||||
templateStr := ""
|
||||
if input.Template != nil {
|
||||
templateStr = *input.Template
|
||||
}
|
||||
webhook, err := dispatcher.NewWebhookDispatcher(input.Name, url, templateStr)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
d = webhook
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "unknown dispatcher type")
|
||||
return
|
||||
}
|
||||
|
||||
id := h.hostService.AddDispatcher(d)
|
||||
|
||||
writeJSON(w, http.StatusCreated, &DispatcherResponse{
|
||||
ID: id,
|
||||
Name: input.Name,
|
||||
Type: input.Type,
|
||||
URL: input.URL,
|
||||
Template: input.Template,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) updateDispatcher(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
var input DispatcherInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
var d dispatcher.Dispatcher
|
||||
switch input.Type {
|
||||
case "webhook":
|
||||
url := ""
|
||||
if input.URL != nil {
|
||||
url = *input.URL
|
||||
}
|
||||
templateStr := ""
|
||||
if input.Template != nil {
|
||||
templateStr = *input.Template
|
||||
}
|
||||
webhook, err := dispatcher.NewWebhookDispatcher(input.Name, url, templateStr)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
d = webhook
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "unknown dispatcher type")
|
||||
return
|
||||
}
|
||||
|
||||
h.hostService.UpdateDispatcher(id, d)
|
||||
|
||||
writeJSON(w, http.StatusOK, &DispatcherResponse{
|
||||
ID: id,
|
||||
Name: input.Name,
|
||||
Type: input.Type,
|
||||
URL: input.URL,
|
||||
Template: input.Template,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) deleteDispatcher(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
h.hostService.RemoveDispatcher(id)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Preview and test handlers
|
||||
func (h *handler) previewExpression(w http.ResponseWriter, r *http.Request) {
|
||||
var input PreviewInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
result := &PreviewResult{
|
||||
MatchedContainers: []container.Container{},
|
||||
MatchedLogs: []container.LogEvent{},
|
||||
}
|
||||
|
||||
sub := ¬ification.Subscription{
|
||||
ContainerExpression: input.ContainerExpression,
|
||||
}
|
||||
if input.LogExpression != nil {
|
||||
sub.LogExpression = *input.LogExpression
|
||||
}
|
||||
|
||||
// Compile container expression
|
||||
if sub.ContainerExpression != "" {
|
||||
program, err := expr.Compile(sub.ContainerExpression, expr.Env(types.NotificationContainer{}))
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
result.ContainerError = &errStr
|
||||
} else {
|
||||
sub.ContainerProgram = program
|
||||
}
|
||||
}
|
||||
|
||||
// Compile log expression
|
||||
if sub.LogExpression != "" {
|
||||
program, err := expr.Compile(sub.LogExpression, expr.Env(types.NotificationLog{}))
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
result.LogError = &errStr
|
||||
} else {
|
||||
sub.LogProgram = program
|
||||
}
|
||||
}
|
||||
|
||||
// Find matching running containers
|
||||
if sub.ContainerProgram != nil {
|
||||
containers, _ := h.hostService.ListAllContainers(container.ContainerLabels{})
|
||||
for _, c := range containers {
|
||||
if c.State != "running" {
|
||||
continue
|
||||
}
|
||||
// Pass empty host for matching - host fields aren't used in container expressions
|
||||
nc := notification.FromContainerModel(c, container.Host{})
|
||||
if sub.MatchesContainer(nc) {
|
||||
result.MatchedContainers = append(result.MatchedContainers, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch real logs from matched containers
|
||||
if sub.LogProgram != nil && len(result.MatchedContainers) > 0 {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
const maxLogs = 10
|
||||
totalMatched := 0
|
||||
|
||||
for _, c := range result.MatchedContainers {
|
||||
if len(result.MatchedLogs) >= maxLogs {
|
||||
break
|
||||
}
|
||||
|
||||
containerService, err := h.hostService.FindContainer(c.Host, c.ID, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
from := time.Now().Add(-2 * time.Hour)
|
||||
to := time.Now()
|
||||
|
||||
logChan, err := containerService.LogsBetweenDates(ctx, from, to, container.STDALL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for logEvent := range logChan {
|
||||
if logEvent == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
notificationLog := notification.FromLogEvent(*logEvent)
|
||||
if sub.MatchesLog(notificationLog) {
|
||||
totalMatched++
|
||||
if len(result.MatchedLogs) < maxLogs {
|
||||
result.MatchedLogs = append(result.MatchedLogs, *logEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.TotalLogs = totalMatched
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) testWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
var input TestWebhookInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
templateStr := ""
|
||||
if input.Template != nil {
|
||||
templateStr = *input.Template
|
||||
}
|
||||
|
||||
webhook, err := dispatcher.NewWebhookDispatcher("test", input.URL, templateStr)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
writeJSON(w, http.StatusOK, &TestWebhookResult{
|
||||
Success: false,
|
||||
Error: &errStr,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
mockNotification := types.Notification{
|
||||
ID: "test-notification",
|
||||
Timestamp: time.Now(),
|
||||
Container: types.NotificationContainer{
|
||||
ID: "abc123",
|
||||
Name: "test-container",
|
||||
Image: "nginx:latest",
|
||||
State: "running",
|
||||
Health: "healthy",
|
||||
HostID: "localhost",
|
||||
HostName: "localhost",
|
||||
Labels: map[string]string{"env": "test"},
|
||||
},
|
||||
Log: types.NotificationLog{
|
||||
ID: 1,
|
||||
Message: "This is a test log message from Dozzle",
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Level: "info",
|
||||
Stream: "stdout",
|
||||
Type: "simple",
|
||||
},
|
||||
}
|
||||
|
||||
result := webhook.SendTest(r.Context(), mockNotification)
|
||||
|
||||
var statusCode *int
|
||||
if result.StatusCode > 0 {
|
||||
statusCode = &result.StatusCode
|
||||
}
|
||||
|
||||
var errStr *string
|
||||
if result.Error != "" {
|
||||
errStr = &result.Error
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, &TestWebhookResult{
|
||||
Success: result.Success,
|
||||
StatusCode: statusCode,
|
||||
Error: errStr,
|
||||
})
|
||||
}
|
||||
|
||||
// Releases handler
|
||||
var releasesCache *cache.Cache[[]releases.Release]
|
||||
|
||||
func (h *handler) getReleases(w http.ResponseWriter, r *http.Request) {
|
||||
if releasesCache == nil {
|
||||
releasesCache = cache.New(func() ([]releases.Release, error) {
|
||||
return releases.Fetch(h.config.Version)
|
||||
}, time.Hour)
|
||||
}
|
||||
|
||||
result, err := releasesCache.Get()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/cache"
|
||||
"github.com/amir20/dozzle/internal/releases"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var cachedReleases *cache.Cache[[]releases.Release]
|
||||
|
||||
func (h *handler) releases(w http.ResponseWriter, r *http.Request) {
|
||||
if cachedReleases == nil {
|
||||
cachedReleases = cache.New(func() ([]releases.Release, error) {
|
||||
return releases.Fetch(h.config.Version)
|
||||
}, time.Hour)
|
||||
}
|
||||
releases, err, hit := cachedReleases.GetWithHit()
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Debug().Err(err).Msg("error fetching releases")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if hit {
|
||||
w.Header().Set("X-Cache", "HIT")
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(releases); err != nil {
|
||||
log.Error().Err(err).Msg("error encoding releases")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
+43
-1
@@ -10,6 +10,8 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"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/go-chi/chi/v5"
|
||||
@@ -72,6 +74,17 @@ type HostService interface {
|
||||
LocalHost() (container.Host, error)
|
||||
SubscribeAvailableHosts(ctx context.Context, hosts chan<- container.Host)
|
||||
LocalClients() []container.Client
|
||||
LocalClientServices() []container_support.ClientService
|
||||
// Notification methods
|
||||
AddSubscription(sub *notification.Subscription) error
|
||||
RemoveSubscription(id int)
|
||||
ReplaceSubscription(sub *notification.Subscription) error
|
||||
UpdateSubscription(id int, updates map[string]any) error
|
||||
Subscriptions() []*notification.Subscription
|
||||
AddDispatcher(d dispatcher.Dispatcher) int
|
||||
UpdateDispatcher(id int, d dispatcher.Dispatcher)
|
||||
RemoveDispatcher(id int)
|
||||
Dispatchers() []notification.DispatcherConfig
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
@@ -116,6 +129,8 @@ func createRouter(h *handler) *chi.Mux {
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
r.Use(auth.RequireAuthentication)
|
||||
}
|
||||
|
||||
// Log streams
|
||||
r.Get("/hosts/{host}/containers/{id}/logs/stream", h.streamContainerLogs)
|
||||
r.Get("/hosts/{host}/logs/stream", h.streamHostLogs)
|
||||
r.Get("/hosts/{host}/containers/{id}/logs", h.fetchLogsBetweenDates)
|
||||
@@ -124,6 +139,8 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Get("/labels/{labels}/logs/stream", h.streamLogsWithLabels)
|
||||
r.Get("/groups/{group}/logs/stream", h.streamGroupedLogs)
|
||||
r.Get("/events/stream", h.streamEvents)
|
||||
|
||||
// Action
|
||||
if h.config.EnableActions {
|
||||
r.Post("/hosts/{host}/containers/{id}/actions/{action}", h.containerActions)
|
||||
}
|
||||
@@ -131,7 +148,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Get("/hosts/{host}/containers/{id}/attach", h.attach)
|
||||
r.Get("/hosts/{host}/containers/{id}/exec", h.exec)
|
||||
}
|
||||
r.Get("/releases", h.releases)
|
||||
|
||||
if !h.config.DisableAvatars {
|
||||
r.Get("/profile/avatar", h.avatar)
|
||||
}
|
||||
@@ -140,6 +157,28 @@ func createRouter(h *handler) *chi.Mux {
|
||||
if log.Debug().Enabled() {
|
||||
r.Get("/debug/store", h.debugStore)
|
||||
}
|
||||
|
||||
// Notifications API
|
||||
r.Route("/notifications", func(r chi.Router) {
|
||||
r.Get("/rules", h.listNotificationRules)
|
||||
r.Post("/rules", h.createNotificationRule)
|
||||
r.Get("/rules/{id}", h.getNotificationRule)
|
||||
r.Put("/rules/{id}", h.replaceNotificationRule)
|
||||
r.Patch("/rules/{id}", h.updateNotificationRule)
|
||||
r.Delete("/rules/{id}", h.deleteNotificationRule)
|
||||
|
||||
r.Get("/dispatchers", h.listDispatchers)
|
||||
r.Post("/dispatchers", h.createDispatcher)
|
||||
r.Get("/dispatchers/{id}", h.getDispatcher)
|
||||
r.Put("/dispatchers/{id}", h.updateDispatcher)
|
||||
r.Delete("/dispatchers/{id}", h.deleteDispatcher)
|
||||
|
||||
r.Post("/preview", h.previewExpression)
|
||||
r.Post("/test-webhook", h.testWebhook)
|
||||
})
|
||||
|
||||
// Releases API
|
||||
r.Get("/releases", h.getReleases)
|
||||
})
|
||||
|
||||
// Public API routes
|
||||
@@ -147,6 +186,9 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Post("/token", h.createToken)
|
||||
r.Delete("/token", h.deleteToken)
|
||||
}
|
||||
|
||||
// Cloud callback (public, handles OAuth-style code exchange)
|
||||
r.Get("/cloud/callback", h.cloudCallback)
|
||||
})
|
||||
|
||||
r.Get("/healthcheck", h.healthcheck)
|
||||
|
||||
+16
-22
@@ -1,9 +1,12 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -48,9 +51,9 @@ func (h *handler) attach(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
wsReader := &webSocketReader{conn: conn}
|
||||
eventReader := &jsonEventReader{conn: conn}
|
||||
wsWriter := &webSocketWriter{conn: conn}
|
||||
if err = containerService.Attach(r.Context(), wsReader, wsWriter); err != nil {
|
||||
if err = containerService.Attach(r.Context(), eventReader, wsWriter); err != nil {
|
||||
log.Error().Err(err).Msg("error while trying to attach to container")
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("🚨 Error while trying to attach to container\r\n"))
|
||||
return
|
||||
@@ -88,9 +91,9 @@ func (h *handler) exec(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
wsReader := &webSocketReader{conn: conn}
|
||||
eventReader := &jsonEventReader{conn: conn}
|
||||
wsWriter := &webSocketWriter{conn: conn}
|
||||
if err = containerService.Exec(r.Context(), []string{"sh", "-c", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"}, wsReader, wsWriter); err != nil {
|
||||
if err = containerService.Exec(r.Context(), []string{"sh", "-c", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"}, eventReader, wsWriter); err != nil {
|
||||
log.Error().Err(err).Msg("error while trying to attach to container")
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("🚨 Error while trying to attach to container\r\n"))
|
||||
return
|
||||
@@ -106,30 +109,21 @@ func (w *webSocketWriter) Write(p []byte) (int, error) {
|
||||
return len(p), err
|
||||
}
|
||||
|
||||
type webSocketReader struct {
|
||||
conn *websocket.Conn
|
||||
buffer []byte
|
||||
// jsonEventReader reads JSON-encoded ExecEvents from a websocket connection
|
||||
type jsonEventReader struct {
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
func (r *webSocketReader) Read(p []byte) (n int, err error) {
|
||||
if len(r.buffer) > 0 {
|
||||
n = copy(p, r.buffer)
|
||||
r.buffer = r.buffer[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Otherwise, read a new message
|
||||
func (r *jsonEventReader) ReadEvent() (*container.ExecEvent, error) {
|
||||
_, message, err := r.conn.ReadMessage()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
n = copy(p, message)
|
||||
|
||||
// If we couldn't copy the entire message, store the rest in our buffer
|
||||
if n < len(message) {
|
||||
r.buffer = message[n:]
|
||||
var event container.ExecEvent
|
||||
if err := json.Unmarshal(message, &event); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return n, nil
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Kopier permalink
|
||||
see-in-context: Se i kontekst
|
||||
show-details: Vis detaljer
|
||||
create-alert: Opret advarsel
|
||||
label:
|
||||
containers: Containere
|
||||
container: Ingen containere | 1 container | {count} containere
|
||||
@@ -155,3 +156,83 @@ analytics:
|
||||
evaluating_query: Evaluerer forespørgsel...
|
||||
total_records: I alt {count} poster.
|
||||
showing_first: Viser de første {count}.
|
||||
notifications:
|
||||
title: Notifikationer
|
||||
description: Konfigurer hvor og hvornår du vil modtage alarmer
|
||||
destinations: Destinationer
|
||||
add-destination: Tilføj destination
|
||||
alerts: Alarmer
|
||||
add: Tilføj
|
||||
filter:
|
||||
all: Alle ({count})
|
||||
enabled: Aktiveret ({count})
|
||||
paused: Pauset ({count})
|
||||
no-alerts: Ingen alarmer konfigureret endnu. Opret en for at komme i gang.
|
||||
alert:
|
||||
paused: Pauset
|
||||
containers: Containere
|
||||
log-filter: Log filter
|
||||
destination: Destination
|
||||
dispatcher-deleted: Dispatcher slettet
|
||||
containers-count: "{count} containere"
|
||||
triggered-count: "{count} udløst"
|
||||
last-triggered: "Sidst: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Rediger
|
||||
delete: Slet
|
||||
alert-form:
|
||||
create-title: Opret Alarm
|
||||
edit-title: Rediger Alarm
|
||||
description: Abonner på log hændelser der matcher dine kriterier
|
||||
alert-name: Alarm Navn
|
||||
alert-name-placeholder: f.eks. Test API Fejl
|
||||
container-filter: Container Filter
|
||||
containers-match: "{count} containere matcher: {names}"
|
||||
no-containers-match: Ingen containere matcher dette filter
|
||||
log-filter: Log Filter
|
||||
logs-match: "{count} logs matcher"
|
||||
no-logs-match: Ingen logs matcher dette filter
|
||||
destination: Destination
|
||||
select-destination: Vælg en destination
|
||||
no-destinations: Ingen destinationer konfigureret. Tilføj en først.
|
||||
preview: Forhåndsvisning
|
||||
cancel: Annuller
|
||||
save: Gem
|
||||
create: Opret Alarm
|
||||
destination-form:
|
||||
create-title: Tilføj Destination
|
||||
edit-title: Rediger Destination
|
||||
description: Hvor skal notifikationer sendes til?
|
||||
name: Navn
|
||||
name-placeholder: f.eks. Produktion Slack
|
||||
type: Type
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, brugerdefineret endpoint
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Push notifikationer, email alarmer og AI-drevne resuméer
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API Nøgle
|
||||
payload-format: Payload Format
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Brugerdefineret
|
||||
template: Skabelon
|
||||
template-hint: Go skabelon syntaks
|
||||
test: Test
|
||||
test-success: Test succesfuld
|
||||
cancel: Annuller
|
||||
close: Luk
|
||||
save: Gem
|
||||
add: Tilføj Destination
|
||||
cloud-exists: Kun én Dozzle Cloud destination kan konfigureres
|
||||
link-cloud: Link Konto
|
||||
link-cloud-button: Link Dozzle Cloud
|
||||
cloud-settings-hint: For at konfigurere dine administrerede kanaler, gå til
|
||||
cloud-settings-link: Dozzle Cloud Indstillinger
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Linket
|
||||
message: Din konto er blevet succesfuldt linket til Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Permalink kopieren
|
||||
see-in-context: Im Kontext anzeigen
|
||||
show-details: Details anzeigen
|
||||
create-alert: Alarm erstellen
|
||||
label:
|
||||
containers: Container
|
||||
container: Keine Container | 1 Container | {count} Container
|
||||
@@ -155,3 +156,83 @@ analytics:
|
||||
evaluating_query: Abfrage wird ausgewertet...
|
||||
total_records: Insgesamt {count} Datensätze.
|
||||
showing_first: Zeige die ersten {count}.
|
||||
notifications:
|
||||
title: Benachrichtigungen
|
||||
description: Konfigurieren Sie, wo und wann Sie Alarme erhalten möchten
|
||||
destinations: Ziele
|
||||
add-destination: Ziel hinzufügen
|
||||
alerts: Alarme
|
||||
add: Hinzufügen
|
||||
filter:
|
||||
all: Alle ({count})
|
||||
enabled: Aktiviert ({count})
|
||||
paused: Pausiert ({count})
|
||||
no-alerts: Noch keine Alarme konfiguriert. Erstellen Sie einen, um loszulegen.
|
||||
alert:
|
||||
paused: Pausiert
|
||||
containers: Container
|
||||
log-filter: Log-Filter
|
||||
destination: Ziel
|
||||
dispatcher-deleted: Dispatcher gelöscht
|
||||
containers-count: "{count} Container"
|
||||
triggered-count: "{count} ausgelöst"
|
||||
last-triggered: "Zuletzt: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Bearbeiten
|
||||
delete: Löschen
|
||||
alert-form:
|
||||
create-title: Alarm erstellen
|
||||
edit-title: Alarm bearbeiten
|
||||
description: Abonnieren Sie Log-Ereignisse, die Ihren Kriterien entsprechen
|
||||
alert-name: Alarmname
|
||||
alert-name-placeholder: z.B. Test API Fehler
|
||||
container-filter: Container-Filter
|
||||
containers-match: "{count} Container stimmen überein: {names}"
|
||||
no-containers-match: Keine Container entsprechen diesem Filter
|
||||
log-filter: Log-Filter
|
||||
logs-match: "{count} Logs stimmen überein"
|
||||
no-logs-match: Keine Logs entsprechen diesem Filter
|
||||
destination: Ziel
|
||||
select-destination: Wählen Sie ein Ziel
|
||||
no-destinations: Keine Ziele konfiguriert. Fügen Sie zuerst eines hinzu.
|
||||
preview: Vorschau
|
||||
cancel: Abbrechen
|
||||
save: Speichern
|
||||
create: Alarm erstellen
|
||||
destination-form:
|
||||
create-title: Ziel hinzufügen
|
||||
edit-title: Ziel bearbeiten
|
||||
description: Wohin sollen Benachrichtigungen gesendet werden?
|
||||
name: Name
|
||||
name-placeholder: z.B. Produktion Slack
|
||||
type: Typ
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, benutzerdefinierter Endpunkt
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Push-Benachrichtigungen, E-Mail-Alarme und KI-gestützte Zusammenfassungen
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API-Schlüssel
|
||||
payload-format: Payload-Format
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Benutzerdefiniert
|
||||
template: Vorlage
|
||||
template-hint: Go-Template-Syntax
|
||||
test: Testen
|
||||
test-success: Test erfolgreich
|
||||
cancel: Abbrechen
|
||||
close: Schließen
|
||||
save: Speichern
|
||||
add: Ziel hinzufügen
|
||||
cloud-exists: Es kann nur ein Dozzle Cloud Ziel konfiguriert werden
|
||||
link-cloud: Konto verknüpfen
|
||||
link-cloud-button: Dozzle Cloud verknüpfen
|
||||
cloud-settings-hint: Um Ihre verwalteten Kanäle zu konfigurieren, gehen Sie zu
|
||||
cloud-settings-link: Dozzle Cloud Einstellungen
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud verknüpft
|
||||
message: Ihr Konto wurde erfolgreich mit Dozzle Cloud verknüpft.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Copy permalink
|
||||
see-in-context: See in context
|
||||
show-details: Show details
|
||||
create-alert: Create alert
|
||||
label:
|
||||
containers: Containers
|
||||
container: No containers | 1 container | {count} containers
|
||||
@@ -80,6 +81,7 @@ title:
|
||||
login: Authentication Required
|
||||
dashboard: 1 container | {count} containers
|
||||
settings: Settings
|
||||
notifications: Notifications
|
||||
button:
|
||||
logout: Logout
|
||||
login: Login
|
||||
@@ -163,3 +165,83 @@ analytics:
|
||||
evaluating_query: Evaluating query...
|
||||
total_records: Total {count} records.
|
||||
showing_first: Showing first {count}.
|
||||
notifications:
|
||||
title: Notifications
|
||||
description: Configure where and when to receive alerts
|
||||
destinations: Destinations
|
||||
add-destination: Add destination
|
||||
alerts: Alerts
|
||||
add: Add
|
||||
filter:
|
||||
all: All ({count})
|
||||
enabled: Enabled ({count})
|
||||
paused: Paused ({count})
|
||||
no-alerts: No alerts configured yet. Create one to get started.
|
||||
alert:
|
||||
paused: Paused
|
||||
containers: Containers
|
||||
log-filter: Log filter
|
||||
destination: Destination
|
||||
dispatcher-deleted: Dispatcher deleted
|
||||
containers-count: "{count} containers"
|
||||
triggered-count: "{count} triggered"
|
||||
last-triggered: "Last: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Edit
|
||||
delete: Delete
|
||||
alert-form:
|
||||
create-title: Create Alert
|
||||
edit-title: Edit Alert
|
||||
description: Subscribe to log events matching your criteria
|
||||
alert-name: Alert Name
|
||||
alert-name-placeholder: e.g., Test API Errors
|
||||
container-filter: Container Filter
|
||||
containers-match: "{count} containers match: {names}"
|
||||
no-containers-match: No containers match this filter
|
||||
log-filter: Log Filter
|
||||
logs-match: "{count} logs match"
|
||||
no-logs-match: No logs match this filter
|
||||
destination: Destination
|
||||
select-destination: Select a destination
|
||||
no-destinations: No destinations configured. Add one first.
|
||||
preview: Preview
|
||||
cancel: Cancel
|
||||
save: Save
|
||||
create: Create Alert
|
||||
destination-form:
|
||||
create-title: Add Destination
|
||||
edit-title: Edit Destination
|
||||
description: Where should notifications be sent?
|
||||
name: Name
|
||||
name-placeholder: e.g., Production Slack
|
||||
type: Type
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, custom endpoint
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Push notifications, email alerts, and AI-powered summaries
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API Key
|
||||
payload-format: Payload Format
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Custom
|
||||
template: Template
|
||||
template-hint: Go template syntax
|
||||
test: Test
|
||||
test-success: Test successful
|
||||
cancel: Cancel
|
||||
close: Close
|
||||
save: Save
|
||||
add: Add Destination
|
||||
cloud-exists: Only one Dozzle Cloud destination can be configured
|
||||
link-cloud: Link Account
|
||||
link-cloud-button: Link Dozzle Cloud
|
||||
cloud-settings-hint: To configure your managed channels, go to
|
||||
cloud-settings-link: Dozzle Cloud Settings
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Linked
|
||||
message: Your account has been successfully linked to Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Copiar enlace permanente
|
||||
see-in-context: Ver en contexto
|
||||
show-details: Mostrar detalles
|
||||
create-alert: Crear alerta
|
||||
label:
|
||||
containers: Contenedores
|
||||
container: No contenedores | 1 contenedor | {count} contenedores
|
||||
@@ -155,3 +156,83 @@ analytics:
|
||||
evaluating_query: Evaluando consulta...
|
||||
total_records: Total {count} registros.
|
||||
showing_first: Mostrando los primeros {count}.
|
||||
notifications:
|
||||
title: Notificaciones
|
||||
description: Configure dónde y cuándo recibir alertas
|
||||
destinations: Destinos
|
||||
add-destination: Añadir destino
|
||||
alerts: Alertas
|
||||
add: Añadir
|
||||
filter:
|
||||
all: Todos ({count})
|
||||
enabled: Habilitados ({count})
|
||||
paused: Pausados ({count})
|
||||
no-alerts: Aún no hay alertas configuradas. Cree una para comenzar.
|
||||
alert:
|
||||
paused: Pausado
|
||||
containers: Contenedores
|
||||
log-filter: Filtro de registro
|
||||
destination: Destino
|
||||
dispatcher-deleted: Dispatcher eliminado
|
||||
containers-count: "{count} contenedores"
|
||||
triggered-count: "{count} activados"
|
||||
last-triggered: "Último: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Editar
|
||||
delete: Eliminar
|
||||
alert-form:
|
||||
create-title: Crear Alerta
|
||||
edit-title: Editar Alerta
|
||||
description: Suscríbase a eventos de registro que coincidan con sus criterios
|
||||
alert-name: Nombre de la Alerta
|
||||
alert-name-placeholder: ej., Errores de API de Prueba
|
||||
container-filter: Filtro de Contenedor
|
||||
containers-match: "{count} contenedores coinciden: {names}"
|
||||
no-containers-match: Ningún contenedor coincide con este filtro
|
||||
log-filter: Filtro de Registro
|
||||
logs-match: "{count} registros coinciden"
|
||||
no-logs-match: Ningún registro coincide con este filtro
|
||||
destination: Destino
|
||||
select-destination: Seleccione un destino
|
||||
no-destinations: No hay destinos configurados. Añada uno primero.
|
||||
preview: Vista previa
|
||||
cancel: Cancelar
|
||||
save: Guardar
|
||||
create: Crear Alerta
|
||||
destination-form:
|
||||
create-title: Añadir Destino
|
||||
edit-title: Editar Destino
|
||||
description: ¿Dónde deben enviarse las notificaciones?
|
||||
name: Nombre
|
||||
name-placeholder: ej., Slack de Producción
|
||||
type: Tipo
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, endpoint personalizado
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Notificaciones push, alertas por correo electrónico y resúmenes con IA
|
||||
webhook-url: URL del Webhook
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: Clave API
|
||||
payload-format: Formato de Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Personalizado
|
||||
template: Plantilla
|
||||
template-hint: Sintaxis de plantilla Go
|
||||
test: Probar
|
||||
test-success: Prueba exitosa
|
||||
cancel: Cancelar
|
||||
close: Cerrar
|
||||
save: Guardar
|
||||
add: Añadir Destino
|
||||
cloud-exists: Solo se puede configurar un destino de Dozzle Cloud
|
||||
link-cloud: Vincular Cuenta
|
||||
link-cloud-button: Vincular Dozzle Cloud
|
||||
cloud-settings-hint: Para configurar sus canales administrados, vaya a
|
||||
cloud-settings-link: Configuración de Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Vinculado
|
||||
message: Su cuenta se ha vinculado correctamente a Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Copier le permalien
|
||||
see-in-context: Voir dans le contexte
|
||||
show-details: Afficher les détails
|
||||
create-alert: Créer une alerte
|
||||
label:
|
||||
containers: Conteneurs
|
||||
container: Pas de conteneur | 1 conteneur | {count} conteneurs
|
||||
@@ -155,3 +156,83 @@ analytics:
|
||||
evaluating_query: Évaluation de la requête...
|
||||
total_records: Total de {count} enregistrements.
|
||||
showing_first: Affichage des {count} premiers.
|
||||
notifications:
|
||||
title: Notifications
|
||||
description: Configurez où et quand recevoir des alertes
|
||||
destinations: Destinations
|
||||
add-destination: Ajouter une destination
|
||||
alerts: Alertes
|
||||
add: Ajouter
|
||||
filter:
|
||||
all: Tous ({count})
|
||||
enabled: Activés ({count})
|
||||
paused: En pause ({count})
|
||||
no-alerts: Aucune alerte configurée. Créez-en une pour commencer.
|
||||
alert:
|
||||
paused: En pause
|
||||
containers: Conteneurs
|
||||
log-filter: Filtre de journal
|
||||
destination: Destination
|
||||
dispatcher-deleted: Dispatcher supprimé
|
||||
containers-count: "{count} conteneurs"
|
||||
triggered-count: "{count} déclenchés"
|
||||
last-triggered: "Dernier: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Modifier
|
||||
delete: Supprimer
|
||||
alert-form:
|
||||
create-title: Créer une Alerte
|
||||
edit-title: Modifier l'Alerte
|
||||
description: Abonnez-vous aux événements de journal correspondant à vos critères
|
||||
alert-name: Nom de l'Alerte
|
||||
alert-name-placeholder: ex., Erreurs API de Test
|
||||
container-filter: Filtre de Conteneur
|
||||
containers-match: "{count} conteneurs correspondent: {names}"
|
||||
no-containers-match: Aucun conteneur ne correspond à ce filtre
|
||||
log-filter: Filtre de Journal
|
||||
logs-match: "{count} journaux correspondent"
|
||||
no-logs-match: Aucun journal ne correspond à ce filtre
|
||||
destination: Destination
|
||||
select-destination: Sélectionnez une destination
|
||||
no-destinations: Aucune destination configurée. Ajoutez-en une d'abord.
|
||||
preview: Aperçu
|
||||
cancel: Annuler
|
||||
save: Enregistrer
|
||||
create: Créer l'Alerte
|
||||
destination-form:
|
||||
create-title: Ajouter une Destination
|
||||
edit-title: Modifier la Destination
|
||||
description: Où les notifications doivent-elles être envoyées?
|
||||
name: Nom
|
||||
name-placeholder: ex., Slack Production
|
||||
type: Type
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, endpoint personnalisé
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Notifications push, alertes par e-mail et résumés alimentés par l'IA
|
||||
webhook-url: URL du Webhook
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: Clé API
|
||||
payload-format: Format du Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Personnalisé
|
||||
template: Modèle
|
||||
template-hint: Syntaxe de modèle Go
|
||||
test: Tester
|
||||
test-success: Test réussi
|
||||
cancel: Annuler
|
||||
close: Fermer
|
||||
save: Enregistrer
|
||||
add: Ajouter la Destination
|
||||
cloud-exists: Une seule destination Dozzle Cloud peut être configurée
|
||||
link-cloud: Lier le Compte
|
||||
link-cloud-button: Lier Dozzle Cloud
|
||||
cloud-settings-hint: Pour configurer vos canaux gérés, allez à
|
||||
cloud-settings-link: Paramètres Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Lié
|
||||
message: Votre compte a été lié avec succès à Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Salin permalink
|
||||
see-in-context: Lihat dalam konteks
|
||||
show-details: Tampilkan detail
|
||||
create-alert: Buat peringatan
|
||||
|
||||
label:
|
||||
containers: Kontainer
|
||||
@@ -167,3 +168,83 @@ analytics:
|
||||
evaluating_query: Mengevaluasi kueri...
|
||||
total_records: Total {count} entri.
|
||||
showing_first: Menampilkan {count} pertama.
|
||||
notifications:
|
||||
title: Notifikasi
|
||||
description: Konfigurasikan di mana dan kapan menerima peringatan
|
||||
destinations: Tujuan
|
||||
add-destination: Tambah tujuan
|
||||
alerts: Peringatan
|
||||
add: Tambah
|
||||
filter:
|
||||
all: Semua ({count})
|
||||
enabled: Diaktifkan ({count})
|
||||
paused: Dijeda ({count})
|
||||
no-alerts: Belum ada peringatan yang dikonfigurasi. Buat satu untuk memulai.
|
||||
alert:
|
||||
paused: Dijeda
|
||||
containers: Kontainer
|
||||
log-filter: Filter log
|
||||
destination: Tujuan
|
||||
dispatcher-deleted: Dispatcher dihapus
|
||||
containers-count: "{count} kontainer"
|
||||
triggered-count: "{count} terpicu"
|
||||
last-triggered: "Terakhir: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Edit
|
||||
delete: Hapus
|
||||
alert-form:
|
||||
create-title: Buat Peringatan
|
||||
edit-title: Edit Peringatan
|
||||
description: Berlangganan ke event log yang sesuai dengan kriteria Anda
|
||||
alert-name: Nama Peringatan
|
||||
alert-name-placeholder: mis., Kesalahan API Test
|
||||
container-filter: Filter Kontainer
|
||||
containers-match: "{count} kontainer cocok: {names}"
|
||||
no-containers-match: Tidak ada kontainer yang cocok dengan filter ini
|
||||
log-filter: Filter Log
|
||||
logs-match: "{count} log cocok"
|
||||
no-logs-match: Tidak ada log yang cocok dengan filter ini
|
||||
destination: Tujuan
|
||||
select-destination: Pilih tujuan
|
||||
no-destinations: Tidak ada tujuan yang dikonfigurasi. Tambahkan satu terlebih dahulu.
|
||||
preview: Pratinjau
|
||||
cancel: Batal
|
||||
save: Simpan
|
||||
create: Buat Peringatan
|
||||
destination-form:
|
||||
create-title: Tambah Tujuan
|
||||
edit-title: Edit Tujuan
|
||||
description: Ke mana notifikasi harus dikirim?
|
||||
name: Nama
|
||||
name-placeholder: mis., Slack Produksi
|
||||
type: Tipe
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, endpoint kustom
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Notifikasi push, peringatan email, dan ringkasan bertenaga AI
|
||||
webhook-url: URL Webhook
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: Kunci API
|
||||
payload-format: Format Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Kustom
|
||||
template: Template
|
||||
template-hint: Sintaks template Go
|
||||
test: Test
|
||||
test-success: Test berhasil
|
||||
cancel: Batal
|
||||
close: Tutup
|
||||
save: Simpan
|
||||
add: Tambah Tujuan
|
||||
cloud-exists: Hanya satu tujuan Dozzle Cloud yang dapat dikonfigurasi
|
||||
link-cloud: Tautkan Akun
|
||||
link-cloud-button: Tautkan Dozzle Cloud
|
||||
cloud-settings-hint: Untuk mengonfigurasi saluran terkelola Anda, buka
|
||||
cloud-settings-link: Pengaturan Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Tertaut
|
||||
message: Akun Anda telah berhasil ditautkan ke Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Copia permalink
|
||||
see-in-context: Vedi nel contesto
|
||||
show-details: Mostra dettagli
|
||||
create-alert: Crea avviso
|
||||
label:
|
||||
containers: Container
|
||||
container: Nessun container | 1 container | {count} container
|
||||
@@ -155,3 +156,83 @@ analytics:
|
||||
evaluating_query: Valutazione della query...
|
||||
total_records: Totale {count} record.
|
||||
showing_first: Mostrati i primi {count}.
|
||||
notifications:
|
||||
title: Notifiche
|
||||
description: Configura dove e quando ricevere avvisi
|
||||
destinations: Destinazioni
|
||||
add-destination: Aggiungi destinazione
|
||||
alerts: Avvisi
|
||||
add: Aggiungi
|
||||
filter:
|
||||
all: Tutti ({count})
|
||||
enabled: Abilitati ({count})
|
||||
paused: In pausa ({count})
|
||||
no-alerts: Nessun avviso configurato. Creane uno per iniziare.
|
||||
alert:
|
||||
paused: In pausa
|
||||
containers: Container
|
||||
log-filter: Filtro log
|
||||
destination: Destinazione
|
||||
dispatcher-deleted: Dispatcher eliminato
|
||||
containers-count: "{count} container"
|
||||
triggered-count: "{count} attivati"
|
||||
last-triggered: "Ultimo: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Modifica
|
||||
delete: Elimina
|
||||
alert-form:
|
||||
create-title: Crea Avviso
|
||||
edit-title: Modifica Avviso
|
||||
description: Iscriviti agli eventi di log che corrispondono ai tuoi criteri
|
||||
alert-name: Nome Avviso
|
||||
alert-name-placeholder: es., Errori API Test
|
||||
container-filter: Filtro Container
|
||||
containers-match: "{count} container corrispondono: {names}"
|
||||
no-containers-match: Nessun container corrisponde a questo filtro
|
||||
log-filter: Filtro Log
|
||||
logs-match: "{count} log corrispondono"
|
||||
no-logs-match: Nessun log corrisponde a questo filtro
|
||||
destination: Destinazione
|
||||
select-destination: Seleziona una destinazione
|
||||
no-destinations: Nessuna destinazione configurata. Aggiungine una prima.
|
||||
preview: Anteprima
|
||||
cancel: Annulla
|
||||
save: Salva
|
||||
create: Crea Avviso
|
||||
destination-form:
|
||||
create-title: Aggiungi Destinazione
|
||||
edit-title: Modifica Destinazione
|
||||
description: Dove devono essere inviate le notifiche?
|
||||
name: Nome
|
||||
name-placeholder: es., Slack Produzione
|
||||
type: Tipo
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, endpoint personalizzato
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Notifiche push, avvisi email e riepiloghi basati su AI
|
||||
webhook-url: URL Webhook
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: Chiave API
|
||||
payload-format: Formato Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Personalizzato
|
||||
template: Template
|
||||
template-hint: Sintassi template Go
|
||||
test: Test
|
||||
test-success: Test riuscito
|
||||
cancel: Annulla
|
||||
close: Chiudi
|
||||
save: Salva
|
||||
add: Aggiungi Destinazione
|
||||
cloud-exists: È possibile configurare solo una destinazione Dozzle Cloud
|
||||
link-cloud: Collega Account
|
||||
link-cloud-button: Collega Dozzle Cloud
|
||||
cloud-settings-hint: Per configurare i tuoi canali gestiti, vai a
|
||||
cloud-settings-link: Impostazioni Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Collegato
|
||||
message: Il tuo account è stato collegato con successo a Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: 영구 링크 복사
|
||||
see-in-context: 컨텍스트에서 보기
|
||||
show-details: 세부 정보 표시
|
||||
create-alert: 알림 생성
|
||||
label:
|
||||
containers: 컨테이너
|
||||
container: 컨테이너가 없습니다 | 컨테이너 1개 | 컨테이너 {count}개
|
||||
@@ -158,3 +159,83 @@ analytics:
|
||||
evaluating_query: 쿼리를 실행하고 있습니다...
|
||||
total_records: 총 {count}개 기록
|
||||
showing_first: 처음 {count}개 표시
|
||||
notifications:
|
||||
title: 알림
|
||||
description: 알림을 받을 위치와 시간을 설정합니다
|
||||
destinations: 목적지
|
||||
add-destination: 목적지 추가
|
||||
alerts: 알림
|
||||
add: 추가
|
||||
filter:
|
||||
all: 전체 ({count})
|
||||
enabled: 활성화됨 ({count})
|
||||
paused: 일시정지됨 ({count})
|
||||
no-alerts: 아직 설정된 알림이 없습니다. 시작하려면 하나를 만드세요.
|
||||
alert:
|
||||
paused: 일시정지됨
|
||||
containers: 컨테이너
|
||||
log-filter: 로그 필터
|
||||
destination: 목적지
|
||||
dispatcher-deleted: 디스패처 삭제됨
|
||||
containers-count: "컨테이너 {count}개"
|
||||
triggered-count: "{count}회 트리거됨"
|
||||
last-triggered: "마지막: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP 웹훅
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: 편집
|
||||
delete: 삭제
|
||||
alert-form:
|
||||
create-title: 알림 만들기
|
||||
edit-title: 알림 편집
|
||||
description: 기준에 맞는 로그 이벤트를 구독합니다
|
||||
alert-name: 알림 이름
|
||||
alert-name-placeholder: "예: 테스트 API 오류"
|
||||
container-filter: 컨테이너 필터
|
||||
containers-match: "컨테이너 {count}개 일치: {names}"
|
||||
no-containers-match: 이 필터와 일치하는 컨테이너가 없습니다
|
||||
log-filter: 로그 필터
|
||||
logs-match: "로그 {count}개 일치"
|
||||
no-logs-match: 이 필터와 일치하는 로그가 없습니다
|
||||
destination: 목적지
|
||||
select-destination: 목적지 선택
|
||||
no-destinations: 설정된 목적지가 없습니다. 먼저 하나를 추가하세요.
|
||||
preview: 미리보기
|
||||
cancel: 취소
|
||||
save: 저장
|
||||
create: 알림 만들기
|
||||
destination-form:
|
||||
create-title: 목적지 추가
|
||||
edit-title: 목적지 편집
|
||||
description: 알림을 어디로 보낼까요?
|
||||
name: 이름
|
||||
name-placeholder: "예: 프로덕션 Slack"
|
||||
type: 유형
|
||||
webhook-title: HTTP 웹훅
|
||||
webhook-description: Slack, Discord, 사용자 정의 엔드포인트
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: 푸시 알림, 이메일 알림 및 AI 기반 요약
|
||||
webhook-url: 웹훅 URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API 키
|
||||
payload-format: 페이로드 형식
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: 사용자 정의
|
||||
template: 템플릿
|
||||
template-hint: Go 템플릿 구문
|
||||
test: 테스트
|
||||
test-success: 테스트 성공
|
||||
cancel: 취소
|
||||
close: 닫기
|
||||
save: 저장
|
||||
add: 목적지 추가
|
||||
cloud-exists: Dozzle Cloud 목적지는 하나만 설정할 수 있습니다
|
||||
link-cloud: 계정 연결
|
||||
link-cloud-button: Dozzle Cloud 연결
|
||||
cloud-settings-hint: 관리 채널을 설정하려면 다음으로 이동하세요
|
||||
cloud-settings-link: Dozzle Cloud 설정
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud 연결됨
|
||||
message: 계정이 Dozzle Cloud에 성공적으로 연결되었습니다.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Permalink kopiëren
|
||||
see-in-context: Bekijk in context
|
||||
show-details: Toon details
|
||||
create-alert: Waarschuwing aanmaken
|
||||
label:
|
||||
containers: Containers
|
||||
container: Geen containers | 1 container | {count} containers
|
||||
@@ -156,3 +157,83 @@ analytics:
|
||||
evaluating_query: Query wordt geëvalueerd...
|
||||
total_records: In totaal {count} resultaten.
|
||||
showing_first: Eerste {count} worden weergegeven.
|
||||
notifications:
|
||||
title: Meldingen
|
||||
description: Configureer waar en wanneer je waarschuwingen ontvangt
|
||||
destinations: Bestemmingen
|
||||
add-destination: Bestemming toevoegen
|
||||
alerts: Waarschuwingen
|
||||
add: Toevoegen
|
||||
filter:
|
||||
all: Alle ({count})
|
||||
enabled: Ingeschakeld ({count})
|
||||
paused: Gepauzeerd ({count})
|
||||
no-alerts: Nog geen waarschuwingen geconfigureerd. Maak er een om te beginnen.
|
||||
alert:
|
||||
paused: Gepauzeerd
|
||||
containers: Containers
|
||||
log-filter: Logfilter
|
||||
destination: Bestemming
|
||||
dispatcher-deleted: Dispatcher verwijderd
|
||||
containers-count: "{count} containers"
|
||||
triggered-count: "{count} geactiveerd"
|
||||
last-triggered: "Laatst: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Bewerken
|
||||
delete: Verwijderen
|
||||
alert-form:
|
||||
create-title: Waarschuwing aanmaken
|
||||
edit-title: Waarschuwing bewerken
|
||||
description: Abonneer op loggebeurtenissen die voldoen aan je criteria
|
||||
alert-name: Waarschuwingsnaam
|
||||
alert-name-placeholder: bijv. Test API-fouten
|
||||
container-filter: Containerfilter
|
||||
containers-match: "{count} containers komen overeen: {names}"
|
||||
no-containers-match: Geen containers komen overeen met dit filter
|
||||
log-filter: Logfilter
|
||||
logs-match: "{count} logs komen overeen"
|
||||
no-logs-match: Geen logs komen overeen met dit filter
|
||||
destination: Bestemming
|
||||
select-destination: Selecteer een bestemming
|
||||
no-destinations: Geen bestemmingen geconfigureerd. Voeg er eerst een toe.
|
||||
preview: Voorbeeld
|
||||
cancel: Annuleren
|
||||
save: Opslaan
|
||||
create: Waarschuwing aanmaken
|
||||
destination-form:
|
||||
create-title: Bestemming toevoegen
|
||||
edit-title: Bestemming bewerken
|
||||
description: Waar moeten meldingen naartoe worden gestuurd?
|
||||
name: Naam
|
||||
name-placeholder: bijv. Productie Slack
|
||||
type: Type
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, aangepast eindpunt
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Pushmeldingen, e-mailwaarschuwingen en AI-samenvattingen
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API-sleutel
|
||||
payload-format: Payload-formaat
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Aangepast
|
||||
template: Sjabloon
|
||||
template-hint: Go-sjabloonsyntaxis
|
||||
test: Testen
|
||||
test-success: Test geslaagd
|
||||
cancel: Annuleren
|
||||
close: Sluiten
|
||||
save: Opslaan
|
||||
add: Bestemming toevoegen
|
||||
cloud-exists: Er kan slechts één Dozzle Cloud-bestemming worden geconfigureerd
|
||||
link-cloud: Account koppelen
|
||||
link-cloud-button: Dozzle Cloud koppelen
|
||||
cloud-settings-hint: Om je beheerde kanalen te configureren, ga naar
|
||||
cloud-settings-link: Dozzle Cloud-instellingen
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud gekoppeld
|
||||
message: Je account is succesvol gekoppeld aan Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Skopiuj permalink
|
||||
see-in-context: Zobacz w kontekście
|
||||
show-details: Pokaż szczegóły
|
||||
create-alert: Utwórz alert
|
||||
label:
|
||||
service: Brak usług | 1 usługa | {count} usług
|
||||
host-count: Brak Hostów | 1 Host | {count} Hostów
|
||||
@@ -162,3 +163,83 @@ analytics:
|
||||
evaluating_query: Przetwarzanie zapytania...
|
||||
total_records: Razem {count} rekordów.
|
||||
showing_first: Pokazywanie pierwszych {count}.
|
||||
notifications:
|
||||
title: Powiadomienia
|
||||
description: Skonfiguruj gdzie i kiedy otrzymywać alerty
|
||||
destinations: Miejsca docelowe
|
||||
add-destination: Dodaj miejsce docelowe
|
||||
alerts: Alerty
|
||||
add: Dodaj
|
||||
filter:
|
||||
all: Wszystkie ({count})
|
||||
enabled: Włączone ({count})
|
||||
paused: Wstrzymane ({count})
|
||||
no-alerts: Brak skonfigurowanych alertów. Utwórz jeden, aby rozpocząć.
|
||||
alert:
|
||||
paused: Wstrzymany
|
||||
containers: Kontenery
|
||||
log-filter: Filtr logów
|
||||
destination: Miejsce docelowe
|
||||
dispatcher-deleted: Dispatcher usunięty
|
||||
containers-count: "{count} kontenerów"
|
||||
triggered-count: "{count} wyzwolonych"
|
||||
last-triggered: "Ostatnio: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Edytuj
|
||||
delete: Usuń
|
||||
alert-form:
|
||||
create-title: Utwórz Alert
|
||||
edit-title: Edytuj Alert
|
||||
description: Subskrybuj zdarzenia logów pasujące do twoich kryteriów
|
||||
alert-name: Nazwa Alertu
|
||||
alert-name-placeholder: np. Błędy API Testowego
|
||||
container-filter: Filtr Kontenerów
|
||||
containers-match: "{count} kontenerów pasuje: {names}"
|
||||
no-containers-match: Żaden kontener nie pasuje do tego filtra
|
||||
log-filter: Filtr Logów
|
||||
logs-match: "{count} logów pasuje"
|
||||
no-logs-match: Żaden log nie pasuje do tego filtra
|
||||
destination: Miejsce docelowe
|
||||
select-destination: Wybierz miejsce docelowe
|
||||
no-destinations: Brak skonfigurowanych miejsc docelowych. Najpierw dodaj jedno.
|
||||
preview: Podgląd
|
||||
cancel: Anuluj
|
||||
save: Zapisz
|
||||
create: Utwórz Alert
|
||||
destination-form:
|
||||
create-title: Dodaj Miejsce Docelowe
|
||||
edit-title: Edytuj Miejsce Docelowe
|
||||
description: Gdzie mają być wysyłane powiadomienia?
|
||||
name: Nazwa
|
||||
name-placeholder: np. Produkcyjny Slack
|
||||
type: Typ
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, własny endpoint
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Powiadomienia push, alerty e-mail i podsumowania AI
|
||||
webhook-url: URL Webhooka
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: Klucz API
|
||||
payload-format: Format Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Własny
|
||||
template: Szablon
|
||||
template-hint: Składnia szablonu Go
|
||||
test: Test
|
||||
test-success: Test zakończony pomyślnie
|
||||
cancel: Anuluj
|
||||
close: Zamknij
|
||||
save: Zapisz
|
||||
add: Dodaj Miejsce Docelowe
|
||||
cloud-exists: Można skonfigurować tylko jedno miejsce docelowe Dozzle Cloud
|
||||
link-cloud: Połącz Konto
|
||||
link-cloud-button: Połącz Dozzle Cloud
|
||||
cloud-settings-hint: Aby skonfigurować zarządzane kanały, przejdź do
|
||||
cloud-settings-link: Ustawienia Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Połączony
|
||||
message: Twoje konto zostało pomyślnie połączone z Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Copiar hiperligação permanente
|
||||
see-in-context: Ver no contexto
|
||||
show-details: Mostrar detalhes
|
||||
create-alert: Criar alerta
|
||||
label:
|
||||
containers: Contentores
|
||||
container: Nenhum contentor | 1 contentor | {count} contentores
|
||||
@@ -164,3 +165,83 @@ analytics:
|
||||
evaluating_query: A avaliar consulta...
|
||||
total_records: Total de {count} registos.
|
||||
showing_first: A mostrar os primeiros {count}.
|
||||
notifications:
|
||||
title: Notificações
|
||||
description: Configure onde e quando receber alertas
|
||||
destinations: Destinos
|
||||
add-destination: Adicionar destino
|
||||
alerts: Alertas
|
||||
add: Adicionar
|
||||
filter:
|
||||
all: Todos ({count})
|
||||
enabled: Ativados ({count})
|
||||
paused: Pausados ({count})
|
||||
no-alerts: Ainda não há alertas configurados. Crie um para começar.
|
||||
alert:
|
||||
paused: Pausado
|
||||
containers: Contentores
|
||||
log-filter: Filtro de registo
|
||||
destination: Destino
|
||||
dispatcher-deleted: Dispatcher eliminado
|
||||
containers-count: "{count} contentores"
|
||||
triggered-count: "{count} acionados"
|
||||
last-triggered: "Último: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Editar
|
||||
delete: Eliminar
|
||||
alert-form:
|
||||
create-title: Criar Alerta
|
||||
edit-title: Editar Alerta
|
||||
description: Subscreva eventos de registo que correspondam aos seus critérios
|
||||
alert-name: Nome do Alerta
|
||||
alert-name-placeholder: ex., Erros de API de Teste
|
||||
container-filter: Filtro de Contentor
|
||||
containers-match: "{count} contentores correspondem: {names}"
|
||||
no-containers-match: Nenhum contentor corresponde a este filtro
|
||||
log-filter: Filtro de Registo
|
||||
logs-match: "{count} registos correspondem"
|
||||
no-logs-match: Nenhum registo corresponde a este filtro
|
||||
destination: Destino
|
||||
select-destination: Selecione um destino
|
||||
no-destinations: Nenhum destino configurado. Adicione um primeiro.
|
||||
preview: Pré-visualização
|
||||
cancel: Cancelar
|
||||
save: Guardar
|
||||
create: Criar Alerta
|
||||
destination-form:
|
||||
create-title: Adicionar Destino
|
||||
edit-title: Editar Destino
|
||||
description: Para onde devem ser enviadas as notificações?
|
||||
name: Nome
|
||||
name-placeholder: ex., Slack de Produção
|
||||
type: Tipo
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, endpoint personalizado
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Notificações push, alertas por email e resumos com IA
|
||||
webhook-url: URL do Webhook
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: Chave API
|
||||
payload-format: Formato do Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Personalizado
|
||||
template: Modelo
|
||||
template-hint: Sintaxe de modelo Go
|
||||
test: Testar
|
||||
test-success: Teste bem-sucedido
|
||||
cancel: Cancelar
|
||||
close: Fechar
|
||||
save: Guardar
|
||||
add: Adicionar Destino
|
||||
cloud-exists: Apenas um destino Dozzle Cloud pode ser configurado
|
||||
link-cloud: Ligar Conta
|
||||
link-cloud-button: Ligar Dozzle Cloud
|
||||
cloud-settings-hint: Para configurar os seus canais geridos, vá a
|
||||
cloud-settings-link: Definições do Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Ligado
|
||||
message: A sua conta foi ligada com sucesso ao Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Copiar permalink
|
||||
see-in-context: Ver no contexto
|
||||
show-details: Mostrar detalhes
|
||||
create-alert: Criar alerta
|
||||
label:
|
||||
containers: Contêineres
|
||||
container: Nenhum container | 1 container | {count} containers
|
||||
@@ -154,3 +155,83 @@ analytics:
|
||||
evaluating_query: Avaliando consulta...
|
||||
total_records: Total de {count} registros.
|
||||
showing_first: Mostrando os primeiros {count}.
|
||||
notifications:
|
||||
title: Notificações
|
||||
description: Configure onde e quando receber alertas
|
||||
destinations: Destinos
|
||||
add-destination: Adicionar destino
|
||||
alerts: Alertas
|
||||
add: Adicionar
|
||||
filter:
|
||||
all: Todos ({count})
|
||||
enabled: Habilitados ({count})
|
||||
paused: Pausados ({count})
|
||||
no-alerts: Nenhum alerta configurado ainda. Crie um para começar.
|
||||
alert:
|
||||
paused: Pausado
|
||||
containers: Containers
|
||||
log-filter: Filtro de log
|
||||
destination: Destino
|
||||
dispatcher-deleted: Dispatcher excluído
|
||||
containers-count: "{count} containers"
|
||||
triggered-count: "{count} acionados"
|
||||
last-triggered: "Último: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Editar
|
||||
delete: Excluir
|
||||
alert-form:
|
||||
create-title: Criar Alerta
|
||||
edit-title: Editar Alerta
|
||||
description: Inscreva-se em eventos de log que correspondam aos seus critérios
|
||||
alert-name: Nome do Alerta
|
||||
alert-name-placeholder: ex., Erros de API de Teste
|
||||
container-filter: Filtro de Container
|
||||
containers-match: "{count} containers correspondem: {names}"
|
||||
no-containers-match: Nenhum container corresponde a este filtro
|
||||
log-filter: Filtro de Log
|
||||
logs-match: "{count} logs correspondem"
|
||||
no-logs-match: Nenhum log corresponde a este filtro
|
||||
destination: Destino
|
||||
select-destination: Selecione um destino
|
||||
no-destinations: Nenhum destino configurado. Adicione um primeiro.
|
||||
preview: Prévia
|
||||
cancel: Cancelar
|
||||
save: Salvar
|
||||
create: Criar Alerta
|
||||
destination-form:
|
||||
create-title: Adicionar Destino
|
||||
edit-title: Editar Destino
|
||||
description: Para onde as notificações devem ser enviadas?
|
||||
name: Nome
|
||||
name-placeholder: ex., Slack de Produção
|
||||
type: Tipo
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, endpoint personalizado
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Notificações push, alertas por email e resumos com IA
|
||||
webhook-url: URL do Webhook
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: Chave API
|
||||
payload-format: Formato do Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Personalizado
|
||||
template: Template
|
||||
template-hint: Sintaxe de template Go
|
||||
test: Testar
|
||||
test-success: Teste bem-sucedido
|
||||
cancel: Cancelar
|
||||
close: Fechar
|
||||
save: Salvar
|
||||
add: Adicionar Destino
|
||||
cloud-exists: Apenas um destino Dozzle Cloud pode ser configurado
|
||||
link-cloud: Vincular Conta
|
||||
link-cloud-button: Vincular Dozzle Cloud
|
||||
cloud-settings-hint: Para configurar seus canais gerenciados, vá para
|
||||
cloud-settings-link: Configurações do Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Vinculado
|
||||
message: Sua conta foi vinculada com sucesso ao Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Копировать постоянную ссылку
|
||||
see-in-context: Посмотреть в контексте
|
||||
show-details: Показать детали
|
||||
create-alert: Создать оповещение
|
||||
label:
|
||||
containers: Контейнеры
|
||||
container: Нет контейнеров | 1 контейнер | {count} контейнеров
|
||||
@@ -155,3 +156,83 @@ analytics:
|
||||
evaluating_query: Выполнение запроса...
|
||||
total_records: Всего {count} записей.
|
||||
showing_first: Показаны первые {count}.
|
||||
notifications:
|
||||
title: Уведомления
|
||||
description: Настройте где и когда получать оповещения
|
||||
destinations: Назначения
|
||||
add-destination: Добавить назначение
|
||||
alerts: Оповещения
|
||||
add: Добавить
|
||||
filter:
|
||||
all: Все ({count})
|
||||
enabled: Включены ({count})
|
||||
paused: Приостановлены ({count})
|
||||
no-alerts: Оповещения ещё не настроены. Создайте одно для начала.
|
||||
alert:
|
||||
paused: Приостановлено
|
||||
containers: Контейнеры
|
||||
log-filter: Фильтр логов
|
||||
destination: Назначение
|
||||
dispatcher-deleted: Диспетчер удалён
|
||||
containers-count: "{count} контейнеров"
|
||||
triggered-count: "{count} сработало"
|
||||
last-triggered: "Последнее: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Редактировать
|
||||
delete: Удалить
|
||||
alert-form:
|
||||
create-title: Создать Оповещение
|
||||
edit-title: Редактировать Оповещение
|
||||
description: Подпишитесь на события логов, соответствующие вашим критериям
|
||||
alert-name: Имя Оповещения
|
||||
alert-name-placeholder: напр., Ошибки Тестового API
|
||||
container-filter: Фильтр Контейнеров
|
||||
containers-match: "{count} контейнеров соответствует: {names}"
|
||||
no-containers-match: Нет контейнеров, соответствующих этому фильтру
|
||||
log-filter: Фильтр Логов
|
||||
logs-match: "{count} логов соответствует"
|
||||
no-logs-match: Нет логов, соответствующих этому фильтру
|
||||
destination: Назначение
|
||||
select-destination: Выберите назначение
|
||||
no-destinations: Назначения не настроены. Сначала добавьте одно.
|
||||
preview: Предпросмотр
|
||||
cancel: Отмена
|
||||
save: Сохранить
|
||||
create: Создать Оповещение
|
||||
destination-form:
|
||||
create-title: Добавить Назначение
|
||||
edit-title: Редактировать Назначение
|
||||
description: Куда отправлять уведомления?
|
||||
name: Имя
|
||||
name-placeholder: напр., Продакшн Slack
|
||||
type: Тип
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, пользовательская конечная точка
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Push-уведомления, email-оповещения и сводки на основе ИИ
|
||||
webhook-url: URL Webhook
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API Ключ
|
||||
payload-format: Формат Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Пользовательский
|
||||
template: Шаблон
|
||||
template-hint: Синтаксис шаблона Go
|
||||
test: Тест
|
||||
test-success: Тест успешен
|
||||
cancel: Отмена
|
||||
close: Закрыть
|
||||
save: Сохранить
|
||||
add: Добавить Назначение
|
||||
cloud-exists: Можно настроить только одно назначение Dozzle Cloud
|
||||
link-cloud: Связать Аккаунт
|
||||
link-cloud-button: Связать Dozzle Cloud
|
||||
cloud-settings-hint: Чтобы настроить управляемые каналы, перейдите в
|
||||
cloud-settings-link: Настройки Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Связан
|
||||
message: Ваш аккаунт успешно связан с Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Kopiraj trajno povezavo
|
||||
see-in-context: Glej v kontekstu
|
||||
show-details: Prikaži podrobnosti
|
||||
create-alert: Ustvari opozorilo
|
||||
label:
|
||||
containers: Zabojniki
|
||||
host-count: Ni Gostiteljev | 1 Gostitelj | {count} Gostiteljev
|
||||
@@ -160,3 +161,83 @@ analytics:
|
||||
total_records: Skupaj zapisov {count}.
|
||||
showing_first: Prikaz prvih {count}.
|
||||
creating_table: Ustvarjanje začasne tabele...
|
||||
notifications:
|
||||
title: Obvestila
|
||||
description: Nastavite kje in kdaj prejemati opozorila
|
||||
destinations: Cilji
|
||||
add-destination: Dodaj cilj
|
||||
alerts: Opozorila
|
||||
add: Dodaj
|
||||
filter:
|
||||
all: Vsi ({count})
|
||||
enabled: Omogočeni ({count})
|
||||
paused: Ustavljeni ({count})
|
||||
no-alerts: Opozorila še niso nastavljena. Ustvarite enega za začetek.
|
||||
alert:
|
||||
paused: Ustavljeno
|
||||
containers: Zabojniki
|
||||
log-filter: Filter dnevnika
|
||||
destination: Cilj
|
||||
dispatcher-deleted: Pošiljatelj izbrisan
|
||||
containers-count: "{count} zabojnikov"
|
||||
triggered-count: "{count} sproženih"
|
||||
last-triggered: "Zadnje: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Uredi
|
||||
delete: Izbriši
|
||||
alert-form:
|
||||
create-title: Ustvari Opozorilo
|
||||
edit-title: Uredi Opozorilo
|
||||
description: Naročite se na dogodke dnevnika, ki ustrezajo vašim kriterijem
|
||||
alert-name: Ime Opozorila
|
||||
alert-name-placeholder: npr., Napake Testnega API-ja
|
||||
container-filter: Filter Zabojnikov
|
||||
containers-match: "{count} zabojnikov ustreza: {names}"
|
||||
no-containers-match: Noben zabojnik ne ustreza temu filtru
|
||||
log-filter: Filter Dnevnika
|
||||
logs-match: "{count} dnevnikov ustreza"
|
||||
no-logs-match: Noben dnevnik ne ustreza temu filtru
|
||||
destination: Cilj
|
||||
select-destination: Izberite cilj
|
||||
no-destinations: Cilji niso nastavljeni. Najprej dodajte enega.
|
||||
preview: Predogled
|
||||
cancel: Prekliči
|
||||
save: Shrani
|
||||
create: Ustvari Opozorilo
|
||||
destination-form:
|
||||
create-title: Dodaj Cilj
|
||||
edit-title: Uredi Cilj
|
||||
description: Kam naj se pošiljajo obvestila?
|
||||
name: Ime
|
||||
name-placeholder: npr., Produkcijski Slack
|
||||
type: Tip
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, končna točka po meri
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Push obvestila, e-poštna opozorila in povzetki z umetno inteligenco
|
||||
webhook-url: URL Webhook
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API Ključ
|
||||
payload-format: Format Payload
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Po meri
|
||||
template: Predloga
|
||||
template-hint: Sintaksa predloge Go
|
||||
test: Test
|
||||
test-success: Test uspešen
|
||||
cancel: Prekliči
|
||||
close: Zapri
|
||||
save: Shrani
|
||||
add: Dodaj Cilj
|
||||
cloud-exists: Nastavljen je lahko samo en cilj Dozzle Cloud
|
||||
link-cloud: Poveži Račun
|
||||
link-cloud-button: Poveži Dozzle Cloud
|
||||
cloud-settings-hint: Za nastavitev upravljanih kanalov pojdite na
|
||||
cloud-settings-link: Nastavitve Dozzle Cloud
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Povezan
|
||||
message: Vaš račun je bil uspešno povezan z Dozzle Cloud.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: Kalıcı bağlantıyı kopyala
|
||||
see-in-context: Bağlamda gör
|
||||
show-details: Detayları göster
|
||||
create-alert: Uyarı oluştur
|
||||
label:
|
||||
containers: Konteynerlar
|
||||
container: Konteyner yok | 1 konteyner | {count} konteyner
|
||||
@@ -155,3 +156,83 @@ analytics:
|
||||
evaluating_query: Sorgu değerlendiriliyor...
|
||||
total_records: Toplam {count} kayıt.
|
||||
showing_first: İlk {count} gösteriliyor.
|
||||
notifications:
|
||||
title: Bildirimler
|
||||
description: Uyarıları nerede ve ne zaman alacağınızı yapılandırın
|
||||
destinations: Hedefler
|
||||
add-destination: Hedef ekle
|
||||
alerts: Uyarılar
|
||||
add: Ekle
|
||||
filter:
|
||||
all: Tümü ({count})
|
||||
enabled: Etkin ({count})
|
||||
paused: Duraklatılmış ({count})
|
||||
no-alerts: Henüz uyarı yapılandırılmadı. Başlamak için bir tane oluşturun.
|
||||
alert:
|
||||
paused: Duraklatıldı
|
||||
containers: Konteynerler
|
||||
log-filter: Günlük filtresi
|
||||
destination: Hedef
|
||||
dispatcher-deleted: Dağıtıcı silindi
|
||||
containers-count: "{count} konteyner"
|
||||
triggered-count: "{count} tetiklendi"
|
||||
last-triggered: "Son: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: Düzenle
|
||||
delete: Sil
|
||||
alert-form:
|
||||
create-title: Uyarı Oluştur
|
||||
edit-title: Uyarıyı Düzenle
|
||||
description: Kriterlerinize uyan günlük olaylarına abone olun
|
||||
alert-name: Uyarı Adı
|
||||
alert-name-placeholder: ör., Test API Hataları
|
||||
container-filter: Konteyner Filtresi
|
||||
containers-match: "{count} konteyner eşleşiyor: {names}"
|
||||
no-containers-match: Bu filtreyle eşleşen konteyner yok
|
||||
log-filter: Günlük Filtresi
|
||||
logs-match: "{count} günlük eşleşiyor"
|
||||
no-logs-match: Bu filtreyle eşleşen günlük yok
|
||||
destination: Hedef
|
||||
select-destination: Bir hedef seçin
|
||||
no-destinations: Yapılandırılmış hedef yok. Önce bir tane ekleyin.
|
||||
preview: Önizleme
|
||||
cancel: İptal
|
||||
save: Kaydet
|
||||
create: Uyarı Oluştur
|
||||
destination-form:
|
||||
create-title: Hedef Ekle
|
||||
edit-title: Hedefi Düzenle
|
||||
description: Bildirimler nereye gönderilmeli?
|
||||
name: Ad
|
||||
name-placeholder: ör., Prodüksiyon Slack
|
||||
type: Tür
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack, Discord, özel uç nokta
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: Push bildirimleri, e-posta uyarıları ve yapay zeka destekli özetler
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API Anahtarı
|
||||
payload-format: Payload Formatı
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: Özel
|
||||
template: Şablon
|
||||
template-hint: Go şablon söz dizimi
|
||||
test: Test
|
||||
test-success: Test başarılı
|
||||
cancel: İptal
|
||||
close: Kapat
|
||||
save: Kaydet
|
||||
add: Hedef Ekle
|
||||
cloud-exists: Yalnızca bir Dozzle Cloud hedefi yapılandırılabilir
|
||||
link-cloud: Hesabı Bağla
|
||||
link-cloud-button: Dozzle Cloud'u Bağla
|
||||
cloud-settings-hint: Yönetilen kanallarınızı yapılandırmak için şuraya gidin
|
||||
cloud-settings-link: Dozzle Cloud Ayarları
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud Bağlandı
|
||||
message: Hesabınız Dozzle Cloud'a başarıyla bağlandı.
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: 複製永久連結
|
||||
see-in-context: 在內容中查看
|
||||
show-details: 顯示詳細資訊
|
||||
create-alert: 建立警報
|
||||
label:
|
||||
containers: 容器
|
||||
container: 無容器 | 1 個容器 | {count} 個容器
|
||||
@@ -158,3 +159,83 @@ analytics:
|
||||
evaluating_query: 正在評估查詢...
|
||||
total_records: 共計 {count} 筆記錄。
|
||||
showing_first: 顯示前 {count} 筆。
|
||||
notifications:
|
||||
title: 通知
|
||||
description: 設定接收警報的位置和時間
|
||||
destinations: 目標
|
||||
add-destination: 新增目標
|
||||
alerts: 警報
|
||||
add: 新增
|
||||
filter:
|
||||
all: 全部 ({count})
|
||||
enabled: 已啟用 ({count})
|
||||
paused: 已暫停 ({count})
|
||||
no-alerts: 尚未設定任何警報。建立一個以開始使用。
|
||||
alert:
|
||||
paused: 已暫停
|
||||
containers: 容器
|
||||
log-filter: 日誌篩選器
|
||||
destination: 目標
|
||||
dispatcher-deleted: 發送器已刪除
|
||||
containers-count: "{count} 個容器"
|
||||
triggered-count: "{count} 次觸發"
|
||||
last-triggered: "最後觸發: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: 編輯
|
||||
delete: 刪除
|
||||
alert-form:
|
||||
create-title: 建立警報
|
||||
edit-title: 編輯警報
|
||||
description: 訂閱符合您條件的日誌事件
|
||||
alert-name: 警報名稱
|
||||
alert-name-placeholder: 例如,測試 API 錯誤
|
||||
container-filter: 容器篩選器
|
||||
containers-match: "{count} 個容器符合: {names}"
|
||||
no-containers-match: 沒有容器符合此篩選器
|
||||
log-filter: 日誌篩選器
|
||||
logs-match: "{count} 筆日誌符合"
|
||||
no-logs-match: 沒有日誌符合此篩選器
|
||||
destination: 目標
|
||||
select-destination: 選擇目標
|
||||
no-destinations: 尚未設定目標。請先新增一個。
|
||||
preview: 預覽
|
||||
cancel: 取消
|
||||
save: 儲存
|
||||
create: 建立警報
|
||||
destination-form:
|
||||
create-title: 新增目標
|
||||
edit-title: 編輯目標
|
||||
description: 通知應發送到哪裡?
|
||||
name: 名稱
|
||||
name-placeholder: 例如,正式環境 Slack
|
||||
type: 類型
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack、Discord、自訂端點
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: 推送通知、電子郵件警報和 AI 摘要
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API 金鑰
|
||||
payload-format: Payload 格式
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: 自訂
|
||||
template: 範本
|
||||
template-hint: Go 範本語法
|
||||
test: 測試
|
||||
test-success: 測試成功
|
||||
cancel: 取消
|
||||
close: 關閉
|
||||
save: 儲存
|
||||
add: 新增目標
|
||||
cloud-exists: 只能設定一個 Dozzle Cloud 目標
|
||||
link-cloud: 連結帳戶
|
||||
link-cloud-button: 連結 Dozzle Cloud
|
||||
cloud-settings-hint: 若要設定您的管理頻道,請前往
|
||||
cloud-settings-link: Dozzle Cloud 設定
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud 已連結
|
||||
message: 您的帳戶已成功連結至 Dozzle Cloud。
|
||||
|
||||
@@ -17,6 +17,7 @@ action:
|
||||
copy-link: 复制永久链接
|
||||
see-in-context: 在上下文中查看
|
||||
show-details: 显示详细信息
|
||||
create-alert: 创建告警
|
||||
label:
|
||||
containers: 容器
|
||||
container: 无容器 | 1 容器 | {count} 容器
|
||||
@@ -155,3 +156,83 @@ analytics:
|
||||
evaluating_query: 正在评估查询...
|
||||
total_records: 总共 {count} 条记录。
|
||||
showing_first: 显示前 {count} 条。
|
||||
notifications:
|
||||
title: 通知
|
||||
description: 配置接收警报的位置和时间
|
||||
destinations: 目标
|
||||
add-destination: 添加目标
|
||||
alerts: 警报
|
||||
add: 添加
|
||||
filter:
|
||||
all: 全部 ({count})
|
||||
enabled: 已启用 ({count})
|
||||
paused: 已暂停 ({count})
|
||||
no-alerts: 尚未配置任何警报。创建一个以开始使用。
|
||||
alert:
|
||||
paused: 已暂停
|
||||
containers: 容器
|
||||
log-filter: 日志筛选器
|
||||
destination: 目标
|
||||
dispatcher-deleted: 发送器已删除
|
||||
containers-count: "{count} 个容器"
|
||||
triggered-count: "{count} 次触发"
|
||||
last-triggered: "最后触发: {time}"
|
||||
destination:
|
||||
http-webhook: HTTP Webhook
|
||||
dozzle-cloud: Dozzle Cloud
|
||||
edit: 编辑
|
||||
delete: 删除
|
||||
alert-form:
|
||||
create-title: 创建警报
|
||||
edit-title: 编辑警报
|
||||
description: 订阅符合您条件的日志事件
|
||||
alert-name: 警报名称
|
||||
alert-name-placeholder: 例如,测试 API 错误
|
||||
container-filter: 容器筛选器
|
||||
containers-match: "{count} 个容器匹配: {names}"
|
||||
no-containers-match: 没有容器匹配此筛选器
|
||||
log-filter: 日志筛选器
|
||||
logs-match: "{count} 条日志匹配"
|
||||
no-logs-match: 没有日志匹配此筛选器
|
||||
destination: 目标
|
||||
select-destination: 选择目标
|
||||
no-destinations: 尚未配置目标。请先添加一个。
|
||||
preview: 预览
|
||||
cancel: 取消
|
||||
save: 保存
|
||||
create: 创建警报
|
||||
destination-form:
|
||||
create-title: 添加目标
|
||||
edit-title: 编辑目标
|
||||
description: 通知应发送到哪里?
|
||||
name: 名称
|
||||
name-placeholder: 例如,生产环境 Slack
|
||||
type: 类型
|
||||
webhook-title: HTTP Webhook
|
||||
webhook-description: Slack、Discord、自定义端点
|
||||
cloud-title: Dozzle Cloud
|
||||
cloud-description: 推送通知、邮件警报和 AI 摘要
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API 密钥
|
||||
payload-format: Payload 格式
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
format-ntfy: ntfy
|
||||
format-custom: 自定义
|
||||
template: 模板
|
||||
template-hint: Go 模板语法
|
||||
test: 测试
|
||||
test-success: 测试成功
|
||||
cancel: 取消
|
||||
close: 关闭
|
||||
save: 保存
|
||||
add: 添加目标
|
||||
cloud-exists: 只能配置一个 Dozzle Cloud 目标
|
||||
link-cloud: 关联账户
|
||||
link-cloud-button: 关联 Dozzle Cloud
|
||||
cloud-settings-hint: 要配置您的托管频道,请前往
|
||||
cloud-settings-link: Dozzle Cloud 设置
|
||||
cloud-link-success:
|
||||
title: Dozzle Cloud 已关联
|
||||
message: 您的账户已成功关联到 Dozzle Cloud。
|
||||
|
||||
@@ -53,15 +53,20 @@ func main() {
|
||||
|
||||
log.Info().Msgf("Dozzle version %s", args.Version())
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
var hostService web.HostService
|
||||
if args.Mode == "server" {
|
||||
|
||||
multiHostService := cli.CreateMultiHostService(certs, args)
|
||||
if multiHostService.TotalClients() == 0 {
|
||||
log.Fatal().Msg("Could not connect to any Docker Engine")
|
||||
} else {
|
||||
log.Info().Int("clients", multiHostService.TotalClients()).Msg("Connected to Docker")
|
||||
}
|
||||
if err := multiHostService.StartNotificationManager(ctx); err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not start notification manager")
|
||||
}
|
||||
hostService = multiHostService
|
||||
} else if args.Mode == "swarm" {
|
||||
localClient, err := docker.NewLocalClient("")
|
||||
@@ -74,13 +79,20 @@ func main() {
|
||||
}
|
||||
agentManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs)
|
||||
manager := docker_support.NewSwarmClientManager(localClient, certs, args.Timeout, agentManager, args.Filter)
|
||||
hostService = docker_support.NewMultiHostService(manager, args.Timeout)
|
||||
multiHostService := docker_support.NewMultiHostService(manager, args.Timeout)
|
||||
if err := multiHostService.StartNotificationManager(ctx); err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not start notification manager")
|
||||
}
|
||||
hostService = multiHostService
|
||||
log.Info().Msg("Starting in swarm mode")
|
||||
listener, err := net.Listen("tcp", ":7007")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to listen")
|
||||
}
|
||||
server, err := agent.NewServer(localClient, certs, args.Version(), args.Filter)
|
||||
// Create client service for agent server in swarm mode
|
||||
clientService := docker_support.NewDockerClientService(localClient, args.Filter)
|
||||
// TODO add notification for swarm mode
|
||||
server, err := agent.NewServer(clientService, certs, args.Version(), nil)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to create agent")
|
||||
}
|
||||
@@ -115,8 +127,6 @@ func main() {
|
||||
log.Fatal().Err(err).Msg("failed to listen")
|
||||
}
|
||||
}()
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
<-ctx.Done()
|
||||
stop()
|
||||
|
||||
+38
-30
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "9.0.1",
|
||||
"version": "10.0.0",
|
||||
"description": "Realtime log viewer for docker containers.",
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/amir20/dozzle/issues"
|
||||
},
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -29,28 +29,37 @@
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@duckdb/duckdb-wasm": "1.33.1-dev16.0",
|
||||
"@iconify-json/carbon": "^1.2.16",
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.39.13",
|
||||
"@duckdb/duckdb-wasm": "1.33.1-dev18.0",
|
||||
"@iconify-json/carbon": "^1.2.18",
|
||||
"@iconify-json/cil": "^1.2.3",
|
||||
"@iconify-json/ic": "^1.2.4",
|
||||
"@iconify-json/material-symbols": "^1.2.51",
|
||||
"@iconify-json/material-symbols": "^1.2.54",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/mdi-light": "^1.2.2",
|
||||
"@iconify-json/octicon": "^1.2.19",
|
||||
"@iconify-json/octicon": "^1.2.20",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.3",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "4.1.18",
|
||||
"@vueuse/components": "^14.1.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/integrations": "^14.1.0",
|
||||
"@vueuse/router": "14.1.0",
|
||||
"@vueuse/components": "^14.2.0",
|
||||
"@vueuse/core": "^14.2.0",
|
||||
"@vueuse/integrations": "^14.2.0",
|
||||
"@vueuse/router": "14.2.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"daisyui": "5.5.14",
|
||||
"entities": "^7.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"daisyui": "5.5.18",
|
||||
"entities": "^7.0.1",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"pinia": "^3.0.4",
|
||||
@@ -58,45 +67,44 @@
|
||||
"splitpanes": "^4.0.4",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tailwindcss": "4.1.18",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"unplugin-icons": "^22.5.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-icons": "^23.0.1",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"unplugin-vue-macros": "^2.14.5",
|
||||
"unplugin-vue-router": "^0.19.2",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitepress": "1.6.4",
|
||||
"vue": "^3.5.26",
|
||||
"vue": "^3.5.28",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4"
|
||||
"vue-router": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apache-arrow/esnext-esm": "^21.1.0",
|
||||
"@iconify-json/ion": "^1.2.6",
|
||||
"@iconify-json/material-symbols-light": "^1.2.51",
|
||||
"@iconify-json/ri": "^1.2.7",
|
||||
"@iconify-json/material-symbols-light": "^1.2.54",
|
||||
"@iconify-json/ri": "^1.2.9",
|
||||
"@pinia/testing": "^1.0.3",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^25.0.6",
|
||||
"@vitejs/plugin-vue": "6.0.3",
|
||||
"@vue/compiler-sfc": "^3.5.26",
|
||||
"@types/node": "^25.2.2",
|
||||
"@vitejs/plugin-vue": "6.0.4",
|
||||
"@vue/compiler-sfc": "^3.5.28",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"bumpp": "^10.3.2",
|
||||
"bumpp": "^10.4.1",
|
||||
"c8": "^10.1.3",
|
||||
"concurrently": "^9.2.1",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.16",
|
||||
"vue-component-type-helpers": "3.2.2",
|
||||
"vue-tsc": "3.2.2"
|
||||
"vitest": "^4.0.18",
|
||||
"vue-component-type-helpers": "3.2.4",
|
||||
"vue-tsc": "3.2.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css,ts,html,md}": [
|
||||
|
||||
Generated
+1021
-802
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user