mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 16:11:58 +00:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8fbd796b7 | |||
| e4a42125c5 | |||
| db2b7b12a3 | |||
| d6b262aead | |||
| 917489838d | |||
| cefee41857 | |||
| 8455e2743d | |||
| a5cc1f7ca0 | |||
| 1ffc338720 | |||
| 19bb865913 | |||
| bb4dca2386 | |||
| 808439365f | |||
| c1038c0a9e | |||
| 15e410a874 | |||
| 5049a53a95 | |||
| c86d108997 | |||
| 604c4b9b16 | |||
| 64b03d7b92 | |||
| a533533d4c | |||
| 2d50820932 | |||
| 3190868268 | |||
| 2c031da510 | |||
| 58ececc42a | |||
| 7be55cbc08 | |||
| 90b90ceea4 | |||
| bc842389f9 | |||
| cf9bf4dcd5 | |||
| 3eec803e45 | |||
| e332b4225b | |||
| 538d16a5b7 | |||
| 561081ebf0 | |||
| c3d2e7ac09 | |||
| 90808aff68 | |||
| 77bfb43b6d | |||
| 49881c2dfe | |||
| 3f901769cb | |||
| 26f7704edb | |||
| 3686f5c54a | |||
| aabd9dd994 | |||
| 52e0be0ac6 | |||
| bb0ffbfa04 | |||
| 4022b510e2 | |||
| 21fb03c9d0 | |||
| 392f480c76 | |||
| c9418d4b32 | |||
| 14b14e6210 | |||
| 3d5b279a1b | |||
| aca3e3a219 | |||
| 241d349d39 | |||
| b098dcaef0 | |||
| 7cf67eb99a | |||
| 869d5e5002 | |||
| 392dfafc3d | |||
| 21fe5b84f2 | |||
| 65fa04c877 | |||
| 3a352a7320 | |||
| 3bdc9c04f8 | |||
| 6e3c811974 | |||
| ddff9e96a2 | |||
| 1daf813e1b | |||
| 09a169851b | |||
| 1d5ddee1ae | |||
| b7e9871382 | |||
| 9e028b9916 | |||
| 78c0e5fd7f | |||
| 4861503488 | |||
| 115b0e475d | |||
| 6d928815df | |||
| 2a70d0b458 | |||
| 8c9c9a83af | |||
| 571de4084e | |||
| 5abf7e4a9d | |||
| d580e9447b | |||
| 877faaa740 | |||
| 1d4698d77a | |||
| 13da2a4222 | |||
| 209ce312d4 |
@@ -23,6 +23,7 @@ body:
|
||||
- Agents
|
||||
- Remote Hosts
|
||||
- Swarm Mode
|
||||
- Kubernetes
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
buy_me_a_coffee: amirraminfar
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
|
||||
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
|
||||
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
check-latest: true
|
||||
- name: Generate dependencies
|
||||
run: make fake_assets shared_key.pem shared_cert.pem
|
||||
|
||||
Vendored
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug test",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"args": ["test"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": ["locales"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"cSpell.words": ["healthcheck", "orderedmap", "Retriable", "stdcopy", "Warnf"],
|
||||
"editor.formatOnSave": true,
|
||||
"i18n-ally.extract.autoDetect": true
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:23.7.0-alpine AS node
|
||||
FROM --platform=$BUILDPLATFORM node:23.9.0-alpine AS node
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
@@ -22,7 +22,7 @@ COPY public ./public
|
||||
# Build assets
|
||||
RUN pnpm build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23.6-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
|
||||
|
||||
# install gRPC dependencies
|
||||
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
|
||||
|
||||
@@ -97,9 +97,9 @@ Once you have the podman remote socket you can run Dozzle on podman.
|
||||
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 docker.io/amir20/dozzle:latest
|
||||
```
|
||||
|
||||
Additionally you 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 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.
|
||||
|
||||
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```.
|
||||
Under `/var/lib/docker` create a file named `engine-id`. On a system with Podman you will have to create the folder path as well. Inside the file place the UUID, for instance using `uuidgen > engine-id`. After that the file should have an identifier that looks like this: `b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9`.
|
||||
|
||||
For more details 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)
|
||||
|
||||
@@ -127,12 +127,14 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
|
||||
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
|
||||
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
|
||||
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
|
||||
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | false |
|
||||
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | `false` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | `false` |
|
||||
| `--mode` | `DOZZLE_MODE` | `server` |
|
||||
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
|
||||
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
|
||||
| `--timeout` | `DOZZLE_TIMEOUT` | `10s` |
|
||||
| `--namespace` | `DOZZLE_NAMESPACE` | `""` |
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
Vendored
+2
@@ -2,6 +2,7 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
@@ -12,6 +13,7 @@ declare module 'vue' {
|
||||
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
|
||||
'Carbon:copyFile': typeof import('~icons/carbon/copy-file')['default']
|
||||
'Carbon:information': typeof import('~icons/carbon/information')['default']
|
||||
'Carbon:logoKubernetes': typeof import('~icons/carbon/logo-kubernetes')['default']
|
||||
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
|
||||
'Carbon:play': typeof import('~icons/carbon/play')['default']
|
||||
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
</ul>
|
||||
<ul class="flex flex-row flex-wrap gap-x-2 text-sm md:gap-3">
|
||||
<li class="flex items-center gap-1">
|
||||
<octicon:container-24 class="inline-block" /> {{ $t("label.container", hostContainers[host.id]?.length) }}
|
||||
<octicon:container-24 class="inline-block" />
|
||||
{{ $t("label.container", hostContainers[host.id]?.length ?? 0) }}
|
||||
</li>
|
||||
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
|
||||
</ul>
|
||||
@@ -107,7 +108,10 @@ useIntervalFn(
|
||||
stat.totalCPU += container.stat.cpu;
|
||||
stat.totalMem += container.stat.memoryUsage;
|
||||
}
|
||||
weightedStats[host].mostRecent = stat;
|
||||
if (weightedStats[host]) {
|
||||
// TODO fix this init
|
||||
weightedStats[host].mostRecent = stat;
|
||||
}
|
||||
}
|
||||
},
|
||||
1000,
|
||||
|
||||
@@ -19,8 +19,8 @@ import stripAnsi from "strip-ansi";
|
||||
|
||||
const ansiConvertor = new AnsiConvertor({
|
||||
escapeXML: false,
|
||||
fg: "oklch(var(--base-content-color))",
|
||||
bg: "oklch(var(--base-color))",
|
||||
fg: "var(--color-base-content)",
|
||||
bg: "var(--color-base-100)",
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<mdi:satellite-variant v-if="type == 'agent'" />
|
||||
<ph:globe-simple v-else-if="type == 'remote'" />
|
||||
<mdi:hexagon-multiple v-else-if="type == 'swarm'" />
|
||||
<carbon:logo-kubernetes v-else-if="type == 'k8s'" />
|
||||
<ph:computer-tower v-else />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -14,21 +14,7 @@ type Announcement = {
|
||||
|
||||
const { data: releases } = useFetch(withBase("/api/releases")).get().json<Announcement[]>();
|
||||
|
||||
const otherAnnouncements = [
|
||||
{
|
||||
body: "I'd love to hear about your experience in this short survey to shape the future of Dozzle!",
|
||||
createdAt: new Date("2025-01-22T18:00:00Z"),
|
||||
htmlUrl: "https://tally.so/r/wLv4g2?ref=notification",
|
||||
name: "Take survey!",
|
||||
announcement: true,
|
||||
tag: "survey-2025-01",
|
||||
latest: true,
|
||||
mentionsCount: 0,
|
||||
features: 0,
|
||||
bugFixes: 0,
|
||||
breaking: 0,
|
||||
},
|
||||
] as Announcement[];
|
||||
const otherAnnouncements = [] as Announcement[];
|
||||
|
||||
const announcements = computed(() => {
|
||||
const newReleases =
|
||||
|
||||
@@ -3,7 +3,7 @@ export type Host = {
|
||||
name: string;
|
||||
nCPU: number;
|
||||
memTotal: number;
|
||||
type: "agent" | "local" | "remote" | "swarm";
|
||||
type: "agent" | "local" | "remote" | "swarm" | "k8s";
|
||||
endpoint: string;
|
||||
available: boolean;
|
||||
dockerVersion: string;
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ services:
|
||||
|
||||
playwright:
|
||||
container_name: playwright
|
||||
image: mcr.microsoft.com/playwright:v1.50.1-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.51.0-jammy
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
@@ -59,6 +59,14 @@ export default defineConfig({
|
||||
{ text: "Getting Started", link: "/guide/getting-started" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Platforms",
|
||||
items: [
|
||||
{ text: "Swarm", link: "/guide/swarm-mode" },
|
||||
{ text: "K8s", link: "/guide/k8s" },
|
||||
{ text: "Podman", link: "/guide/podman" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Advanced Configuration",
|
||||
items: [
|
||||
@@ -73,8 +81,6 @@ export default defineConfig({
|
||||
{ text: "Filters", link: "/guide/filters" },
|
||||
{ text: "Healthcheck", link: "/guide/healthcheck" },
|
||||
{ text: "Remote Hosts", link: "/guide/remote-hosts" },
|
||||
{ text: "Swarm Mode", link: "/guide/swarm-mode" },
|
||||
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
|
||||
{ text: "Logging Files on Disk", link: "/guide/log-files-on-disk" },
|
||||
{ text: "SQL Engine", link: "/guide/sql-engine" },
|
||||
],
|
||||
@@ -84,6 +90,7 @@ export default defineConfig({
|
||||
items: [
|
||||
{ text: "FAQ", link: "/guide/faq" },
|
||||
{ text: "Debugging", link: "/guide/debugging" },
|
||||
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
class="banner fixed top-0 right-0 left-0 z-(--vp-z-index-layout-top) flex items-center overflow-hidden bg-[oklch(74%_0.16_232.661)] p-4 font-bold text-gray-800 dark:bg-[oklch(60%_0.126_221.723)] dark:text-white"
|
||||
>
|
||||
<div class="mx-auto flex items-center gap-2 lg:gap-4">
|
||||
<span class="animate-bounce">👋🏼</span> Using Dozzle? I'd love to hear about your experience in this short survey
|
||||
to shape the future of Dozzle.
|
||||
<a href="https://tally.so/r/wLv4g2?ref=docs" target="_blank" class="btn btn-sm btn-primary text-white!">
|
||||
Take a quick survey 🙏🏼
|
||||
<span class="animate-bounce">🚀</span>K8s users, I need your help! I have added K8s support to Dozzle and I need
|
||||
your feedback! 🙏
|
||||
<a
|
||||
href="https://github.com/amir20/dozzle/discussions/3614"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-primary text-white!"
|
||||
>
|
||||
See the discussion
|
||||
</a>
|
||||
</div>
|
||||
<button class="btn btn-circle btn-ghost btn-xs ml-auto" @click="dismiss">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,6 +8,7 @@ import HeroVideo from "./components/HeroVideo.vue";
|
||||
import BuyMeCoffee from "./components/BuyMeCoffee.vue";
|
||||
import Stats from "./components/Stats.vue";
|
||||
import Banner from "./components/Banner.vue";
|
||||
import Supported from "./components/Supported.vue";
|
||||
|
||||
export default {
|
||||
...DefaultTheme,
|
||||
@@ -16,7 +17,8 @@ export default {
|
||||
"home-hero-image": () => h(HeroVideo),
|
||||
"sidebar-nav-after": () => h(BuyMeCoffee),
|
||||
"home-hero-actions-after": () => h(Stats),
|
||||
"layout-top": () => h(Banner),
|
||||
// "layout-top": () => h(Banner),
|
||||
"home-hero-after": () => h(Supported),
|
||||
});
|
||||
},
|
||||
enhanceApp(ctx) {
|
||||
|
||||
Vendored
+2
@@ -2,6 +2,7 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
@@ -14,5 +15,6 @@ declare module 'vue' {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Stats: typeof import('./.vitepress/theme/components/Stats.vue')['default']
|
||||
Supported: typeof import('./.vitepress/theme/components/Supported.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ title: Container Actions
|
||||
|
||||
# Using Container Actions
|
||||
|
||||
Dozzle now supports **Container Actions**, which allows you to `start`, `stop` and `restart` containers from within the UI in the dropdown menu.
|
||||
|
||||
This feature is **disabled** by default and can be enabled by setting the environment variable `DOZZLE_ENABLE_ACTIONS` to `true`.
|
||||
Dozzle supports Container Actions, which allows you to `start`, `stop` and `restart` containers from within the UI in the dropdown menu. This feature is **disabled** by default and can be enabled by setting the environment variable `DOZZLE_ENABLE_ACTIONS` to `true`.
|
||||
|
||||
::: code-group
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
title: Agent Mode
|
||||
---
|
||||
|
||||
# Agent Mode
|
||||
# Agent Mode <Badge type="warning" text="Docker Only" />
|
||||
|
||||
Dozzle can run in agent mode which can expose Docker hosts to other Dozzle instances. All communication is done over a secured connection using TLS. This means that you can deploy Dozzle on a remote host and connect to it from your local machine.
|
||||
|
||||
|
||||
@@ -4,9 +4,12 @@ title: Getting Started
|
||||
|
||||
# Getting Started
|
||||
|
||||
Dozzle supports multiple ways to run the application. You can run it using Docker CLI, Docker Compose, or in Swarm. The following sections will guide you through the process of setting up Dozzle.
|
||||
Dozzle supports multiple ways to run the application. You can run it using Docker CLI, Docker Compose, Swarm, or Kubernetes. The following sections will guide you through the process of setting up Dozzle.
|
||||
|
||||
## Running with Docker <Badge type="tip" text="Updated" />
|
||||
> [!TIP]
|
||||
> If Docker Hub is blocked in your network, you can use the [GitHub Container Registry](https://ghcr.io/amir20/dozzle:latest) to pull the image. Use `ghcr.io/amir20/dozzle:latest` instead of `amir20/dozzle:latest`.
|
||||
|
||||
## Standalone Docker
|
||||
|
||||
The easiest way to set up Dozzle is to use the CLI and mount `docker.sock` file. This file is usually located at `/var/run/docker.sock` and can be mounted with the `--volume` flag. You also need to expose the port to view Dozzle. By default, Dozzle listens on port 8080, but you can change the external port using `-p`. You can also run using compose or as a service in Swarm.
|
||||
|
||||
@@ -27,6 +30,14 @@ services:
|
||||
- 8080:8080
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Dozzle also supports connecting to remote agents to monitor multiple Docker hosts. See [agent](/guide/agent) to learn more.
|
||||
|
||||
## Docker Swarm
|
||||
|
||||
Dozzle supports running in Swarm mode by deploying it on every node. To run Dozzle in Swarm mode, you can use the following configuration:
|
||||
|
||||
```yaml [dozzle-stack.yml]
|
||||
# Run with docker stack deploy -c dozzle-stack.yml <name>
|
||||
services:
|
||||
@@ -47,9 +58,82 @@ networks:
|
||||
driver: overlay
|
||||
```
|
||||
|
||||
:::
|
||||
Then you can deploy the stack using the following command:
|
||||
|
||||
See [swarm mode](/guide/swarm-mode) for more information on running Dozzle in Swarm.
|
||||
```bash
|
||||
docker stack deploy -c dozzle-stack.yml <name>
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> If Docker Hub is blocked in your network, you can use the [GitHub Container Registry](https://ghcr.io/amir20/dozzle:latest) to pull the image. Use `ghcr.io/amir20/dozzle:latest` instead of `amir20/dozzle:latest`.
|
||||
See [swarm mode](/guide/swarm-mode) for more information.
|
||||
|
||||
## K8s <Badge type="tip" text="New" />
|
||||
|
||||
Dozzle supports running in Kubernetes. It only needs to be deployed on one node within the cluster.
|
||||
|
||||
```yaml [k8s-dozzle.yml]
|
||||
# rbac.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: pod-viewer
|
||||
---
|
||||
# clusterrole.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: pod-viewer-role
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log", "nodes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["metrics.k8s.io"]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list"]
|
||||
---
|
||||
# clusterrolebinding.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: pod-viewer-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: pod-viewer
|
||||
namespace: default
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: pod-viewer-role
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
# deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dozzle
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dozzle
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dozzle
|
||||
spec:
|
||||
serviceAccountName: pod-viewer
|
||||
containers:
|
||||
- name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: DOZZLE_MODE
|
||||
value: "k8s"
|
||||
```
|
||||
|
||||
Then apply the configuration using the following command:
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s-dozzle.yml
|
||||
```
|
||||
|
||||
See [Kubernetes mode](/guide/k8s) for more information.
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: Kubernetes Support
|
||||
---
|
||||
|
||||
# Kubernetes Support <Badge type="warning" text="beta" /> <Badge type="tip" text="v8.11.x" />
|
||||
|
||||
Dozzle now supports Kubernetes, allowing you to view logs from your Kubernetes pods. This feature is available in `v8.11` version of Dozzle.
|
||||
|
||||
## Kubernetes Setup
|
||||
|
||||
To set up Dozzle in Kubernetes, you can use the following YAML configuration using `DOZZLE_MODE=k8s`. This configuration includes a deployment and a service to expose Dozzle.
|
||||
|
||||
```yaml
|
||||
# rbac.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: pod-viewer
|
||||
---
|
||||
# clusterrole.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: pod-viewer-role
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log", "nodes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["metrics.k8s.io"]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list"]
|
||||
---
|
||||
# clusterrolebinding.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: pod-viewer-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: pod-viewer
|
||||
namespace: default
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: pod-viewer-role
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
# deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dozzle
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dozzle
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dozzle
|
||||
spec:
|
||||
serviceAccountName: pod-viewer
|
||||
containers:
|
||||
- name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: DOZZLE_MODE
|
||||
value: "k8s"
|
||||
---
|
||||
# service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dozzle-service
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: dozzle
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
```
|
||||
|
||||
This configuration creates a service account, a cluster role, and a cluster role binding to allow Dozzle to access the necessary Kubernetes resources. It also creates a deployment for Dozzle and exposes it via a service.
|
||||
|
||||
All other features are supported as well, including authentication, filtering, and more. You can use the same environment variables as you would in Docker to configure Dozzle in Kubernetes.
|
||||
|
||||
> [!NOTE]
|
||||
> Dozzle in Kubernetes is a new feature and may have some limitations compared to the Docker version. Please use this [discussion](https://github.com/amir20/dozzle/discussions/3614) to report any issues or suggestions for improvement.
|
||||
|
||||
## Metrics API
|
||||
|
||||
Dozzle relies on the [Kubernetes Metrics API](https://github.com/kubernetes-sigs/metrics-server) to retrieve resource usage information. The API can be installed using the following command:
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
|
||||
```
|
||||
|
||||
To verify that the API is running, you can run the following command:
|
||||
|
||||
```bash
|
||||
kubectl top pod
|
||||
```
|
||||
|
||||
For now this is required to use Dozzle in Kubernetes.
|
||||
|
||||
## Namespaces and Filters
|
||||
|
||||
### Namespaces
|
||||
|
||||
By default, Dozzle will monitor all namespaces in the cluster. If you want to restrict Dozzle to a specific namespace, you can set the `DOZZLE_NAMESPACE` environment variable to the name of the namespace.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dozzle
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dozzle
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dozzle
|
||||
spec:
|
||||
serviceAccountName: pod-viewer
|
||||
containers:
|
||||
- name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: DOZZLE_MODE
|
||||
value: "k8s"
|
||||
- name: DOZZLE_NAMESPACE
|
||||
value: "default"
|
||||
```
|
||||
|
||||
### Labels and Filters
|
||||
|
||||
`DOZZLE_FILTER` behave similarlty to Docker filters. You can limit the scope of Dozzle using the `DOZZLE_FILTER` environment variable. For example, to scope only to `env=prod`:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dozzle
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dozzle
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dozzle
|
||||
spec:
|
||||
serviceAccountName: pod-viewer
|
||||
containers:
|
||||
- name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: DOZZLE_MODE
|
||||
value: "k8s"
|
||||
- name: DOZZLE_FILTER
|
||||
value: "env=prod"
|
||||
```
|
||||
@@ -2,6 +2,10 @@
|
||||
title: Podman
|
||||
---
|
||||
|
||||
# Podman
|
||||
|
||||
Dozzle supports Podman. However, there are some issues with Podman that might prevent Dozzle from working properly. One of the main issues is that Podman doesn't create an engine-id like Docker.
|
||||
|
||||
## I am seeing host not found error in the logs. How do I fix it?
|
||||
|
||||
This should be mainly a Podman only error: Using Podman doesn't create an engine-id like Docker.
|
||||
|
||||
@@ -9,7 +9,7 @@ Dozzle supports connecting to remote Docker hosts. This is useful when running D
|
||||
However, with Dozzle agents, you can connect to remote hosts without exposing the Docker socket. See the [agent](/guide/agent) page for more information.
|
||||
|
||||
> [!WARNING]
|
||||
> Remote hosts have been replaced with agents. Agents provide a more secure way to connect to remote hosts. Although remote hosts are still supported, it is recommended to use agents. See the [agent](/guide/agent) page for more information and examples. For comparison, see the [comparing agents with remote connections](/guide/agent#comparing-agents-with-remote-connection) section.
|
||||
> Remote hosts have been replaced with agents. Agents provide a more secure way to connect to remote hosts. Although remote hosts are still supported, it is recommended to use agents. See the [agent](/guide/agent) page for more information and examples. For comparison, see the [comparing agents with remote connections](/guide/agent#comparing-agents-with-remote-connection) section. I won't be able to investigate user's issues with remote hosts as it is very time consuming.
|
||||
|
||||
## Connecting to Remote Hosts with TLS
|
||||
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
If you are in a private network, then you can use [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy) which exposes `docker.sock` file without the need for TLS. Dozzle will never try to write to Docker but it will need access to list APIs. The following command will start a proxy with minimal access:
|
||||
|
||||
```sh
|
||||
docker container run --privileged -e CONTAINERS=1 -e INFO=1 -v /var/run/docker.sock:/var/run/docker.sock -p 2375:2375 tecnativa/docker-socket-proxy
|
||||
$ docker container run --privileged -e CONTAINERS=1 -e INFO=1 -v /var/run/docker.sock:/var/run/docker.sock -p 2375:2375 tecnativa/docker-socket-proxy
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
@@ -53,12 +53,28 @@ docker container run --privileged -e CONTAINERS=1 -e INFO=1 -v /var/run/docker.s
|
||||
|
||||
Running Dozzle without any certificates should work. Here is an example:
|
||||
|
||||
```sh
|
||||
docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --remote-host tcp://123.1.1.1:2375
|
||||
::: code-group
|
||||
|
||||
```sh [cli]
|
||||
$ docker run -p 8080:8080 amir20/dozzle --remote-host tcp://123.1.1.1:2375
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:latest
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_REMOTE_HOST: tcp://123.1.1.1:2375
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
When using remote host, mounting `/var/run/docker.sock` is optional. You need to have at least one remote host to connect to.
|
||||
|
||||
> [!WARNING]
|
||||
> Docker Socket Proxy is not recommended for production use. It is only for private networks.
|
||||
> Docker Socket Proxy exposes the Docker API to the internet. This can be a security risk if not properly secured.
|
||||
|
||||
## Adding Labels to Hosts
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ Configurations can be done with flags or environment variables. The table below
|
||||
| `--mode` | `DOZZLE_MODE` | `server` |
|
||||
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
|
||||
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
|
||||
| `--timeout` | `DOZZLE_TIMEOUT` | `3s` |
|
||||
| `--timeout` | `DOZZLE_TIMEOUT` | `10s` |
|
||||
| `--namespace` | `DOZZLE_NAMESPACE` | `""` |
|
||||
|
||||
> [!TIP]
|
||||
> Some flags like `--remote-host` or `--remote-agent` can be used multiple times. For example, `--remote-agent tcp://167.99.1.1:7007 --remote-agent tcp://167.99.1.2:7007` or comma-separated `DOZZLE_REMOTE_AGENT=tcp://167.99.1.1:7007,tcp://167.99.1.2:7007`.
|
||||
|
||||
+4
-1
@@ -6,7 +6,7 @@ title: Home
|
||||
|
||||
hero:
|
||||
name: "Dozzle"
|
||||
tagline: Real-time logging and monitoring for Docker in the browser
|
||||
tagline: Simple Container Monitoring and Logging
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
@@ -14,6 +14,9 @@ hero:
|
||||
- theme: alt
|
||||
text: View on GitHub
|
||||
link: https://github.com/amir20/dozzle
|
||||
- theme: alt
|
||||
text: Support 🙏🏼
|
||||
link: https://www.buymeacoffee.com/amirraminfar
|
||||
|
||||
features:
|
||||
- title: Self Hosted
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"dozzle": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/playfair-display": "^5.1.1"
|
||||
"@fontsource-variable/playfair-display": "^5.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+5
-5
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@fontsource-variable/playfair-display':
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1
|
||||
specifier: ^5.2.5
|
||||
version: 5.2.5
|
||||
devDependencies:
|
||||
dozzle:
|
||||
specifier: workspace:*
|
||||
@@ -18,9 +18,9 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@fontsource-variable/playfair-display@5.1.1':
|
||||
resolution: {integrity: sha512-89gLVOcVRIp0aayWwTcaLGQxhxDT2ZIv7cFU/BjxcWA8iALi8SrlT7dNHdhakb8MeXc0UUw9n8k6kG+jWvX61A==}
|
||||
'@fontsource-variable/playfair-display@5.2.5':
|
||||
resolution: {integrity: sha512-nzSCC5pbWHjkJBqjPe81cPIC7fqJ6U5DwJ6tec7vF1bQ2DJnaKRBZluRQJuseFpACZLTN8LeN/1gBQEA7PnKOw==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@fontsource-variable/playfair-display@5.1.1': {}
|
||||
'@fontsource-variable/playfair-display@5.2.5': {}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
> [!WARNING]
|
||||
> This directory contains examples that are used for testing and demonstration purposes. The configurations may vary and use different tags that are not wanted for the production environment.
|
||||
|
||||
This directory contains scripts to setup swarm, k8s, and other environments. There is also examples to demonstrate how to use Dozzle with different environments. I use these examples to test and demonstrate the functionality of Dozzle.
|
||||
|
||||
For official documentation, please visit [https://dozzle.dev](https://dozzle.dev).
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: dozzle-ingress
|
||||
annotations:
|
||||
ingress.kubernetes.io/ssl-redirect: "false"
|
||||
spec:
|
||||
rules:
|
||||
- host: dozzle.k3d.local
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: dozzle-service
|
||||
port:
|
||||
number: 8080
|
||||
@@ -0,0 +1,74 @@
|
||||
# rbac.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: pod-viewer
|
||||
---
|
||||
# clusterrole.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: pod-viewer-role
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log", "nodes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["metrics.k8s.io"]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list"]
|
||||
---
|
||||
# clusterrolebinding.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: pod-viewer-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: pod-viewer
|
||||
namespace: default
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: pod-viewer-role
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
# deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dozzle
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dozzle
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dozzle
|
||||
spec:
|
||||
serviceAccountName: pod-viewer
|
||||
containers:
|
||||
- name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: DOZZLE_MODE
|
||||
value: "k8s"
|
||||
- name: DOZZLE_LEVEL
|
||||
value: "debug"
|
||||
---
|
||||
# service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dozzle-service
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: dozzle
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
+1
-1
@@ -2,4 +2,4 @@ users:
|
||||
amir:
|
||||
email: ""
|
||||
name: Amir
|
||||
password: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
|
||||
password: $2a$11$nXizfBMJrSwfo4ofkS62denLyCKX0X7VsmfTbMyD3thTkmuGNp8.m
|
||||
|
||||
@@ -4,7 +4,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/alexflint/go-arg v1.5.1
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/docker/docker v27.5.1+incompatible
|
||||
github.com/docker/docker v28.0.1+incompatible
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
@@ -18,24 +18,28 @@ require (
|
||||
github.com/spf13/afero v1.12.0
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/PuerkitoBio/goquery v1.10.2
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-chi/jwtauth/v5 v5.3.2
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/sync v0.11.0
|
||||
google.golang.org/grpc v1.70.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/sync v0.12.0
|
||||
google.golang.org/grpc v1.71.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
k8s.io/api v0.32.2
|
||||
k8s.io/apimachinery v0.32.2
|
||||
k8s.io/client-go v0.32.2
|
||||
k8s.io/metrics v0.32.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -45,36 +49,63 @@ require (
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
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.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/kr/pretty v0.2.1 // 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/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // 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
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
go 1.23.6
|
||||
go 1.24.1
|
||||
|
||||
@@ -2,16 +2,12 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
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.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
|
||||
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
|
||||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
|
||||
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
|
||||
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-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
@@ -27,38 +23,31 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
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/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 v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.4.0+incompatible h1:I9z7sQ5qyzO0BfAb9IMOawRkAGxhYsidKiTMcm0DU+A=
|
||||
github.com/docker/docker v27.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
|
||||
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U=
|
||||
github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
|
||||
github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM=
|
||||
github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
|
||||
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
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/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.2 h1:s+ON3ATyyMs3Me0kqyuua6Rwu+2zqIIkL0GCaMarwvs=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.2/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -66,25 +55,46 @@ 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
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/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/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/go-cmp v0.3.0/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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/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.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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=
|
||||
@@ -101,14 +111,13 @@ github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
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/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=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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=
|
||||
@@ -117,8 +126,19 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
|
||||
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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
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/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
@@ -126,23 +146,16 @@ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2sz
|
||||
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.1 h1:wWXLKXwzpsduC3kUSahiL45MWxkGb+AQG0dsri4iftA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
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/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/samber/lo v1.48.0 h1:ELOfcaM7vdYPe0egBS2Nxa8LxkY4lR+9LBzj0l6cHJ0=
|
||||
github.com/samber/lo v1.48.0/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/samber/lo v1.49.0 h1:AGnTnQrg1jpFuwECPUSoxZCfVH5W22b605kWSry3YxM=
|
||||
github.com/samber/lo v1.49.0/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
@@ -151,45 +164,48 @@ 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.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
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.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/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/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.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
|
||||
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
|
||||
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/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
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/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -199,16 +215,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA=
|
||||
golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -224,15 +237,15 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
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=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
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.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
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=
|
||||
@@ -241,12 +254,11 @@ 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.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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=
|
||||
@@ -258,29 +270,24 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
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=
|
||||
@@ -289,14 +296,11 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
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.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
@@ -306,46 +310,31 @@ 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/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 v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
|
||||
google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI=
|
||||
google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
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/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
|
||||
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
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/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/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -354,3 +343,23 @@ 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.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
|
||||
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
|
||||
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
|
||||
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
|
||||
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
|
||||
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/metrics v0.32.2 h1:7t/rZzTHFrGa9f94XcgLlm3ToAuJtdlHANcJEHlYl9g=
|
||||
k8s.io/metrics v0.32.2/go.mod h1:VL3nJpzcgB6L5nSljkkzoE0nilZhVgcjCfNRgoylaIQ=
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758/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=
|
||||
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/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
||||
@@ -306,12 +306,12 @@ func (c *Client) FindContainer(ctx context.Context, containerID string) (contain
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
|
||||
func (c *Client) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
in := &pb.ListContainersRequest{}
|
||||
|
||||
if filter != nil {
|
||||
if labels != nil {
|
||||
in.Filter = make(map[string]*pb.RepeatedString)
|
||||
for k, v := range filter {
|
||||
for k, v := range labels {
|
||||
in.Filter[k] = &pb.RepeatedString{Values: v}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- contai
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
|
||||
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
|
||||
args := m.Called(ctx, filter)
|
||||
return args.Get(0).([]container.Container), args.Error(1)
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func init() {
|
||||
FinishedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}, nil)
|
||||
|
||||
server, _ := NewServer(client, certs, "test", container.ContainerFilter{})
|
||||
server, _ := NewServer(client, certs, "test", container.ContainerLabels{})
|
||||
|
||||
go server.Serve(lis)
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func TestListContainers(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
containers, _ := rpc.ListContainers(context.Background(), container.ContainerFilter{})
|
||||
containers, _ := rpc.ListContainers(context.Background(), container.ContainerLabels{})
|
||||
|
||||
assert.Equal(t, containers, []container.Container{
|
||||
{
|
||||
|
||||
+66
-81
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.36.2
|
||||
// protoc v5.29.3
|
||||
// source: rpc.proto
|
||||
|
||||
@@ -22,11 +22,10 @@ const (
|
||||
)
|
||||
|
||||
type ListContainersRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Filter map[string]*RepeatedString `protobuf:"bytes,1,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Filter map[string]*RepeatedString `protobuf:"bytes,1,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListContainersRequest) Reset() {
|
||||
@@ -67,11 +66,10 @@ func (x *ListContainersRequest) GetFilter() map[string]*RepeatedString {
|
||||
}
|
||||
|
||||
type RepeatedString struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RepeatedString) Reset() {
|
||||
@@ -112,11 +110,10 @@ func (x *RepeatedString) GetValues() []string {
|
||||
}
|
||||
|
||||
type ListContainersResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListContainersResponse) Reset() {
|
||||
@@ -157,12 +154,11 @@ func (x *ListContainersResponse) GetContainers() []*Container {
|
||||
}
|
||||
|
||||
type FindContainerRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Filter map[string]*RepeatedString `protobuf:"bytes,2,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Filter map[string]*RepeatedString `protobuf:"bytes,2,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FindContainerRequest) Reset() {
|
||||
@@ -210,11 +206,10 @@ func (x *FindContainerRequest) GetFilter() map[string]*RepeatedString {
|
||||
}
|
||||
|
||||
type FindContainerResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FindContainerResponse) Reset() {
|
||||
@@ -255,13 +250,12 @@ func (x *FindContainerResponse) GetContainer() *Container {
|
||||
}
|
||||
|
||||
type StreamLogsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
|
||||
StreamTypes int32 `protobuf:"varint,3,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
|
||||
StreamTypes int32 `protobuf:"varint,3,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamLogsRequest) Reset() {
|
||||
@@ -316,11 +310,10 @@ func (x *StreamLogsRequest) GetStreamTypes() int32 {
|
||||
}
|
||||
|
||||
type StreamLogsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Event *LogEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Event *LogEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamLogsResponse) Reset() {
|
||||
@@ -361,14 +354,13 @@ func (x *StreamLogsResponse) GetEvent() *LogEvent {
|
||||
}
|
||||
|
||||
type LogsBetweenDatesRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
|
||||
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
|
||||
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
|
||||
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
|
||||
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LogsBetweenDatesRequest) Reset() {
|
||||
@@ -430,14 +422,13 @@ func (x *LogsBetweenDatesRequest) GetStreamTypes() int32 {
|
||||
}
|
||||
|
||||
type StreamRawBytesRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
|
||||
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
|
||||
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
|
||||
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
|
||||
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamRawBytesRequest) Reset() {
|
||||
@@ -499,11 +490,10 @@ func (x *StreamRawBytesRequest) GetStreamTypes() int32 {
|
||||
}
|
||||
|
||||
type StreamRawBytesResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamRawBytesResponse) Reset() {
|
||||
@@ -544,9 +534,9 @@ func (x *StreamRawBytesResponse) GetData() []byte {
|
||||
}
|
||||
|
||||
type StreamEventsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamEventsRequest) Reset() {
|
||||
@@ -580,11 +570,10 @@ func (*StreamEventsRequest) Descriptor() ([]byte, []int) {
|
||||
}
|
||||
|
||||
type StreamEventsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Event *ContainerEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Event *ContainerEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamEventsResponse) Reset() {
|
||||
@@ -625,9 +614,9 @@ func (x *StreamEventsResponse) GetEvent() *ContainerEvent {
|
||||
}
|
||||
|
||||
type StreamStatsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamStatsRequest) Reset() {
|
||||
@@ -661,11 +650,10 @@ func (*StreamStatsRequest) Descriptor() ([]byte, []int) {
|
||||
}
|
||||
|
||||
type StreamStatsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Stat *ContainerStat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Stat *ContainerStat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamStatsResponse) Reset() {
|
||||
@@ -706,9 +694,9 @@ func (x *StreamStatsResponse) GetStat() *ContainerStat {
|
||||
}
|
||||
|
||||
type HostInfoRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HostInfoRequest) Reset() {
|
||||
@@ -742,11 +730,10 @@ func (*HostInfoRequest) Descriptor() ([]byte, []int) {
|
||||
}
|
||||
|
||||
type HostInfoResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Host *Host `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Host *Host `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HostInfoResponse) Reset() {
|
||||
@@ -787,9 +774,9 @@ func (x *HostInfoResponse) GetHost() *Host {
|
||||
}
|
||||
|
||||
type StreamContainerStartedRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamContainerStartedRequest) Reset() {
|
||||
@@ -823,11 +810,10 @@ func (*StreamContainerStartedRequest) Descriptor() ([]byte, []int) {
|
||||
}
|
||||
|
||||
type StreamContainerStartedResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamContainerStartedResponse) Reset() {
|
||||
@@ -868,12 +854,11 @@ func (x *StreamContainerStartedResponse) GetContainer() *Container {
|
||||
}
|
||||
|
||||
type ContainerActionRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Action ContainerAction `protobuf:"varint,2,opt,name=action,proto3,enum=protobuf.ContainerAction" json:"action,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Action ContainerAction `protobuf:"varint,2,opt,name=action,proto3,enum=protobuf.ContainerAction" json:"action,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ContainerActionRequest) Reset() {
|
||||
@@ -921,9 +906,9 @@ func (x *ContainerActionRequest) GetAction() ContainerAction {
|
||||
}
|
||||
|
||||
type ContainerActionResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ContainerActionResponse) Reset() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.36.2
|
||||
// protoc v5.29.3
|
||||
// source: types.proto
|
||||
|
||||
@@ -72,26 +72,25 @@ func (ContainerAction) EnumDescriptor() ([]byte, []int) {
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // deprecated
|
||||
State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"`
|
||||
ImageId string `protobuf:"bytes,6,opt,name=ImageId,proto3" json:"ImageId,omitempty"` // deprecated
|
||||
Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"`
|
||||
Started *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=started,proto3" json:"started,omitempty"`
|
||||
Health string `protobuf:"bytes,9,opt,name=health,proto3" json:"health,omitempty"`
|
||||
Host string `protobuf:"bytes,10,opt,name=host,proto3" json:"host,omitempty"`
|
||||
Tty bool `protobuf:"varint,11,opt,name=tty,proto3" json:"tty,omitempty"`
|
||||
Labels map[string]string `protobuf:"bytes,12,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
Stats []*ContainerStat `protobuf:"bytes,13,rep,name=stats,proto3" json:"stats,omitempty"`
|
||||
Group string `protobuf:"bytes,14,opt,name=group,proto3" json:"group,omitempty"`
|
||||
Command string `protobuf:"bytes,15,opt,name=command,proto3" json:"command,omitempty"`
|
||||
Finished *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=finished,proto3" json:"finished,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // deprecated
|
||||
State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"`
|
||||
ImageId string `protobuf:"bytes,6,opt,name=ImageId,proto3" json:"ImageId,omitempty"` // deprecated
|
||||
Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"`
|
||||
Started *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=started,proto3" json:"started,omitempty"`
|
||||
Health string `protobuf:"bytes,9,opt,name=health,proto3" json:"health,omitempty"`
|
||||
Host string `protobuf:"bytes,10,opt,name=host,proto3" json:"host,omitempty"`
|
||||
Tty bool `protobuf:"varint,11,opt,name=tty,proto3" json:"tty,omitempty"`
|
||||
Labels map[string]string `protobuf:"bytes,12,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Stats []*ContainerStat `protobuf:"bytes,13,rep,name=stats,proto3" json:"stats,omitempty"`
|
||||
Group string `protobuf:"bytes,14,opt,name=group,proto3" json:"group,omitempty"`
|
||||
Command string `protobuf:"bytes,15,opt,name=command,proto3" json:"command,omitempty"`
|
||||
Finished *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=finished,proto3" json:"finished,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Container) Reset() {
|
||||
@@ -237,14 +236,13 @@ func (x *Container) GetFinished() *timestamppb.Timestamp {
|
||||
}
|
||||
|
||||
type ContainerStat struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
|
||||
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
|
||||
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
|
||||
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
|
||||
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ContainerStat) Reset() {
|
||||
@@ -306,17 +304,16 @@ func (x *ContainerStat) GetMemoryPercent() float64 {
|
||||
}
|
||||
|
||||
type LogEvent struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
ContainerId string `protobuf:"bytes,2,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Message *anypb.Any `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
Level string `protobuf:"bytes,5,opt,name=level,proto3" json:"level,omitempty"`
|
||||
Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"`
|
||||
Position string `protobuf:"bytes,7,opt,name=position,proto3" json:"position,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
ContainerId string `protobuf:"bytes,2,opt,name=containerId,proto3" json:"containerId,omitempty"`
|
||||
Message *anypb.Any `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
|
||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
Level string `protobuf:"bytes,5,opt,name=level,proto3" json:"level,omitempty"`
|
||||
Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"`
|
||||
Position string `protobuf:"bytes,7,opt,name=position,proto3" json:"position,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LogEvent) Reset() {
|
||||
@@ -399,11 +396,10 @@ func (x *LogEvent) GetPosition() string {
|
||||
}
|
||||
|
||||
type SimpleMessage struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SimpleMessage) Reset() {
|
||||
@@ -444,11 +440,10 @@ func (x *SimpleMessage) GetMessage() string {
|
||||
}
|
||||
|
||||
type ComplexMessage struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ComplexMessage) Reset() {
|
||||
@@ -489,14 +484,13 @@ func (x *ComplexMessage) GetData() []byte {
|
||||
}
|
||||
|
||||
type ContainerEvent struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ActorId string `protobuf:"bytes,1,opt,name=actorId,proto3" json:"actorId,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
|
||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ActorId string `protobuf:"bytes,1,opt,name=actorId,proto3" json:"actorId,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
|
||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ContainerEvent) Reset() {
|
||||
@@ -558,22 +552,21 @@ func (x *ContainerEvent) GetTimestamp() *timestamppb.Timestamp {
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
NodeAddress string `protobuf:"bytes,3,opt,name=nodeAddress,proto3" json:"nodeAddress,omitempty"`
|
||||
Swarm bool `protobuf:"varint,4,opt,name=swarm,proto3" json:"swarm,omitempty"`
|
||||
Labels map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
OperatingSystem string `protobuf:"bytes,6,opt,name=operatingSystem,proto3" json:"operatingSystem,omitempty"`
|
||||
OsVersion string `protobuf:"bytes,7,opt,name=osVersion,proto3" json:"osVersion,omitempty"`
|
||||
OsType string `protobuf:"bytes,8,opt,name=osType,proto3" json:"osType,omitempty"`
|
||||
CpuCores uint32 `protobuf:"varint,9,opt,name=cpuCores,proto3" json:"cpuCores,omitempty"`
|
||||
Memory uint64 `protobuf:"varint,10,opt,name=memory,proto3" json:"memory,omitempty"`
|
||||
AgentVersion string `protobuf:"bytes,11,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"`
|
||||
DockerVersion string `protobuf:"bytes,12,opt,name=dockerVersion,proto3" json:"dockerVersion,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
NodeAddress string `protobuf:"bytes,3,opt,name=nodeAddress,proto3" json:"nodeAddress,omitempty"`
|
||||
Swarm bool `protobuf:"varint,4,opt,name=swarm,proto3" json:"swarm,omitempty"`
|
||||
Labels map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
OperatingSystem string `protobuf:"bytes,6,opt,name=operatingSystem,proto3" json:"operatingSystem,omitempty"`
|
||||
OsVersion string `protobuf:"bytes,7,opt,name=osVersion,proto3" json:"osVersion,omitempty"`
|
||||
OsType string `protobuf:"bytes,8,opt,name=osType,proto3" json:"osType,omitempty"`
|
||||
CpuCores uint32 `protobuf:"varint,9,opt,name=cpuCores,proto3" json:"cpuCores,omitempty"`
|
||||
Memory uint64 `protobuf:"varint,10,opt,name=memory,proto3" json:"memory,omitempty"`
|
||||
AgentVersion string `protobuf:"bytes,11,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"`
|
||||
DockerVersion string `protobuf:"bytes,12,opt,name=dockerVersion,proto3" json:"dockerVersion,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Host) Reset() {
|
||||
|
||||
+17
-13
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent/pb"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
"google.golang.org/grpc"
|
||||
@@ -32,12 +33,13 @@ type server struct {
|
||||
pb.UnimplementedAgentServiceServer
|
||||
}
|
||||
|
||||
func newServer(client container.Client, dozzleVersion string, filter container.ContainerFilter) pb.AgentServiceServer {
|
||||
func newServer(client container.Client, dozzleVersion string, labels container.ContainerLabels) pb.AgentServiceServer {
|
||||
statsCollector := docker.NewDockerStatsCollector(client, labels)
|
||||
return &server{
|
||||
client: client,
|
||||
version: dozzleVersion,
|
||||
|
||||
store: container.NewContainerStore(context.Background(), client, filter),
|
||||
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +49,7 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
since = in.Since.AsTime()
|
||||
}
|
||||
|
||||
c, err := s.store.FindContainer(in.ContainerId, container.ContainerFilter{})
|
||||
c, err := s.store.FindContainer(in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -57,7 +59,8 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
return err
|
||||
}
|
||||
|
||||
g := container.NewEventGenerator(out.Context(), reader, c)
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(out.Context(), dockerReader, c)
|
||||
|
||||
for event := range g.Events {
|
||||
out.Send(&pb.StreamLogsResponse{
|
||||
@@ -84,7 +87,8 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
|
||||
return err
|
||||
}
|
||||
|
||||
g := container.NewEventGenerator(out.Context(), reader, c)
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(out.Context(), dockerReader, c)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -173,14 +177,14 @@ func (s *server) StreamStats(in *pb.StreamStatsRequest, out pb.AgentService_Stre
|
||||
}
|
||||
|
||||
func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest) (*pb.FindContainerResponse, error) {
|
||||
filter := make(container.ContainerFilter)
|
||||
labels := make(container.ContainerLabels)
|
||||
if in.GetFilter() != nil {
|
||||
for k, v := range in.GetFilter() {
|
||||
filter[k] = append(filter[k], v.GetValues()...)
|
||||
labels[k] = append(labels[k], v.GetValues()...)
|
||||
}
|
||||
}
|
||||
|
||||
container, err := s.store.FindContainer(in.ContainerId, filter)
|
||||
container, err := s.store.FindContainer(in.ContainerId, labels)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
@@ -205,14 +209,14 @@ func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest)
|
||||
}
|
||||
|
||||
func (s *server) ListContainers(ctx context.Context, in *pb.ListContainersRequest) (*pb.ListContainersResponse, error) {
|
||||
filter := make(container.ContainerFilter)
|
||||
labels := make(container.ContainerLabels)
|
||||
if in.GetFilter() != nil {
|
||||
for k, v := range in.GetFilter() {
|
||||
filter[k] = append(filter[k], v.GetValues()...)
|
||||
labels[k] = append(labels[k], v.GetValues()...)
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := s.store.ListContainers(filter)
|
||||
containers, err := s.store.ListContainers(labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -322,7 +326,7 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
|
||||
return &pb.ContainerActionResponse{}, nil
|
||||
}
|
||||
|
||||
func NewServer(client container.Client, certificates tls.Certificate, dozzleVersion string, filter container.ContainerFilter) (*grpc.Server, error) {
|
||||
func NewServer(client container.Client, certificates tls.Certificate, dozzleVersion string, labels container.ContainerLabels) (*grpc.Server, error) {
|
||||
caCertPool := x509.NewCertPool()
|
||||
c, err := x509.ParseCertificate(certificates.Certificate[0])
|
||||
if err != nil {
|
||||
@@ -341,7 +345,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, filter))
|
||||
pb.RegisterAgentServiceServer(grpcServer, newServer(client, dozzleVersion, labels))
|
||||
|
||||
return grpcServer, nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type User struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Password string `json:"-" yaml:"password"`
|
||||
Filter string `json:"-" yaml:"filter"`
|
||||
ContainerFilter container.ContainerFilter `json:"-" yaml:"-"`
|
||||
ContainerLabels container.ContainerLabels `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func (u User) AvatarURL() string {
|
||||
@@ -35,12 +35,12 @@ func (u User) AvatarURL() string {
|
||||
return fmt.Sprintf("https://gravatar.com/avatar/%s?d=https%%3A%%2F%%2Fui-avatars.com%%2Fapi%%2F/%s/128", hashEmail(u.Email), url.QueryEscape(name))
|
||||
}
|
||||
|
||||
func newUser(username, email, name string, filter container.ContainerFilter) User {
|
||||
func newUser(username, email, name string, labels container.ContainerLabels) User {
|
||||
return User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Name: name,
|
||||
ContainerFilter: filter,
|
||||
ContainerLabels: labels,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ func UserFromContext(ctx context.Context) *User {
|
||||
}
|
||||
email := claims["email"].(string)
|
||||
name := claims["name"].(string)
|
||||
containerFilter := container.ContainerFilter{}
|
||||
containerFilter := container.ContainerLabels{}
|
||||
if filter, ok := claims["filter"].(string); ok {
|
||||
containerFilter, err = container.ParseContainerFilter(filter)
|
||||
if err != nil {
|
||||
|
||||
@@ -29,7 +29,7 @@ func (s StdType) String() string {
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
ListContainers(context.Context, ContainerFilter) ([]Container, error)
|
||||
ListContainers(context.Context, ContainerLabels) ([]Container, error)
|
||||
FindContainer(context.Context, string) (Container, error)
|
||||
ContainerLogs(context.Context, string, time.Time, StdType) (io.ReadCloser, error)
|
||||
ContainerEvents(context.Context, chan<- ContainerEvent) error
|
||||
@@ -38,5 +38,4 @@ type Client interface {
|
||||
Ping(context.Context) error
|
||||
Host() Host
|
||||
ContainerActions(ctx context.Context, action ContainerAction, containerID string) error
|
||||
IsSwarmMode() bool
|
||||
}
|
||||
|
||||
@@ -13,34 +13,40 @@ import (
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
type StatsCollector interface {
|
||||
Start(parentCtx context.Context) bool
|
||||
Subscribe(ctx context.Context, stats chan<- ContainerStat)
|
||||
Stop()
|
||||
}
|
||||
|
||||
type ContainerStore struct {
|
||||
containers *xsync.MapOf[string, *Container]
|
||||
subscribers *xsync.MapOf[context.Context, chan<- ContainerEvent]
|
||||
newContainerSubscribers *xsync.MapOf[context.Context, chan<- Container]
|
||||
client Client
|
||||
statsCollector *StatsCollector
|
||||
statsCollector StatsCollector
|
||||
wg sync.WaitGroup
|
||||
connected atomic.Bool
|
||||
events chan ContainerEvent
|
||||
ctx context.Context
|
||||
filter ContainerFilter
|
||||
labels ContainerLabels
|
||||
}
|
||||
|
||||
const defaultTimeout = 10 * time.Second
|
||||
|
||||
func NewContainerStore(ctx context.Context, client Client, filter ContainerFilter) *ContainerStore {
|
||||
log.Debug().Str("host", client.Host().Name).Interface("filter", filter).Msg("initializing container store")
|
||||
func NewContainerStore(ctx context.Context, client Client, statsCollect StatsCollector, labels ContainerLabels) *ContainerStore {
|
||||
log.Debug().Str("host", client.Host().Name).Interface("labels", labels).Msg("initializing container store")
|
||||
|
||||
s := &ContainerStore{
|
||||
containers: xsync.NewMapOf[string, *Container](),
|
||||
client: client,
|
||||
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerEvent](),
|
||||
newContainerSubscribers: xsync.NewMapOf[context.Context, chan<- Container](),
|
||||
statsCollector: NewStatsCollector(client, filter),
|
||||
statsCollector: statsCollect,
|
||||
wg: sync.WaitGroup{},
|
||||
events: make(chan ContainerEvent),
|
||||
ctx: ctx,
|
||||
filter: filter,
|
||||
labels: labels,
|
||||
}
|
||||
|
||||
s.wg.Add(1)
|
||||
@@ -68,7 +74,7 @@ func (s *ContainerStore) checkConnectivity() error {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
if containers, err := s.client.ListContainers(ctx, s.filter); err != nil {
|
||||
if containers, err := s.client.ListContainers(ctx, s.labels); err != nil {
|
||||
return err
|
||||
} else {
|
||||
s.containers.Clear()
|
||||
@@ -78,7 +84,7 @@ func (s *ContainerStore) checkConnectivity() error {
|
||||
}
|
||||
|
||||
running := lo.Filter(containers, func(item Container, index int) bool {
|
||||
return item.State != "exited"
|
||||
return item.State != "exited" && !item.FullyLoaded
|
||||
})
|
||||
|
||||
sem := semaphore.NewWeighted(maxFetchParallelism)
|
||||
@@ -109,7 +115,7 @@ func (s *ContainerStore) checkConnectivity() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ContainerStore) ListContainers(filter ContainerFilter) ([]Container, error) {
|
||||
func (s *ContainerStore) ListContainers(labels ContainerLabels) ([]Container, error) {
|
||||
s.wg.Wait()
|
||||
|
||||
if err := s.checkConnectivity(); err != nil {
|
||||
@@ -117,12 +123,16 @@ func (s *ContainerStore) ListContainers(filter ContainerFilter) ([]Container, er
|
||||
}
|
||||
|
||||
containers := make([]Container, 0)
|
||||
if filter.Exists() {
|
||||
validContainers, err := s.client.ListContainers(s.ctx, filter)
|
||||
if labels.Exists() {
|
||||
validContainers, err := s.client.ListContainers(s.ctx, labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(validContainers) == 0 {
|
||||
log.Warn().Interface("userLabels", labels).Msg("no containers found with user labels")
|
||||
}
|
||||
|
||||
validIDMap := lo.KeyBy(validContainers, func(item Container) string {
|
||||
return item.ID
|
||||
})
|
||||
@@ -143,11 +153,10 @@ func (s *ContainerStore) ListContainers(filter ContainerFilter) ([]Container, er
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Container, error) {
|
||||
func (s *ContainerStore) FindContainer(id string, labels ContainerLabels) (Container, error) {
|
||||
s.wg.Wait()
|
||||
|
||||
if filter.Exists() {
|
||||
validContainers, err := s.client.ListContainers(s.ctx, filter)
|
||||
if labels.Exists() {
|
||||
validContainers, err := s.client.ListContainers(s.ctx, labels)
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
@@ -163,31 +172,38 @@ func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Conta
|
||||
}
|
||||
|
||||
if container, ok := s.containers.Load(id); ok {
|
||||
if container.StartedAt.IsZero() {
|
||||
log.Debug().Str("id", id).Msg("container doesn't have detailed information, fetching it")
|
||||
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, bool) {
|
||||
if loaded {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
if newContainer, err := s.client.FindContainer(ctx, id); err == nil {
|
||||
return &newContainer, false
|
||||
} else {
|
||||
log.Error().Err(err).Msg("failed to fetch container")
|
||||
return c, false
|
||||
}
|
||||
}
|
||||
return c, false
|
||||
}); ok {
|
||||
event := ContainerEvent{
|
||||
Name: "update",
|
||||
Host: s.client.Host().ID,
|
||||
ActorID: id,
|
||||
}
|
||||
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
|
||||
select {
|
||||
case events <- event:
|
||||
case <-c.Done():
|
||||
s.subscribers.Delete(c)
|
||||
go func() {
|
||||
event := ContainerEvent{
|
||||
Name: "update",
|
||||
Host: newContainer.Host,
|
||||
ActorID: id,
|
||||
Container: newContainer,
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -243,13 +259,37 @@ func (s *ContainerStore) init() {
|
||||
for {
|
||||
select {
|
||||
case event := <-s.events:
|
||||
log.Trace().Str("event", event.Name).Str("id", event.ActorID).Msg("received container event")
|
||||
log.Debug().Str("event", event.Name).Str("id", event.ActorID).Msg("received container event")
|
||||
switch event.Name {
|
||||
case "start":
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
case "create":
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
|
||||
if container, err := s.client.FindContainer(ctx, event.ActorID); err == nil {
|
||||
list, _ := s.client.ListContainers(ctx, s.filter)
|
||||
list, _ := s.client.ListContainers(ctx, s.labels)
|
||||
|
||||
// make sure the container is in the list of containers when using filter
|
||||
valid := lo.ContainsBy(list, func(item Container) bool {
|
||||
return item.ID == container.ID
|
||||
})
|
||||
|
||||
if valid {
|
||||
s.containers.Store(container.ID, &container)
|
||||
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
|
||||
select {
|
||||
case containers <- container:
|
||||
case <-c.Done():
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
|
||||
case "start":
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
|
||||
if container, err := s.client.FindContainer(ctx, event.ActorID); err == nil {
|
||||
list, _ := s.client.ListContainers(ctx, s.labels)
|
||||
|
||||
// make sure the container is in the list of containers when using filter
|
||||
valid := lo.ContainsBy(list, func(item Container) bool {
|
||||
@@ -257,7 +297,6 @@ func (s *ContainerStore) init() {
|
||||
})
|
||||
|
||||
if valid {
|
||||
log.Debug().Str("id", container.ID).Msg("container started")
|
||||
s.containers.Store(container.ID, &container)
|
||||
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
|
||||
select {
|
||||
@@ -273,6 +312,42 @@ func (s *ContainerStore) init() {
|
||||
log.Debug().Str("id", event.ActorID).Msg("container destroyed")
|
||||
s.containers.Delete(event.ActorID)
|
||||
|
||||
case "update":
|
||||
started := false
|
||||
updatedContainer, _ := s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, bool) {
|
||||
if loaded {
|
||||
newContainer := event.Container
|
||||
if newContainer.State == "running" && c.State != "running" {
|
||||
started = true
|
||||
}
|
||||
c.Name = newContainer.Name
|
||||
c.State = newContainer.State
|
||||
c.Labels = newContainer.Labels
|
||||
c.StartedAt = newContainer.StartedAt
|
||||
c.FinishedAt = newContainer.FinishedAt
|
||||
c.Created = newContainer.Created
|
||||
c.Host = newContainer.Host
|
||||
return c, false
|
||||
} else {
|
||||
return c, true
|
||||
}
|
||||
})
|
||||
|
||||
if started {
|
||||
s.subscribers.Range(func(ctx context.Context, events chan<- ContainerEvent) bool {
|
||||
select {
|
||||
case events <- ContainerEvent{
|
||||
Name: "start",
|
||||
ActorID: updatedContainer.ID,
|
||||
Host: updatedContainer.Host,
|
||||
}:
|
||||
case <-ctx.Done():
|
||||
s.subscribers.Delete(ctx)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
case "die":
|
||||
s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, bool) {
|
||||
if loaded {
|
||||
|
||||
@@ -14,7 +14,7 @@ type mockedClient struct {
|
||||
Client
|
||||
}
|
||||
|
||||
func (m *mockedClient) ListContainers(ctx context.Context, filter ContainerFilter) ([]Container, error) {
|
||||
func (m *mockedClient) ListContainers(ctx context.Context, filter ContainerLabels) ([]Container, error) {
|
||||
args := m.Called(ctx, filter)
|
||||
return args.Get(0).([]Container), args.Error(1)
|
||||
}
|
||||
@@ -63,60 +63,61 @@ func TestContainerStore_List(t *testing.T) {
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300),
|
||||
}, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
store := NewContainerStore(ctx, client, ContainerFilter{})
|
||||
containers, _ := store.ListContainers(ContainerFilter{})
|
||||
collector := &fakeStatsCollector{}
|
||||
store := NewContainerStore(t.Context(), client, collector, ContainerLabels{})
|
||||
containers, _ := store.ListContainers(ContainerLabels{})
|
||||
|
||||
assert.Equal(t, containers[0].ID, "1234")
|
||||
}
|
||||
|
||||
//TODO fix this test
|
||||
// func TestContainerStore_die(t *testing.T) {
|
||||
// client := new(mockedClient)
|
||||
// client.On("ListContainers", mock.Anything, mock.Anything).Return([]Container{
|
||||
// {
|
||||
// ID: "1234",
|
||||
// Name: "test",
|
||||
// State: "running",
|
||||
// Stats: utils.NewRingBuffer[ContainerStat](300),
|
||||
// },
|
||||
// }, nil)
|
||||
func TestContainerStore_die(t *testing.T) {
|
||||
client := new(mockedClient)
|
||||
client.On("ListContainers", mock.Anything, mock.Anything).Return([]Container{
|
||||
{
|
||||
ID: "1234",
|
||||
Name: "test",
|
||||
State: "running",
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
// client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).
|
||||
// Run(func(args mock.Arguments) {
|
||||
// ctx := args.Get(0).(context.Context)
|
||||
// events := args.Get(1).(chan<- ContainerEvent)
|
||||
// events <- ContainerEvent{
|
||||
// Name: "die",
|
||||
// ActorID: "1234",
|
||||
// Host: "localhost",
|
||||
// }
|
||||
// <-ctx.Done()
|
||||
// })
|
||||
// client.On("Host").Return(Host{
|
||||
// ID: "localhost",
|
||||
// })
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
ctx := args.Get(0).(context.Context)
|
||||
events := args.Get(1).(chan<- ContainerEvent)
|
||||
events <- ContainerEvent{
|
||||
Name: "die",
|
||||
ActorID: "1234",
|
||||
Host: "localhost",
|
||||
}
|
||||
<-ctx.Done()
|
||||
})
|
||||
client.On("Host").Return(Host{
|
||||
ID: "localhost",
|
||||
})
|
||||
|
||||
// client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- container.ContainerStat")).Return(nil)
|
||||
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- container.ContainerStat")).Return(nil)
|
||||
|
||||
// client.On("FindContainer", mock.Anything, "1234").Return(Container{
|
||||
// ID: "1234",
|
||||
// Name: "test",
|
||||
// Image: "test",
|
||||
// Stats: utils.NewRingBuffer[ContainerStat](300),
|
||||
// }, nil)
|
||||
client.On("FindContainer", mock.Anything, "1234").Return(Container{
|
||||
ID: "1234",
|
||||
Name: "test",
|
||||
Image: "test",
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300),
|
||||
}, nil)
|
||||
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// t.Cleanup(cancel)
|
||||
// store := NewContainerStore(ctx, client, ContainerFilter{})
|
||||
store := NewContainerStore(t.Context(), client, &fakeStatsCollector{}, ContainerLabels{})
|
||||
|
||||
// // Wait until we get the event
|
||||
// events := make(chan ContainerEvent)
|
||||
// store.SubscribeEvents(ctx, events)
|
||||
// <-events
|
||||
// Wait until we get the event
|
||||
events := make(chan ContainerEvent)
|
||||
store.SubscribeEvents(t.Context(), events)
|
||||
<-events
|
||||
|
||||
// containers, _ := store.ListContainers(ContainerFilter{})
|
||||
// assert.Equal(t, containers[0].State, "exited")
|
||||
// }
|
||||
containers, _ := store.ListContainers(ContainerLabels{})
|
||||
assert.Equal(t, containers[0].State, "exited")
|
||||
}
|
||||
|
||||
type fakeStatsCollector struct{}
|
||||
|
||||
func (f *fakeStatsCollector) Subscribe(_ context.Context, _ chan<- ContainerStat) {}
|
||||
func (f *fakeStatsCollector) Start(_ context.Context) bool { return true }
|
||||
func (f *fakeStatsCollector) Stop() {}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -23,30 +19,26 @@ import (
|
||||
type EventGenerator struct {
|
||||
Events chan *LogEvent
|
||||
Errors chan error
|
||||
reader *bufio.Reader
|
||||
reader LogReader
|
||||
next *LogEvent
|
||||
buffer chan *LogEvent
|
||||
tty bool
|
||||
wg sync.WaitGroup
|
||||
containerID string
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
var ErrBadHeader = fmt.Errorf("dozzle/docker: unable to read header")
|
||||
|
||||
func NewEventGenerator(ctx context.Context, reader io.Reader, container Container) *EventGenerator {
|
||||
type LogReader interface {
|
||||
Read() (string, StdType, error)
|
||||
}
|
||||
|
||||
func NewEventGenerator(ctx context.Context, reader LogReader, container Container) *EventGenerator {
|
||||
generator := &EventGenerator{
|
||||
reader: bufio.NewReader(reader),
|
||||
reader: reader,
|
||||
buffer: make(chan *LogEvent, 100),
|
||||
Errors: make(chan error, 1),
|
||||
Events: make(chan *LogEvent),
|
||||
tty: container.Tty,
|
||||
containerID: container.ID,
|
||||
ctx: ctx,
|
||||
}
|
||||
@@ -90,7 +82,7 @@ loop:
|
||||
|
||||
func (g *EventGenerator) consumeReader() {
|
||||
for {
|
||||
message, streamType, readerError := readEvent(g.reader, g.tty)
|
||||
message, streamType, readerError := g.reader.Read()
|
||||
if message != "" {
|
||||
logEvent := createEvent(message, streamType)
|
||||
logEvent.ContainerID = g.containerID
|
||||
@@ -123,50 +115,6 @@ func (g *EventGenerator) peek() *LogEvent {
|
||||
}
|
||||
}
|
||||
|
||||
func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
|
||||
header := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
buffer := bufPool.Get().(*bytes.Buffer)
|
||||
buffer.Reset()
|
||||
defer bufPool.Put(buffer)
|
||||
var streamType StdType = STDOUT
|
||||
if tty {
|
||||
message, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return message, streamType, err
|
||||
}
|
||||
return message, streamType, nil
|
||||
} else {
|
||||
n, err := io.ReadFull(reader, header)
|
||||
if err != nil {
|
||||
return "", streamType, err
|
||||
}
|
||||
if n != 8 {
|
||||
log.Warn().Bytes("header", header).Msg("short read")
|
||||
message, _ := reader.ReadString('\n')
|
||||
return message, streamType, ErrBadHeader
|
||||
}
|
||||
|
||||
switch header[0] {
|
||||
case 1:
|
||||
streamType = STDOUT
|
||||
case 2:
|
||||
streamType = STDERR
|
||||
default:
|
||||
log.Warn().Bytes("header", header).Msg("unknown stream type")
|
||||
}
|
||||
|
||||
count := binary.BigEndian.Uint32(header[4:])
|
||||
if count == 0 {
|
||||
return "", streamType, nil
|
||||
}
|
||||
_, err = io.CopyN(buffer, reader, int64(count))
|
||||
if err != nil {
|
||||
return "", streamType, err
|
||||
}
|
||||
return buffer.String(), streamType, nil
|
||||
}
|
||||
}
|
||||
|
||||
func createEvent(message string, streamType StdType) *LogEvent {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(message))
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -18,9 +15,8 @@ import (
|
||||
|
||||
func TestEventGenerator_Events_tty(t *testing.T) {
|
||||
input := "example input"
|
||||
reader := bufio.NewReader(strings.NewReader(input))
|
||||
|
||||
g := NewEventGenerator(context.Background(), reader, Container{Tty: true})
|
||||
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: true})
|
||||
event := <-g.Events
|
||||
|
||||
require.NotNil(t, event, "Expected event to not be nil, but got nil")
|
||||
@@ -29,9 +25,8 @@ func TestEventGenerator_Events_tty(t *testing.T) {
|
||||
|
||||
func TestEventGenerator_Events_non_tty(t *testing.T) {
|
||||
input := "example input"
|
||||
reader := bytes.NewReader(makeMessage(input, STDOUT))
|
||||
|
||||
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
|
||||
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
|
||||
event := <-g.Events
|
||||
|
||||
require.NotNil(t, event, "Expected event to not be nil, but got nil")
|
||||
@@ -40,9 +35,8 @@ func TestEventGenerator_Events_non_tty(t *testing.T) {
|
||||
|
||||
func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) {
|
||||
input := "example input"
|
||||
reader := bytes.NewReader(makeMessage(input, STDOUT))
|
||||
|
||||
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
|
||||
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
|
||||
<-g.Events
|
||||
_, ok := <-g.Events
|
||||
|
||||
@@ -51,20 +45,31 @@ func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) {
|
||||
|
||||
func TestEventGenerator_Events_routines_done(t *testing.T) {
|
||||
input := "example input"
|
||||
reader := bytes.NewReader(makeMessage(input, STDOUT))
|
||||
|
||||
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
|
||||
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
|
||||
<-g.Events
|
||||
assert.False(t, waitTimeout(&g.wg, 1*time.Second), "Expected routines to be done")
|
||||
}
|
||||
|
||||
func makeMessage(message string, stream StdType) []byte {
|
||||
data := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(data[4:], uint32(len(message)))
|
||||
data[0] = byte(stream / 2)
|
||||
data = append(data, []byte(message)...)
|
||||
type mockLogReader struct {
|
||||
messages []string
|
||||
types []StdType
|
||||
i int
|
||||
}
|
||||
|
||||
return data
|
||||
func (m *mockLogReader) Read() (string, StdType, error) {
|
||||
if m.i >= len(m.messages) {
|
||||
return "", 0, io.EOF
|
||||
}
|
||||
m.i++
|
||||
return m.messages[m.i-1], m.types[m.i-1], nil
|
||||
}
|
||||
|
||||
func makeFakeReader(message string, stream StdType) LogReader {
|
||||
return &mockLogReader{
|
||||
messages: []string{message},
|
||||
types: []StdType{stream},
|
||||
}
|
||||
}
|
||||
|
||||
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
|
||||
@@ -156,23 +161,3 @@ func Test_createEvent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockReadCloser struct {
|
||||
bytes []byte
|
||||
}
|
||||
|
||||
func (m mockReadCloser) Read(p []byte) (int, error) {
|
||||
return copy(p, m.bytes), nil
|
||||
}
|
||||
|
||||
func Benchmark_readEvent(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
data := makeMessage("2020-05-13T18:55:37.772853839Z {\"key\": \"value\"}\n", STDOUT)
|
||||
|
||||
reader := bufio.NewReader(mockReadCloser{bytes: data})
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
readEvent(reader, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,23 +20,18 @@ var logLevels = [][]string{
|
||||
{"fatal", "sev", "severe", "crit", "critical"},
|
||||
}
|
||||
|
||||
var plainLevels = map[string][]*regexp.Regexp{}
|
||||
var bracketLevels = map[string][]*regexp.Regexp{}
|
||||
var plainLevels = map[string]*regexp.Regexp{}
|
||||
var bracketLevels = map[string]*regexp.Regexp{}
|
||||
var separatorLevels = map[string]*regexp.Regexp{}
|
||||
var timestampRegex = regexp.MustCompile(`^(?:\d{4}[-/]\d{2}[-/]\d{2}(?:[T ](?:\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?|\d{2}:\d{2}(?:AM|PM)))?\s+)`)
|
||||
|
||||
func init() {
|
||||
for _, levelGroup := range logLevels {
|
||||
first := levelGroup[0]
|
||||
for _, level := range levelGroup {
|
||||
plainLevels[first] = append(plainLevels[first], regexp.MustCompile("(?i)^"+level+"[^a-z]"))
|
||||
}
|
||||
}
|
||||
|
||||
for _, levelGroup := range logLevels {
|
||||
first := levelGroup[0]
|
||||
for _, level := range levelGroup {
|
||||
bracketLevels[first] = append(bracketLevels[first], regexp.MustCompile("(?i)\\[ ?"+level+" ?\\]"))
|
||||
}
|
||||
levelsGroup := "(?:" + strings.Join(levelGroup, "|") + ")"
|
||||
plainLevels[first] = regexp.MustCompile("(?i)^" + levelsGroup + "[^a-z]")
|
||||
bracketLevels[first] = regexp.MustCompile("(?i)\\[ ?" + levelsGroup + " ?\\]")
|
||||
separatorLevels[first] = regexp.MustCompile("(?i) " + levelsGroup + "[/-]")
|
||||
}
|
||||
|
||||
SupportedLogLevels = make(map[string]struct{}, len(logLevels)+1)
|
||||
@@ -54,17 +49,18 @@ func guessLogLevel(logEvent *LogEvent) string {
|
||||
for _, levelGroup := range logLevels {
|
||||
first := levelGroup[0]
|
||||
// Look for the level at the beginning of the message
|
||||
for _, regex := range plainLevels[first] {
|
||||
if regex.MatchString(value) {
|
||||
return first
|
||||
}
|
||||
if plainLevels[first].MatchString(value) {
|
||||
return first
|
||||
}
|
||||
|
||||
// Look for the level in brackets
|
||||
for _, regex := range bracketLevels[first] {
|
||||
if regex.MatchString(value) {
|
||||
return first
|
||||
}
|
||||
if bracketLevels[first].MatchString(value) {
|
||||
return first
|
||||
}
|
||||
|
||||
// Look for the level with a separator after
|
||||
if separatorLevels[first].MatchString(value) {
|
||||
return first
|
||||
}
|
||||
|
||||
// Look for the level in the middle of the message that are uppercase and surrounded by quotes
|
||||
|
||||
@@ -14,6 +14,8 @@ func TestGuessLogLevel(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{"2024/12/30 12:21AM INF this is a test", "info"},
|
||||
{"2025-01-07 22:00:08,059: DEBUG/MainProcess TaskPool: ", "debug"},
|
||||
{"Some test with error-test", "error"},
|
||||
{"2024-12-30T17:43:16Z DBG loggging debug from here", "debug"},
|
||||
{"2025-01-07 15:40:15,784 LL=\"ERROR\" some message", "error"},
|
||||
{"2025-01-07 15:40:15,784 LL=\"WARN\" some message", "warn"},
|
||||
@@ -35,6 +37,7 @@ func TestGuessLogLevel(t *testing.T) {
|
||||
{"123 ERROR Something went wrong", "error"},
|
||||
{"123 Something went wrong", "unknown"},
|
||||
{"DBG Something went wrong", "debug"},
|
||||
{"DBG with more error=msg", "debug"},
|
||||
{"inf Something went wrong", "info"},
|
||||
{"crit: Something went wrong", "fatal"},
|
||||
{orderedmap.New[string, string](
|
||||
|
||||
+20
-18
@@ -11,20 +11,21 @@ import (
|
||||
|
||||
// Container represents an internal representation of docker containers
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Command string `json:"command"`
|
||||
Created time.Time `json:"created"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt time.Time `json:"finishedAt"`
|
||||
State string `json:"state"`
|
||||
Health string `json:"health,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Tty bool `json:"-"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Command string `json:"command"`
|
||||
Created time.Time `json:"created"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt time.Time `json:"finishedAt"`
|
||||
State string `json:"state"`
|
||||
Health string `json:"health,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Tty bool `json:"-"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
FullyLoaded bool `json:"-,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerStat represent stats instant for a container
|
||||
@@ -42,12 +43,13 @@ type ContainerEvent struct {
|
||||
ActorID string `json:"actorId"`
|
||||
ActorAttributes map[string]string `json:"actorAttributes,omitempty"`
|
||||
Time time.Time `json:"time"`
|
||||
Container *Container `json:"-"`
|
||||
}
|
||||
|
||||
type ContainerFilter map[string][]string
|
||||
type ContainerLabels map[string][]string
|
||||
|
||||
func ParseContainerFilter(commaValues string) (ContainerFilter, error) {
|
||||
filter := make(ContainerFilter)
|
||||
func ParseContainerFilter(commaValues string) (ContainerLabels, error) {
|
||||
filter := make(ContainerLabels)
|
||||
if commaValues == "" {
|
||||
return filter, nil
|
||||
}
|
||||
@@ -65,7 +67,7 @@ func ParseContainerFilter(commaValues string) (ContainerFilter, error) {
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func (f ContainerFilter) Exists() bool {
|
||||
func (f ContainerLabels) Exists() bool {
|
||||
return len(f) > 0
|
||||
}
|
||||
|
||||
|
||||
+32
-36
@@ -17,7 +17,6 @@ import (
|
||||
docker "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
@@ -25,10 +24,10 @@ import (
|
||||
)
|
||||
|
||||
type DockerCLI interface {
|
||||
ContainerList(context.Context, docker.ListOptions) ([]types.Container, error)
|
||||
ContainerList(context.Context, docker.ListOptions) ([]docker.Summary, error)
|
||||
ContainerLogs(context.Context, string, docker.LogsOptions) (io.ReadCloser, error)
|
||||
Events(context.Context, events.ListOptions) (<-chan events.Message, <-chan error)
|
||||
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
|
||||
ContainerInspect(ctx context.Context, containerID string) (docker.InspectResponse, error)
|
||||
ContainerStats(ctx context.Context, containerID string, stream bool) (docker.StatsResponseReader, error)
|
||||
Ping(ctx context.Context) (types.Ping, error)
|
||||
ContainerStart(ctx context.Context, containerID string, options docker.StartOptions) error
|
||||
@@ -37,13 +36,13 @@ type DockerCLI interface {
|
||||
Info(ctx context.Context) (system.Info, error)
|
||||
}
|
||||
|
||||
type httpClient struct {
|
||||
type DockerClient struct {
|
||||
cli DockerCLI
|
||||
host container.Host
|
||||
info system.Info
|
||||
}
|
||||
|
||||
func NewClient(cli DockerCLI, host container.Host) container.Client {
|
||||
func NewClient(cli DockerCLI, host container.Host) *DockerClient {
|
||||
info, err := cli.Info(context.Background())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get docker info")
|
||||
@@ -60,15 +59,15 @@ func NewClient(cli DockerCLI, host container.Host) container.Client {
|
||||
host.DockerVersion = info.ServerVersion
|
||||
host.Swarm = info.Swarm.NodeID != ""
|
||||
|
||||
return &httpClient{
|
||||
return &DockerClient{
|
||||
cli: cli,
|
||||
host: host,
|
||||
info: info,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientWithFilters creates a new instance of Client with docker filters
|
||||
func NewLocalClient(hostname string) (container.Client, error) {
|
||||
// NewLocalClient creates a new instance of Client with docker filters
|
||||
func NewLocalClient(hostname string) (*DockerClient, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation(), client.WithUserAgent("Docker-Client/Dozzle"))
|
||||
|
||||
if err != nil {
|
||||
@@ -93,7 +92,7 @@ func NewLocalClient(hostname string) (container.Client, error) {
|
||||
return NewClient(cli, host), nil
|
||||
}
|
||||
|
||||
func NewRemoteClient(host container.Host) (container.Client, error) {
|
||||
func NewRemoteClient(host container.Host) (*DockerClient, error) {
|
||||
if host.URL.Scheme != "tcp" {
|
||||
return nil, fmt.Errorf("invalid scheme: %s", host.URL.Scheme)
|
||||
}
|
||||
@@ -123,7 +122,7 @@ func NewRemoteClient(host container.Host) (container.Client, error) {
|
||||
}
|
||||
|
||||
// Finds a container by id, skipping the filters
|
||||
func (d *httpClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
|
||||
func (d *DockerClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
|
||||
log.Debug().Str("id", id).Msg("Finding container")
|
||||
if json, err := d.cli.ContainerInspect(ctx, id); err == nil {
|
||||
return newContainerFromJSON(json, d.host.ID), nil
|
||||
@@ -133,7 +132,7 @@ func (d *httpClient) FindContainer(ctx context.Context, id string) (container.Co
|
||||
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
|
||||
func (d *DockerClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
|
||||
switch action {
|
||||
case container.Start:
|
||||
return d.cli.ContainerStart(ctx, containerID, docker.StartOptions{})
|
||||
@@ -146,10 +145,10 @@ func (d *httpClient) ContainerActions(ctx context.Context, action container.Cont
|
||||
}
|
||||
}
|
||||
|
||||
func (d *httpClient) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
|
||||
log.Debug().Interface("filter", filter).Str("host", d.host.Name).Msg("Listing containers")
|
||||
func (d *DockerClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
log.Debug().Interface("labels", labels).Str("host", d.host.Name).Msg("Listing containers")
|
||||
filterArgs := filters.NewArgs()
|
||||
for key, values := range filter {
|
||||
for key, values := range labels {
|
||||
for _, value := range values {
|
||||
filterArgs.Add(key, value)
|
||||
}
|
||||
@@ -175,7 +174,7 @@ func (d *httpClient) ListContainers(ctx context.Context, filter container.Contai
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
|
||||
func (d *DockerClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
|
||||
response, err := d.cli.ContainerStats(ctx, id, true)
|
||||
|
||||
if err != nil {
|
||||
@@ -225,7 +224,7 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
|
||||
}
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
func (d *DockerClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
log.Debug().Str("id", id).Time("since", since).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Streaming logs for container")
|
||||
|
||||
sinceQuery := since.Add(-50 * time.Millisecond).Format(time.RFC3339Nano)
|
||||
@@ -246,7 +245,7 @@ func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Ti
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- container.ContainerEvent) error {
|
||||
func (d *DockerClient) ContainerEvents(ctx context.Context, messages chan<- container.ContainerEvent) error {
|
||||
dockerMessages, err := d.cli.Events(ctx, events.ListOptions{})
|
||||
|
||||
for {
|
||||
@@ -270,7 +269,7 @@ func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- contai
|
||||
}
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
func (d *DockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
log.Debug().Str("id", id).Time("from", from).Time("to", to).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Fetching logs between dates for container")
|
||||
options := docker.LogsOptions{
|
||||
ShowStdout: stdType&container.STDOUT != 0,
|
||||
@@ -288,21 +287,17 @@ func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, f
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (d *httpClient) Ping(ctx context.Context) error {
|
||||
func (d *DockerClient) Ping(ctx context.Context) error {
|
||||
_, err := d.cli.Ping(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *httpClient) Host() container.Host {
|
||||
func (d *DockerClient) Host() container.Host {
|
||||
log.Debug().Str("host", d.host.Name).Msg("Fetching host")
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *httpClient) IsSwarmMode() bool {
|
||||
return d.info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive
|
||||
}
|
||||
|
||||
func newContainer(c types.Container, host string) container.Container {
|
||||
func newContainer(c docker.Summary, host string) container.Container {
|
||||
name := "no name"
|
||||
if c.Labels["dev.dozzle.name"] != "" {
|
||||
name = c.Labels["dev.dozzle.name"]
|
||||
@@ -328,7 +323,7 @@ func newContainer(c types.Container, host string) container.Container {
|
||||
}
|
||||
}
|
||||
|
||||
func newContainerFromJSON(c types.ContainerJSON, host string) container.Container {
|
||||
func newContainerFromJSON(c docker.InspectResponse, host string) container.Container {
|
||||
name := "no name"
|
||||
if c.Config.Labels["dev.dozzle.name"] != "" {
|
||||
name = c.Config.Labels["dev.dozzle.name"]
|
||||
@@ -342,16 +337,17 @@ func newContainerFromJSON(c types.ContainerJSON, host string) container.Containe
|
||||
}
|
||||
|
||||
container := container.Container{
|
||||
ID: c.ID[:12],
|
||||
Name: name,
|
||||
Image: c.Config.Image,
|
||||
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
|
||||
State: c.State.Status,
|
||||
Host: host,
|
||||
Labels: c.Config.Labels,
|
||||
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
|
||||
Group: group,
|
||||
Tty: c.Config.Tty,
|
||||
ID: c.ID[:12],
|
||||
Name: name,
|
||||
Image: c.Config.Image,
|
||||
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
|
||||
State: c.State.Status,
|
||||
Host: host,
|
||||
Labels: c.Config.Labels,
|
||||
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
|
||||
Group: group,
|
||||
Tty: c.Config.Tty,
|
||||
FullyLoaded: true,
|
||||
}
|
||||
|
||||
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
docker "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -24,14 +23,13 @@ type mockedProxy struct {
|
||||
DockerCLI
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerList(context.Context, docker.ListOptions) ([]types.Container, error) {
|
||||
func (m *mockedProxy) ContainerList(context.Context, docker.ListOptions) ([]docker.Summary, error) {
|
||||
args := m.Called()
|
||||
containers, ok := args.Get(0).([]types.Container)
|
||||
containers, ok := args.Get(0).([]docker.Summary)
|
||||
if !ok && args.Get(0) != nil {
|
||||
panic("containers is not of type []types.Container")
|
||||
panic("containers is not of type []docker.Summary")
|
||||
}
|
||||
return containers, args.Error(1)
|
||||
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options docker.LogsOptions) (io.ReadCloser, error) {
|
||||
@@ -43,9 +41,9 @@ func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options dock
|
||||
return reader, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string) (docker.InspectResponse, error) {
|
||||
args := m.Called(ctx, containerID)
|
||||
return args.Get(0).(types.ContainerJSON), args.Error(1)
|
||||
return args.Get(0).(docker.InspectResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, stream bool) (docker.StatsResponseReader, error) {
|
||||
@@ -90,9 +88,9 @@ func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string,
|
||||
func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers(context.Background(), container.ContainerFilter{})
|
||||
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
|
||||
assert.Empty(t, list, "list should be empty")
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
|
||||
@@ -102,9 +100,9 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
func Test_dockerClient_ListContainers_error(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers(context.Background(), container.ContainerFilter{})
|
||||
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
|
||||
assert.Nil(t, list, "list should be nil")
|
||||
require.Error(t, err, "test.")
|
||||
|
||||
@@ -112,7 +110,7 @@ func Test_dockerClient_ListContainers_error(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
containers := []docker.Summary{
|
||||
{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Names: []string{"/z_test_container"},
|
||||
@@ -125,9 +123,9 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers(context.Background(), container.ContainerFilter{})
|
||||
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
|
||||
Ids := []string{"1234567890_a", "abcdefghijkl"}
|
||||
@@ -159,7 +157,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
||||
Since: "2020-12-31T23:59:59.95Z"}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, since, container.STDALL)
|
||||
|
||||
actual, _ := io.ReadAll(logReader)
|
||||
@@ -173,7 +171,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
|
||||
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
|
||||
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
reader, err := client.ContainerLogs(context.Background(), id, time.Time{}, container.STDALL)
|
||||
|
||||
@@ -185,11 +183,11 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
|
||||
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &docker.Config{Tty: false}}
|
||||
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||
json := docker.InspectResponse{ContainerJSONBase: &docker.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &docker.Config{Tty: false}}
|
||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
container, err := client.FindContainer(context.Background(), "abcdefghijkl")
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
@@ -201,8 +199,8 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
|
||||
func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(types.ContainerJSON{}, errors.New("not found"))
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(docker.InspectResponse{}, errors.New("not found"))
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
_, err := client.FindContainer(context.Background(), "not_valid")
|
||||
require.Error(t, err, "error should be thrown")
|
||||
@@ -212,10 +210,10 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
|
||||
func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &docker.Config{Tty: false}}
|
||||
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||
json := docker.InspectResponse{ContainerJSONBase: &docker.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &docker.Config{Tty: false}}
|
||||
|
||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||
proxy.On("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
|
||||
@@ -240,8 +238,8 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
func Test_dockerClient_ContainerActions_error(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
client := &httpClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
proxy.On("ContainerInspect", mock.Anything, "random-id").Return(types.ContainerJSON{}, errors.New("not found"))
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
proxy.On("ContainerInspect", mock.Anything, "random-id").Return(docker.InspectResponse{}, errors.New("not found"))
|
||||
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||
proxy.On("ContainerStop", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||
proxy.On("ContainerRestart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
)
|
||||
|
||||
var ErrBadHeader = errors.New("bad header")
|
||||
|
||||
type StdType int
|
||||
|
||||
const (
|
||||
stdout StdType = iota
|
||||
stderr
|
||||
)
|
||||
|
||||
type LogReader struct {
|
||||
reader *bufio.Reader
|
||||
tty bool
|
||||
pool *sync.Pool
|
||||
}
|
||||
|
||||
func NewLogReader(r io.Reader, tty bool) *LogReader {
|
||||
return &LogReader{
|
||||
reader: bufio.NewReader(r),
|
||||
tty: tty,
|
||||
pool: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *LogReader) Read() (string, container.StdType, error) {
|
||||
message, stdType, err := d.readEvent()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
var std container.StdType
|
||||
switch stdType {
|
||||
case stdout:
|
||||
std = container.STDOUT
|
||||
case stderr:
|
||||
std = container.STDERR
|
||||
}
|
||||
|
||||
return message, std, nil
|
||||
|
||||
}
|
||||
|
||||
func (d *LogReader) readEvent() (string, StdType, error) {
|
||||
header := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
buffer := d.pool.Get().(*bytes.Buffer)
|
||||
buffer.Reset()
|
||||
defer d.pool.Put(buffer)
|
||||
|
||||
var streamType StdType = stdout
|
||||
|
||||
if d.tty {
|
||||
message, err := d.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return message, streamType, err
|
||||
}
|
||||
return message, streamType, nil
|
||||
} else {
|
||||
n, err := io.ReadFull(d.reader, header)
|
||||
if err != nil {
|
||||
return "", streamType, err
|
||||
}
|
||||
if n != 8 {
|
||||
message, _ := d.reader.ReadString('\n')
|
||||
return message, streamType, ErrBadHeader
|
||||
}
|
||||
|
||||
switch header[0] {
|
||||
case 1:
|
||||
streamType = stdout
|
||||
case 2:
|
||||
streamType = stderr
|
||||
}
|
||||
|
||||
count := binary.BigEndian.Uint32(header[4:])
|
||||
if count == 0 {
|
||||
return "", streamType, nil
|
||||
}
|
||||
|
||||
_, err = io.CopyN(buffer, d.reader, int64(count))
|
||||
if err != nil {
|
||||
return "", streamType, err
|
||||
}
|
||||
return buffer.String(), streamType, nil
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package container
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,35 +8,36 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type StatsCollector struct {
|
||||
stream chan ContainerStat
|
||||
subscribers *xsync.MapOf[context.Context, chan<- ContainerStat]
|
||||
client Client
|
||||
type DockerStatsCollector struct {
|
||||
stream chan container.ContainerStat
|
||||
subscribers *xsync.MapOf[context.Context, chan<- container.ContainerStat]
|
||||
client container.Client
|
||||
cancelers *xsync.MapOf[string, context.CancelFunc]
|
||||
stopper context.CancelFunc
|
||||
timer *time.Timer
|
||||
mu sync.Mutex
|
||||
totalStarted atomic.Int32
|
||||
filter ContainerFilter
|
||||
labels container.ContainerLabels
|
||||
}
|
||||
|
||||
var timeToStop = 6 * time.Hour
|
||||
|
||||
func NewStatsCollector(client Client, filter ContainerFilter) *StatsCollector {
|
||||
return &StatsCollector{
|
||||
stream: make(chan ContainerStat),
|
||||
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerStat](),
|
||||
func NewDockerStatsCollector(client container.Client, labels container.ContainerLabels) *DockerStatsCollector {
|
||||
return &DockerStatsCollector{
|
||||
stream: make(chan container.ContainerStat),
|
||||
subscribers: xsync.NewMapOf[context.Context, chan<- container.ContainerStat](),
|
||||
client: client,
|
||||
cancelers: xsync.NewMapOf[string, context.CancelFunc](),
|
||||
filter: filter,
|
||||
labels: labels,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StatsCollector) Subscribe(ctx context.Context, stats chan<- ContainerStat) {
|
||||
func (c *DockerStatsCollector) Subscribe(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
c.subscribers.Store(ctx, stats)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
@@ -44,7 +45,7 @@ func (c *StatsCollector) Subscribe(ctx context.Context, stats chan<- ContainerSt
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *StatsCollector) forceStop() {
|
||||
func (c *DockerStatsCollector) forceStop() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.stopper != nil {
|
||||
@@ -54,7 +55,7 @@ func (c *StatsCollector) forceStop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StatsCollector) Stop() {
|
||||
func (c *DockerStatsCollector) Stop() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.totalStarted.Add(-1) == 0 {
|
||||
@@ -64,7 +65,7 @@ func (c *StatsCollector) Stop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StatsCollector) reset() {
|
||||
func (c *DockerStatsCollector) reset() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.timer != nil {
|
||||
@@ -73,7 +74,7 @@ func (c *StatsCollector) reset() {
|
||||
c.timer = nil
|
||||
}
|
||||
|
||||
func streamStats(parent context.Context, sc *StatsCollector, id string) {
|
||||
func streamStats(parent context.Context, sc *DockerStatsCollector, id string) {
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
sc.cancelers.Store(id, cancel)
|
||||
log.Debug().Str("container", id).Str("host", sc.client.Host().Name).Msg("starting to stream stats")
|
||||
@@ -86,7 +87,7 @@ func streamStats(parent context.Context, sc *StatsCollector, id string) {
|
||||
}
|
||||
|
||||
// Start starts the stats collector and blocks until it's stopped. It returns true if the collector was stopped, false if it was already running
|
||||
func (sc *StatsCollector) Start(parentCtx context.Context) bool {
|
||||
func (sc *DockerStatsCollector) Start(parentCtx context.Context) bool {
|
||||
sc.reset()
|
||||
sc.totalStarted.Add(1)
|
||||
|
||||
@@ -99,8 +100,8 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
|
||||
ctx, sc.stopper = context.WithCancel(parentCtx)
|
||||
sc.mu.Unlock()
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(parentCtx, defaultTimeout)
|
||||
if containers, err := sc.client.ListContainers(timeoutCtx, sc.filter); err == nil {
|
||||
timeoutCtx, cancel := context.WithTimeout(parentCtx, 3*time.Second) // 3 seconds to list containers is hard limit
|
||||
if containers, err := sc.client.ListContainers(timeoutCtx, sc.labels); err == nil {
|
||||
for _, c := range containers {
|
||||
if c.State == "running" {
|
||||
go streamStats(ctx, sc, c.ID)
|
||||
@@ -111,7 +112,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
|
||||
}
|
||||
cancel()
|
||||
|
||||
events := make(chan ContainerEvent)
|
||||
events := make(chan container.ContainerEvent)
|
||||
|
||||
go func() {
|
||||
log.Debug().Str("host", sc.client.Host().Name).Msg("starting to listen to docker events")
|
||||
@@ -142,7 +143,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
|
||||
log.Info().Str("host", sc.client.Host().Name).Msg("stopped container stats collector")
|
||||
return true
|
||||
case stat := <-sc.stream:
|
||||
sc.subscribers.Range(func(c context.Context, stats chan<- ContainerStat) bool {
|
||||
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
|
||||
select {
|
||||
case stats <- stat:
|
||||
case <-c.Done():
|
||||
+39
-8
@@ -1,16 +1,47 @@
|
||||
package container
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func startedCollector(ctx context.Context) *StatsCollector {
|
||||
type mockedClient struct {
|
||||
mock.Mock
|
||||
container.Client
|
||||
}
|
||||
|
||||
func (m *mockedClient) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
|
||||
args := m.Called(ctx, filter)
|
||||
return args.Get(0).([]container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedClient) ContainerEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
|
||||
args := m.Called(ctx, events)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockedClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
|
||||
args := m.Called(ctx, id, stats)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockedClient) Host() container.Host {
|
||||
args := m.Called()
|
||||
return args.Get(0).(container.Host)
|
||||
}
|
||||
|
||||
func startedCollector(ctx context.Context) *DockerStatsCollector {
|
||||
client := new(mockedClient)
|
||||
client.On("ListContainers", mock.Anything, mock.Anything).Return([]Container{
|
||||
client.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
|
||||
{
|
||||
ID: "1234",
|
||||
Name: "test",
|
||||
@@ -26,17 +57,17 @@ func startedCollector(ctx context.Context) *StatsCollector {
|
||||
client.On("ContainerStats", mock.Anything, mock.Anything, mock.AnythingOfType("chan<- container.ContainerStat")).
|
||||
Return(nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
stats := args.Get(2).(chan<- ContainerStat)
|
||||
stats <- ContainerStat{
|
||||
stats := args.Get(2).(chan<- container.ContainerStat)
|
||||
stats <- container.ContainerStat{
|
||||
ID: "1234",
|
||||
}
|
||||
})
|
||||
client.On("Host").Return(Host{
|
||||
client.On("Host").Return(container.Host{
|
||||
ID: "localhost",
|
||||
})
|
||||
|
||||
collector := NewStatsCollector(client, ContainerFilter{})
|
||||
stats := make(chan ContainerStat)
|
||||
collector := NewDockerStatsCollector(client, container.ContainerLabels{})
|
||||
stats := make(chan container.ContainerStat)
|
||||
|
||||
collector.Subscribe(ctx, stats)
|
||||
|
||||
@@ -14,7 +14,7 @@ func RPCRequest(ctx context.Context, addr string, certs tls.Certificate) error {
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to create agent client")
|
||||
}
|
||||
containers, err := client.ListContainers(ctx, container.ContainerFilter{})
|
||||
containers, err := client.ListContainers(ctx, container.ContainerLabels{})
|
||||
log.Trace().Int("containers", len(containers)).Msg("Healtcheck RPC request completed")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
type K8sClient struct {
|
||||
Clientset *kubernetes.Clientset
|
||||
namespace string
|
||||
config *rest.Config
|
||||
host container.Host
|
||||
}
|
||||
|
||||
func NewK8sClient(namespace string) (*K8sClient, error) {
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
// Check if we're running in cluster
|
||||
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
|
||||
config, err = rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Info().Msg("Running in-cluster mode")
|
||||
} else {
|
||||
kubeconfig := os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
kubeconfig = os.Getenv("HOME") + "/.kube/config"
|
||||
}
|
||||
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Info().Msgf("Running in local mode with kubeconfig: %s", kubeconfig)
|
||||
|
||||
}
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes, err := clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(nodes.Items) == 0 {
|
||||
return nil, fmt.Errorf("nodes not found")
|
||||
}
|
||||
node := nodes.Items[0]
|
||||
|
||||
return &K8sClient{
|
||||
Clientset: clientset,
|
||||
namespace: namespace,
|
||||
config: config,
|
||||
host: container.Host{
|
||||
ID: node.Status.NodeInfo.MachineID,
|
||||
Name: node.Name,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func podToContainers(pod *corev1.Pod) []container.Container {
|
||||
started := time.Time{}
|
||||
if pod.Status.StartTime != nil {
|
||||
started = pod.Status.StartTime.Time
|
||||
}
|
||||
var containers []container.Container
|
||||
for _, c := range pod.Spec.Containers {
|
||||
containers = append(containers, container.Container{
|
||||
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
|
||||
Name: pod.Name + "/" + c.Name,
|
||||
Image: c.Image,
|
||||
Created: pod.CreationTimestamp.Time,
|
||||
State: phaseToState(pod.Status.Phase),
|
||||
StartedAt: started,
|
||||
Command: strings.Join(c.Command, " "),
|
||||
Host: pod.Spec.NodeName,
|
||||
Tty: c.TTY,
|
||||
Stats: utils.NewRingBuffer[container.ContainerStat](300),
|
||||
FullyLoaded: true,
|
||||
})
|
||||
}
|
||||
return containers
|
||||
}
|
||||
|
||||
func (k *K8sClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
selector := ""
|
||||
if labels.Exists() {
|
||||
for key, values := range labels {
|
||||
for _, value := range values {
|
||||
if selector != "" {
|
||||
selector += ","
|
||||
}
|
||||
selector += fmt.Sprintf("%s=%s", key, value)
|
||||
}
|
||||
}
|
||||
log.Debug().Str("selector", selector).Msg("Listing containers with labels")
|
||||
}
|
||||
pods, err := k.Clientset.CoreV1().Pods(k.namespace).List(ctx, metav1.ListOptions{LabelSelector: selector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers []container.Container
|
||||
for _, pod := range pods.Items {
|
||||
containers = append(containers, podToContainers(&pod)...)
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func phaseToState(phase corev1.PodPhase) string {
|
||||
switch phase {
|
||||
case corev1.PodPending:
|
||||
return "created"
|
||||
case corev1.PodRunning:
|
||||
return "running"
|
||||
case corev1.PodSucceeded:
|
||||
return "exited"
|
||||
case corev1.PodFailed:
|
||||
return "exited"
|
||||
case corev1.PodUnknown:
|
||||
return "unknown"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (k *K8sClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
|
||||
log.Debug().Str("id", id).Msg("Finding container")
|
||||
namespace, podName, containerName := parsePodContainerID(id)
|
||||
|
||||
pod, err := k.Clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return container.Container{}, err
|
||||
}
|
||||
|
||||
for _, c := range podToContainers(pod) {
|
||||
if c.ID == id {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
return container.Container{}, fmt.Errorf("container %s not found in pod %s", containerName, podName)
|
||||
}
|
||||
|
||||
func (k *K8sClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
namespace, podName, containerName := parsePodContainerID(id)
|
||||
|
||||
var lines int64 = 500
|
||||
opts := &corev1.PodLogOptions{
|
||||
Container: containerName,
|
||||
Follow: true,
|
||||
Previous: false,
|
||||
Timestamps: true,
|
||||
SinceTime: &metav1.Time{Time: since},
|
||||
TailLines: &lines,
|
||||
}
|
||||
|
||||
return k.Clientset.CoreV1().Pods(namespace).GetLogs(podName, opts).Stream(ctx)
|
||||
}
|
||||
|
||||
func (k *K8sClient) ContainerLogsBetweenDates(ctx context.Context, id string, start time.Time, end time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
namespace, podName, containerName := parsePodContainerID(id)
|
||||
|
||||
opts := &corev1.PodLogOptions{
|
||||
Container: containerName,
|
||||
Follow: false,
|
||||
Timestamps: true,
|
||||
SinceTime: &metav1.Time{Time: start},
|
||||
}
|
||||
|
||||
return k.Clientset.CoreV1().Pods(namespace).GetLogs(podName, opts).Stream(ctx)
|
||||
}
|
||||
|
||||
func (k *K8sClient) ContainerEvents(ctx context.Context, ch chan<- container.ContainerEvent) error {
|
||||
watch, err := k.Clientset.CoreV1().Pods(k.namespace).Watch(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for event := range watch.ResultChan() {
|
||||
log.Debug().Interface("event.type", event.Type).Msg("Received kubernetes event")
|
||||
pod, ok := event.Object.(*corev1.Pod)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
name := ""
|
||||
switch event.Type {
|
||||
case "ADDED":
|
||||
name = "create"
|
||||
case "DELETED":
|
||||
name = "destroy"
|
||||
case "MODIFIED":
|
||||
name = "update"
|
||||
}
|
||||
|
||||
for _, c := range podToContainers(pod) {
|
||||
ch <- container.ContainerEvent{
|
||||
Name: name,
|
||||
ActorID: c.ID,
|
||||
Host: pod.Spec.NodeName,
|
||||
Time: time.Now(),
|
||||
Container: &c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *K8sClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (k *K8sClient) Ping(ctx context.Context) error {
|
||||
_, err := k.Clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{Limit: 1})
|
||||
return err
|
||||
}
|
||||
|
||||
func (k *K8sClient) Host() container.Host {
|
||||
return k.host
|
||||
}
|
||||
|
||||
func (k *K8sClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
|
||||
// Implementation for container actions (start, stop, restart, etc.)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to parse pod and container names from container ID
|
||||
func parsePodContainerID(id string) (string, string, string) {
|
||||
parts := strings.Split(id, ":")
|
||||
return parts[0], parts[1], parts[2]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
)
|
||||
|
||||
type LogReader struct {
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
func NewLogReader(reader io.ReadCloser) *LogReader {
|
||||
return &LogReader{
|
||||
reader: bufio.NewReader(reader),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LogReader) Read() (string, container.StdType, error) {
|
||||
line, err := r.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
return line, container.STDOUT, nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metricsclient "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||
)
|
||||
|
||||
var timeToStop = 2 * time.Hour
|
||||
|
||||
type K8sStatsCollector struct {
|
||||
client *K8sClient
|
||||
metrics *metricsclient.Clientset
|
||||
subscribers *xsync.MapOf[context.Context, chan<- container.ContainerStat]
|
||||
stopper context.CancelFunc
|
||||
timer *time.Timer
|
||||
mu sync.Mutex
|
||||
totalStarted atomic.Int32
|
||||
labels container.ContainerLabels
|
||||
}
|
||||
|
||||
func NewK8sStatsCollector(client *K8sClient, labels container.ContainerLabels) (*K8sStatsCollector, error) {
|
||||
metricsClient, err := metricsclient.NewForConfig(client.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &K8sStatsCollector{
|
||||
subscribers: xsync.NewMapOf[context.Context, chan<- container.ContainerStat](),
|
||||
client: client,
|
||||
labels: labels,
|
||||
metrics: metricsClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *K8sStatsCollector) Subscribe(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
c.subscribers.Store(ctx, stats)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.subscribers.Delete(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *K8sStatsCollector) Stop() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.totalStarted.Add(-1) == 0 {
|
||||
c.timer = time.AfterFunc(timeToStop, func() {
|
||||
c.forceStop()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *K8sStatsCollector) forceStop() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.stopper != nil {
|
||||
c.stopper()
|
||||
c.stopper = nil
|
||||
log.Debug().Msg("stopped container k8s stats collector")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *K8sStatsCollector) reset() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.timer != nil {
|
||||
c.timer.Stop()
|
||||
}
|
||||
c.timer = nil
|
||||
}
|
||||
|
||||
// Start starts the stats collector and blocks until it's stopped. It returns true if the collector was stopped, false if it was already running
|
||||
func (sc *K8sStatsCollector) Start(parentCtx context.Context) bool {
|
||||
sc.reset()
|
||||
sc.totalStarted.Add(1)
|
||||
|
||||
sc.mu.Lock()
|
||||
if sc.stopper != nil {
|
||||
sc.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
var ctx context.Context
|
||||
ctx, sc.stopper = context.WithCancel(parentCtx)
|
||||
sc.mu.Unlock()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
metricList, err := sc.metrics.MetricsV1beta1().PodMetricses(sc.client.namespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
for _, pod := range metricList.Items {
|
||||
for _, c := range pod.Containers {
|
||||
stat := container.ContainerStat{
|
||||
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
|
||||
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
|
||||
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
|
||||
}
|
||||
log.Trace().Interface("stat", stat).Msg("k8s stats")
|
||||
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
|
||||
select {
|
||||
case stats <- stat:
|
||||
case <-c.Done():
|
||||
sc.subscribers.Delete(c)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
||||
type AgentCmd struct {
|
||||
Addr string `arg:"--agent-addr,env:DOZZLE_AGENT_ADDR" default:":7007" help:"sets the host:port to bind for the agent"`
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
client, err := docker.NewLocalClient(args.Hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker client: %w", err)
|
||||
}
|
||||
certs, err := ReadCertificates(embeddedCerts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read certificates: %w", err)
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", args.Agent.Addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen: %w", err)
|
||||
}
|
||||
tempFile, err := os.CreateTemp("./", "agent-*.addr")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
io.WriteString(tempFile, listener.Addr().String())
|
||||
go StartEvent(args, "", client, "agent")
|
||||
server, err := agent.NewServer(client, certs, args.Version(), args.Filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create agent server: %w", err)
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
go func() {
|
||||
log.Info().Msgf("Dozzle agent version %s", args.Version())
|
||||
log.Info().Msgf("Agent listening on %s", listener.Addr().String())
|
||||
|
||||
if err := server.Serve(listener); err != nil {
|
||||
log.Error().Err(err).Msg("failed to serve")
|
||||
}
|
||||
}()
|
||||
<-ctx.Done()
|
||||
stop()
|
||||
log.Info().Msg("Shutting down agent")
|
||||
server.Stop()
|
||||
log.Debug().Str("file", tempFile.Name()).Msg("Removing temp file")
|
||||
os.Remove(tempFile.Name())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type AgentTestCmd struct {
|
||||
Address string `arg:"positional"`
|
||||
}
|
||||
|
||||
func (at *AgentTestCmd) Run(args Args, embeddedCerts embed.FS) error {
|
||||
certs, err := ReadCertificates(embeddedCerts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading certificates: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("endpoint", args.AgentTest.Address).Msg("Connecting to agent")
|
||||
|
||||
agent, err := agent.NewClient(args.AgentTest.Address, certs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to agent: %w", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
|
||||
defer cancel()
|
||||
host, err := agent.Host(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching host info for agent: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("endpoint", args.AgentTest.Address).Str("version", host.AgentVersion).Str("name", host.Name).Str("id", host.ID).Msg("Successfully connected to agent")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -29,29 +30,15 @@ type Args struct {
|
||||
Mode string `arg:"env:DOZZLE_MODE" default:"server" help:"sets the mode to run in (server, swarm)"`
|
||||
TimeoutString string `arg:"--timeout,env:DOZZLE_TIMEOUT" default:"10s" help:"sets the timeout for docker client"`
|
||||
Timeout time.Duration `arg:"-"`
|
||||
Namespace string `arg:"env:DOZZLE_NAMESPACE" default:"" help:"sets the namespace to use in k8s"`
|
||||
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"`
|
||||
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
|
||||
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
|
||||
AgentTest *AgentTestCmd `arg:"subcommand:agent-test" help:"tests an agent"`
|
||||
}
|
||||
|
||||
type HealthcheckCmd struct {
|
||||
}
|
||||
|
||||
type AgentCmd struct {
|
||||
Addr string `arg:"--agent-addr,env:DOZZLE_AGENT_ADDR" default:":7007" help:"sets the host:port to bind for the agent"`
|
||||
}
|
||||
|
||||
type AgentTestCmd struct {
|
||||
Address string `arg:"positional"`
|
||||
}
|
||||
|
||||
type GenerateCmd struct {
|
||||
Username string `arg:"positional"`
|
||||
Password string `arg:"--password, -p" help:"sets the password for the user"`
|
||||
Name string `arg:"--name, -n" help:"sets the display name for the user"`
|
||||
Email string `arg:"--email, -e" help:"sets the email for the user"`
|
||||
Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."`
|
||||
type Runnable interface {
|
||||
Run(args Args, embeddedCerts embed.FS) error
|
||||
}
|
||||
|
||||
func (Args) Version() string {
|
||||
|
||||
@@ -6,12 +6,13 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
docker_support "github.com/amir20/dozzle/internal/support/docker"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func CreateMultiHostService(embeddedCerts embed.FS, args Args) (container.Client, *docker_support.MultiHostService) {
|
||||
var clients []docker_support.ClientService
|
||||
func CreateMultiHostService(embeddedCerts embed.FS, args Args) *docker_support.MultiHostService {
|
||||
var clients []container_support.ClientService
|
||||
if len(args.RemoteHost) > 0 {
|
||||
log.Info().Msg(`Consider using Dozzle's remote agent to manage remote hosts. See https://dozzle.dev/guide/agent for more information`)
|
||||
}
|
||||
@@ -47,6 +48,7 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (container.Client
|
||||
log.Debug().Msg("Adding local Docker Engine")
|
||||
clients = append(clients, docker_support.NewDockerClientService(localClient, args.Filter))
|
||||
}
|
||||
go StartEvent(args, "server", localClient, "")
|
||||
}
|
||||
|
||||
certs, err := ReadCertificates(embeddedCerts)
|
||||
@@ -55,5 +57,5 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (container.Client
|
||||
}
|
||||
|
||||
clientManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs, clients...)
|
||||
return localClient, docker_support.NewMultiHostService(clientManager, args.Timeout)
|
||||
return docker_support.NewMultiHostService(clientManager, args.Timeout)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
)
|
||||
|
||||
type GenerateCmd struct {
|
||||
Username string `arg:"positional"`
|
||||
Password string `arg:"--password, -p" help:"sets the password for the user"`
|
||||
Name string `arg:"--name, -n" help:"sets the display name for the user"`
|
||||
Email string `arg:"--email, -e" help:"sets the email for the user"`
|
||||
Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."`
|
||||
}
|
||||
|
||||
func (g *GenerateCmd) Run(args Args, embeddedCerts embed.FS) error {
|
||||
StartEvent(args, "", nil, "generate")
|
||||
if args.Generate.Username == "" || args.Generate.Password == "" {
|
||||
return fmt.Errorf("username and password are required")
|
||||
}
|
||||
|
||||
buffer := auth.GenerateUsers(auth.User{
|
||||
Username: args.Generate.Username,
|
||||
Password: args.Generate.Password,
|
||||
Name: args.Generate.Name,
|
||||
Email: args.Generate.Email,
|
||||
Filter: args.Generate.Filter,
|
||||
}, true)
|
||||
|
||||
if _, err := os.Stdout.Write(buffer.Bytes()); err != nil {
|
||||
return fmt.Errorf("failed to write to stdout: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/amir20/dozzle/internal/healthcheck"
|
||||
)
|
||||
|
||||
type HealthcheckCmd struct {
|
||||
}
|
||||
|
||||
func (h *HealthcheckCmd) Run(args Args, embeddedCerts embed.FS) error {
|
||||
files, err := os.ReadDir(".")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read directory: %w", err)
|
||||
}
|
||||
|
||||
agentAddress := ""
|
||||
for _, file := range files {
|
||||
if match, _ := filepath.Match("agent-*.addr", file.Name()); match {
|
||||
data, err := os.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
agentAddress = string(data)
|
||||
break
|
||||
}
|
||||
}
|
||||
if agentAddress == "" {
|
||||
if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil {
|
||||
return fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
} else {
|
||||
certs, err := ReadCertificates(embeddedCerts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read certificates: %w", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
|
||||
defer cancel()
|
||||
if err := healthcheck.RPCRequest(ctx, agentAddress, certs); err != nil {
|
||||
return fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
func ConfigureLogger(level string) {
|
||||
if level, err := zerolog.ParseLevel(level); err == nil {
|
||||
zerolog.SetGlobalLevel(level)
|
||||
log.Logger = log.With().Str("version", Version).Logger()
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package docker_support
|
||||
package container_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -22,7 +22,7 @@ func NewAgentService(client *agent.Client) ClientService {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agentService) FindContainer(ctx context.Context, id string, filter container.ContainerFilter) (container.Container, error) {
|
||||
func (a *agentService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
|
||||
return a.client.FindContainer(ctx, id)
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ func (a *agentService) StreamLogs(ctx context.Context, container container.Conta
|
||||
return a.client.StreamContainerLogs(ctx, container.ID, from, stdTypes, events)
|
||||
}
|
||||
|
||||
func (a *agentService) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
|
||||
log.Debug().Interface("filter", filter).Msg("Listing containers from agent")
|
||||
return a.client.ListContainers(ctx, filter)
|
||||
func (a *agentService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
log.Debug().Interface("labels", labels).Msg("Listing containers from agent")
|
||||
return a.client.ListContainers(ctx, labels)
|
||||
}
|
||||
|
||||
func (a *agentService) Host(ctx context.Context) (container.Host, error) {
|
||||
@@ -0,0 +1,28 @@
|
||||
package container_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
)
|
||||
|
||||
type ContainerFilter = func(*container.Container) bool
|
||||
|
||||
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)
|
||||
|
||||
// Subscriptions
|
||||
SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat)
|
||||
SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent)
|
||||
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container)
|
||||
|
||||
// Blocking streaming functions that should be used in a goroutine
|
||||
StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error
|
||||
}
|
||||
+13
-6
@@ -1,4 +1,4 @@
|
||||
package docker_support
|
||||
package container_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,23 +8,30 @@ import (
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
)
|
||||
|
||||
type containerService struct {
|
||||
type ContainerService struct {
|
||||
clientService ClientService
|
||||
Container container.Container
|
||||
}
|
||||
|
||||
func (c *containerService) RawLogs(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
|
||||
func NewContainerService(clientService ClientService, container container.Container) *ContainerService {
|
||||
return &ContainerService{
|
||||
clientService: clientService,
|
||||
Container: container,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ContainerService) RawLogs(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
|
||||
return c.clientService.RawLogs(ctx, c.Container, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (c *containerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
func (c *ContainerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
return c.clientService.LogsBetweenDates(ctx, c.Container, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (c *containerService) StreamLogs(ctx context.Context, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
func (c *ContainerService) StreamLogs(ctx context.Context, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
return c.clientService.StreamLogs(ctx, c.Container, from, stdTypes, events)
|
||||
}
|
||||
|
||||
func (c *containerService) Action(ctx context.Context, action container.ContainerAction) error {
|
||||
func (c *ContainerService) Action(ctx context.Context, action container.ContainerAction) error {
|
||||
return c.clientService.ContainerAction(ctx, c.Container, action)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package docker_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
)
|
||||
|
||||
type ClientService interface {
|
||||
FindContainer(ctx context.Context, id string, filter container.ContainerFilter) (container.Container, error)
|
||||
ListContainers(ctx context.Context, filter container.ContainerFilter) ([]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)
|
||||
|
||||
// Subscriptions
|
||||
SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat)
|
||||
SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent)
|
||||
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container)
|
||||
|
||||
// Blocking streaming functions that should be used in a goroutine
|
||||
StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error
|
||||
}
|
||||
|
||||
type dockerClientService struct {
|
||||
client container.Client
|
||||
store *container.ContainerStore
|
||||
}
|
||||
|
||||
func NewDockerClientService(client container.Client, filter container.ContainerFilter) ClientService {
|
||||
return &dockerClientService{
|
||||
client: client,
|
||||
store: container.NewContainerStore(context.Background(), client, filter),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dockerClientService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
|
||||
return d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
reader, err := d.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := container.NewEventGenerator(ctx, reader, c)
|
||||
return g.Events, nil
|
||||
}
|
||||
|
||||
func (d *dockerClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
reader, err := d.client.ContainerLogs(ctx, c.ID, from, stdTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g := container.NewEventGenerator(ctx, reader, c)
|
||||
for event := range g.Events {
|
||||
events <- event
|
||||
}
|
||||
|
||||
select {
|
||||
case e := <-g.Errors:
|
||||
return e
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dockerClientService) FindContainer(ctx context.Context, id string, filter container.ContainerFilter) (container.Container, error) {
|
||||
return d.store.FindContainer(id, filter)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
|
||||
return d.client.ContainerActions(ctx, action, container.ID)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
|
||||
return d.store.ListContainers(filter)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) Host(ctx context.Context) (container.Host, error) {
|
||||
return d.client.Host(), nil
|
||||
}
|
||||
|
||||
func (d *dockerClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
d.store.SubscribeStats(ctx, stats)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
d.store.SubscribeEvents(ctx, events)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
|
||||
d.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package docker_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type DockerClientService struct {
|
||||
client container.Client
|
||||
store *container.ContainerStore
|
||||
}
|
||||
|
||||
func NewDockerClientService(client container.Client, labels container.ContainerLabels) *DockerClientService {
|
||||
statsCollector := docker.NewDockerStatsCollector(client, labels)
|
||||
return &DockerClientService{
|
||||
client: client,
|
||||
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DockerClientService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
|
||||
reader, err := d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
in, out := io.Pipe()
|
||||
|
||||
go func() {
|
||||
if container.Tty {
|
||||
if _, err := io.Copy(out, reader); err != nil {
|
||||
log.Error().Err(err).Msgf("error copying logs for container %s", container.ID)
|
||||
}
|
||||
} else {
|
||||
if _, err := stdcopy.StdCopy(out, out, reader); err != nil {
|
||||
log.Error().Err(err).Msgf("error copying logs for container %s", container.ID)
|
||||
}
|
||||
}
|
||||
|
||||
out.Close()
|
||||
}()
|
||||
|
||||
return in, nil
|
||||
|
||||
}
|
||||
|
||||
func (d *DockerClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
reader, err := d.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(ctx, dockerReader, c)
|
||||
return g.Events, nil
|
||||
}
|
||||
|
||||
func (d *DockerClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
reader, err := d.client.ContainerLogs(ctx, c.ID, from, stdTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(ctx, dockerReader, c)
|
||||
for event := range g.Events {
|
||||
events <- event
|
||||
}
|
||||
|
||||
select {
|
||||
case e := <-g.Errors:
|
||||
return e
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DockerClientService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
|
||||
return d.store.FindContainer(id, labels)
|
||||
}
|
||||
|
||||
func (d *DockerClientService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
|
||||
return d.client.ContainerActions(ctx, action, container.ID)
|
||||
}
|
||||
|
||||
func (d *DockerClientService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
return d.store.ListContainers(labels)
|
||||
}
|
||||
|
||||
func (d *DockerClientService) Host(ctx context.Context) (container.Host, error) {
|
||||
return d.client.Host(), nil
|
||||
}
|
||||
|
||||
func (d *DockerClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
d.store.SubscribeStats(ctx, stats)
|
||||
}
|
||||
|
||||
func (d *DockerClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
d.store.SubscribeEvents(ctx, events)
|
||||
}
|
||||
|
||||
func (d *DockerClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
|
||||
d.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
@@ -6,11 +6,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ContainerFilter = func(*container.Container) bool
|
||||
|
||||
type HostUnavailableError struct {
|
||||
Host container.Host
|
||||
Err error
|
||||
@@ -21,9 +20,9 @@ func (h *HostUnavailableError) Error() string {
|
||||
}
|
||||
|
||||
type ClientManager interface {
|
||||
Find(id string) (ClientService, bool)
|
||||
List() []ClientService
|
||||
RetryAndList() ([]ClientService, []error)
|
||||
Find(id string) (container_support.ClientService, bool)
|
||||
List() []container_support.ClientService
|
||||
RetryAndList() ([]container_support.ClientService, []error)
|
||||
Subscribe(ctx context.Context, channel chan<- container.Host)
|
||||
Hosts(ctx context.Context) []container.Host
|
||||
LocalClients() []container.Client
|
||||
@@ -43,25 +42,22 @@ func NewMultiHostService(manager ClientManager, timeout time.Duration) *MultiHos
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MultiHostService) FindContainer(host string, id string, filter container.ContainerFilter) (*containerService, error) {
|
||||
func (m *MultiHostService) FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error) {
|
||||
client, ok := m.manager.Find(host)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("host %s not found", host)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
defer cancel()
|
||||
container, err := client.FindContainer(ctx, id, filter)
|
||||
container, err := client.FindContainer(ctx, id, labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &containerService{
|
||||
clientService: client,
|
||||
Container: container,
|
||||
}, nil
|
||||
return container_support.NewContainerService(client, container), nil
|
||||
}
|
||||
|
||||
func (m *MultiHostService) ListContainersForHost(host string, filter container.ContainerFilter) ([]container.Container, error) {
|
||||
func (m *MultiHostService) ListContainersForHost(host string, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
client, ok := m.manager.Find(host)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("host %s not found", host)
|
||||
@@ -69,17 +65,17 @@ func (m *MultiHostService) ListContainersForHost(host string, filter container.C
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
defer cancel()
|
||||
|
||||
return client.ListContainers(ctx, filter)
|
||||
return client.ListContainers(ctx, labels)
|
||||
}
|
||||
|
||||
func (m *MultiHostService) ListAllContainers(filter container.ContainerFilter) ([]container.Container, []error) {
|
||||
func (m *MultiHostService) ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error) {
|
||||
containers := make([]container.Container, 0)
|
||||
clients, errors := m.manager.RetryAndList()
|
||||
|
||||
for _, client := range clients {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
defer cancel()
|
||||
list, err := client.ListContainers(ctx, filter)
|
||||
list, err := client.ListContainers(ctx, labels)
|
||||
if err != nil {
|
||||
host, _ := client.Host(ctx)
|
||||
log.Debug().Err(err).Str("host", host.Name).Msg("error listing containers")
|
||||
@@ -94,8 +90,8 @@ func (m *MultiHostService) ListAllContainers(filter container.ContainerFilter) (
|
||||
return containers, errors
|
||||
}
|
||||
|
||||
func (m *MultiHostService) ListAllContainersFiltered(userFilter container.ContainerFilter, filter ContainerFilter) ([]container.Container, []error) {
|
||||
containers, err := m.ListAllContainers(userFilter)
|
||||
func (m *MultiHostService) ListAllContainersFiltered(userLabels container.ContainerLabels, filter container_support.ContainerFilter) ([]container.Container, []error) {
|
||||
containers, err := m.ListAllContainers(userLabels)
|
||||
filtered := make([]container.Container, 0, len(containers))
|
||||
for _, container := range containers {
|
||||
if filter(&container) {
|
||||
@@ -112,7 +108,7 @@ func (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events c
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter ContainerFilter) {
|
||||
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
|
||||
newContainers := make(chan container.Container)
|
||||
for _, client := range m.manager.List() {
|
||||
client.SubscribeContainersStarted(ctx, newContainers)
|
||||
@@ -135,10 +131,6 @@ func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, conta
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *MultiHostService) TotalClients() int {
|
||||
return len(m.manager.List())
|
||||
}
|
||||
|
||||
func (m *MultiHostService) Hosts() []container.Host {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
defer cancel()
|
||||
@@ -161,3 +153,7 @@ func (m *MultiHostService) SubscribeAvailableHosts(ctx context.Context, hosts ch
|
||||
func (m *MultiHostService) LocalClients() []container.Client {
|
||||
return m.manager.LocalClients()
|
||||
}
|
||||
|
||||
func (m *MultiHostService) TotalClients() int {
|
||||
return len(m.manager.List())
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/samber/lo"
|
||||
lop "github.com/samber/lo/parallel"
|
||||
@@ -17,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
type RetriableClientManager struct {
|
||||
clients map[string]ClientService
|
||||
clients map[string]container_support.ClientService
|
||||
failedAgents []string
|
||||
certs tls.Certificate
|
||||
mu sync.RWMutex
|
||||
@@ -25,8 +27,8 @@ type RetriableClientManager struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls.Certificate, clients ...ClientService) *RetriableClientManager {
|
||||
clientMap := make(map[string]ClientService)
|
||||
func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls.Certificate, clients ...container_support.ClientService) *RetriableClientManager {
|
||||
clientMap := make(map[string]container_support.ClientService)
|
||||
for _, client := range clients {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
@@ -64,7 +66,7 @@ func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls
|
||||
if _, ok := clientMap[host.ID]; ok {
|
||||
log.Warn().Str("host", host.Name).Str("id", host.ID).Msg("duplicate host with same ID found")
|
||||
} else {
|
||||
clientMap[host.ID] = NewAgentService(agent)
|
||||
clientMap[host.ID] = container_support.NewAgentService(agent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ func (m *RetriableClientManager) Subscribe(ctx context.Context, channel chan<- c
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
|
||||
func (m *RetriableClientManager) RetryAndList() ([]container_support.ClientService, []error) {
|
||||
m.mu.Lock()
|
||||
errors := make([]error, 0)
|
||||
if len(m.failedAgents) > 0 {
|
||||
@@ -110,7 +112,7 @@ func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
|
||||
continue
|
||||
}
|
||||
|
||||
m.clients[host.ID] = NewAgentService(agent)
|
||||
m.clients[host.ID] = container_support.NewAgentService(agent)
|
||||
m.subscribers.Range(func(ctx context.Context, channel chan<- container.Host) bool {
|
||||
host.Available = true
|
||||
|
||||
@@ -133,14 +135,14 @@ func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
|
||||
return m.List(), errors
|
||||
}
|
||||
|
||||
func (m *RetriableClientManager) List() []ClientService {
|
||||
func (m *RetriableClientManager) List() []container_support.ClientService {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return lo.Values(m.clients)
|
||||
}
|
||||
|
||||
func (m *RetriableClientManager) Find(id string) (ClientService, bool) {
|
||||
func (m *RetriableClientManager) Find(id string) (container_support.ClientService, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
@@ -155,7 +157,7 @@ func (m *RetriableClientManager) String() string {
|
||||
func (m *RetriableClientManager) Hosts(ctx context.Context) []container.Host {
|
||||
clients := m.List()
|
||||
|
||||
hosts := lop.Map(clients, func(client ClientService, _ int) container.Host {
|
||||
hosts := lop.Map(clients, func(client container_support.ClientService, _ int) container.Host {
|
||||
host, err := client.Host(ctx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("host", host.Name).Msg("error fetching host info for client")
|
||||
@@ -186,7 +188,7 @@ func (m *RetriableClientManager) LocalClients() []container.Client {
|
||||
clients := make([]container.Client, 0)
|
||||
|
||||
for _, service := range services {
|
||||
if clientService, ok := service.(*dockerClientService); ok {
|
||||
if clientService, ok := service.(*DockerClientService); ok {
|
||||
clients = append(clients, clientService.client)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/samber/lo"
|
||||
lop "github.com/samber/lo/parallel"
|
||||
@@ -19,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
type SwarmClientManager struct {
|
||||
clients map[string]ClientService
|
||||
clients map[string]container_support.ClientService
|
||||
certs tls.Certificate
|
||||
mu sync.RWMutex
|
||||
subscribers *xsync.MapOf[context.Context, chan<- container.Host]
|
||||
@@ -47,9 +50,9 @@ func localIPs() []string {
|
||||
return ips
|
||||
}
|
||||
|
||||
func NewSwarmClientManager(localClient container.Client, certs tls.Certificate, timeout time.Duration, agentManager *RetriableClientManager, filter container.ContainerFilter) *SwarmClientManager {
|
||||
clientMap := make(map[string]ClientService)
|
||||
localService := NewDockerClientService(localClient, filter)
|
||||
func NewSwarmClientManager(localClient *docker.DockerClient, certs tls.Certificate, timeout time.Duration, agentManager *RetriableClientManager, labels container.ContainerLabels) *SwarmClientManager {
|
||||
clientMap := make(map[string]container_support.ClientService)
|
||||
localService := NewDockerClientService(localClient, labels)
|
||||
clientMap[localClient.Host().ID] = localService
|
||||
|
||||
id, ok := os.LookupEnv("HOSTNAME")
|
||||
@@ -90,7 +93,7 @@ func (m *SwarmClientManager) Subscribe(ctx context.Context, channel chan<- conta
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
|
||||
func (m *SwarmClientManager) RetryAndList() ([]container_support.ClientService, []error) {
|
||||
m.mu.Lock()
|
||||
|
||||
ips, err := net.LookupIP(fmt.Sprintf("tasks.%s", m.name))
|
||||
@@ -104,7 +107,7 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
|
||||
}
|
||||
|
||||
clients := lo.Values(m.clients)
|
||||
endpoints := lo.KeyBy(clients, func(client ClientService) string {
|
||||
endpoints := lo.KeyBy(clients, func(client container_support.ClientService) string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
defer cancel()
|
||||
host, _ := client.Host(ctx)
|
||||
@@ -155,7 +158,7 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
|
||||
continue
|
||||
}
|
||||
|
||||
client := NewAgentService(agent)
|
||||
client := container_support.NewAgentService(agent)
|
||||
m.clients[host.ID] = client
|
||||
log.Info().Stringer("ip", ip).Str("id", host.ID).Str("name", host.Name).Msg("added new swarm agent")
|
||||
|
||||
@@ -182,7 +185,7 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
|
||||
return m.List(), errors
|
||||
}
|
||||
|
||||
func (m *SwarmClientManager) List() []ClientService {
|
||||
func (m *SwarmClientManager) List() []container_support.ClientService {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
@@ -192,7 +195,7 @@ func (m *SwarmClientManager) List() []ClientService {
|
||||
return append(agents, clients...)
|
||||
}
|
||||
|
||||
func (m *SwarmClientManager) Find(id string) (ClientService, bool) {
|
||||
func (m *SwarmClientManager) Find(id string) (container_support.ClientService, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
@@ -210,7 +213,7 @@ func (m *SwarmClientManager) Hosts(ctx context.Context) []container.Host {
|
||||
clients := lo.Values(m.clients)
|
||||
m.mu.RUnlock()
|
||||
|
||||
swarmNodes := lop.Map(clients, func(client ClientService, _ int) container.Host {
|
||||
swarmNodes := lop.Map(clients, func(client container_support.ClientService, _ int) container.Host {
|
||||
host, err := client.Host(ctx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("id", host.ID).Msg("error getting host from client")
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package k8s_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/k8s"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type K8sClusterService struct {
|
||||
client *K8sClientService
|
||||
timeout time.Duration
|
||||
hosts []container.Host
|
||||
}
|
||||
|
||||
func NewK8sClusterService(client *k8s.K8sClient, timeout time.Duration) (*K8sClusterService, error) {
|
||||
hosts := make([]container.Host, 0)
|
||||
nodes, err := client.Clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(nodes.Items) == 0 {
|
||||
return nil, fmt.Errorf("nodes not found")
|
||||
}
|
||||
|
||||
for _, node := range nodes.Items {
|
||||
hosts = append(hosts, container.Host{
|
||||
ID: node.Name,
|
||||
Name: node.Name,
|
||||
MemTotal: node.Status.Capacity.Memory().Value(),
|
||||
NCPU: int(node.Status.Capacity.Cpu().Value()),
|
||||
DockerVersion: node.Status.NodeInfo.ContainerRuntimeVersion,
|
||||
Type: "k8s",
|
||||
Available: true,
|
||||
})
|
||||
}
|
||||
|
||||
return &K8sClusterService{
|
||||
client: NewK8sClientService(client, container.ContainerLabels{}),
|
||||
timeout: timeout,
|
||||
hosts: hosts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error) {
|
||||
container, err := m.client.FindContainer(context.Background(), id, labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return container_support.NewContainerService(m.client, container), nil
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) ListContainersForHost(host string, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
containers, err := m.client.ListContainers(context.Background(), labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredContainers := make([]container.Container, 0)
|
||||
for _, container := range containers {
|
||||
if container.Host == host {
|
||||
filteredContainers = append(filteredContainers, container)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredContainers, nil
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error) {
|
||||
containers, err := m.client.ListContainers(context.Background(), labels)
|
||||
if err != nil {
|
||||
return nil, []error{err}
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) ListAllContainersFiltered(userLabels container.ContainerLabels, filter container_support.ContainerFilter) ([]container.Container, []error) {
|
||||
containers, err := m.ListAllContainers(userLabels)
|
||||
filtered := make([]container.Container, 0, len(containers))
|
||||
for _, container := range containers {
|
||||
if filter(&container) {
|
||||
filtered = append(filtered, container)
|
||||
}
|
||||
}
|
||||
return filtered, err
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) SubscribeEventsAndStats(ctx context.Context, events chan<- container.ContainerEvent, stats chan<- container.ContainerStat) {
|
||||
m.client.SubscribeEvents(ctx, events)
|
||||
m.client.SubscribeStats(ctx, stats)
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
|
||||
newContainers := make(chan container.Container)
|
||||
m.client.SubscribeContainersStarted(ctx, newContainers)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
close(newContainers)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for container := range newContainers {
|
||||
if filter(&container) {
|
||||
select {
|
||||
case containers <- container:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) Hosts() []container.Host {
|
||||
return m.hosts
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) LocalHost() (container.Host, error) {
|
||||
return m.client.client.Host(), nil
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) SubscribeAvailableHosts(ctx context.Context, hosts chan<- container.Host) {
|
||||
}
|
||||
|
||||
func (m *K8sClusterService) LocalClients() []container.Client {
|
||||
return []container.Client{m.client.client}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package k8s_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/k8s"
|
||||
)
|
||||
|
||||
type K8sClientService struct {
|
||||
client *k8s.K8sClient
|
||||
store *container.ContainerStore
|
||||
}
|
||||
|
||||
func NewK8sClientService(client *k8s.K8sClient, labels container.ContainerLabels) *K8sClientService {
|
||||
statsCollector, err := k8s.NewK8sStatsCollector(client, labels)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not create k8s stats collector")
|
||||
}
|
||||
return &K8sClientService{
|
||||
client: client,
|
||||
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
|
||||
}
|
||||
}
|
||||
|
||||
func (k *K8sClientService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
|
||||
return k.store.FindContainer(id, labels)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
return k.store.ListContainers(labels)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) Host(ctx context.Context) (container.Host, error) {
|
||||
return k.client.Host(), nil
|
||||
}
|
||||
|
||||
func (k *K8sClientService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
|
||||
return k.client.ContainerActions(ctx, action, container.ID)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
|
||||
reader, err := k.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k8sReader := k8s.NewLogReader(reader)
|
||||
g := container.NewEventGenerator(ctx, k8sReader, c)
|
||||
return g.Events, nil
|
||||
}
|
||||
|
||||
func (k *K8sClientService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
|
||||
return k.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
|
||||
reader, err := k.client.ContainerLogs(ctx, c.ID, from, stdTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k8sReader := k8s.NewLogReader(reader)
|
||||
g := container.NewEventGenerator(ctx, k8sReader, c)
|
||||
for event := range g.Events {
|
||||
events <- event
|
||||
}
|
||||
|
||||
select {
|
||||
case e := <-g.Errors:
|
||||
return e
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (k *K8sClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
|
||||
k.store.SubscribeStats(ctx, stats)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
|
||||
k.store.SubscribeEvents(ctx, events)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
|
||||
k.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
@@ -15,15 +13,15 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
|
||||
action := chi.URLParam(r, "action")
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
usersFilter := h.config.Filter
|
||||
userLabels := h.config.Labels
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerFilter.Exists() {
|
||||
usersFilter = user.ContainerFilter
|
||||
if user.ContainerLabels.Exists() {
|
||||
userLabels = user.ContainerLabels
|
||||
}
|
||||
}
|
||||
|
||||
containerService, err := h.multiHostService.FindContainer(hostKey(r), id, usersFilter)
|
||||
containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error while trying to find container")
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
@@ -37,9 +35,7 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := containerService.Action(ctx, parsedAction); err != nil {
|
||||
if err := containerService.Action(r.Context(), parsedAction); err != nil {
|
||||
log.Error().Err(err).Msg("error while trying to perform container action")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
func (h *handler) debugStore(w http.ResponseWriter, r *http.Request) {
|
||||
respone := make(map[string]interface{})
|
||||
respone["hosts"] = h.multiHostService.Hosts()
|
||||
containers, errors := h.multiHostService.ListAllContainers(container.ContainerFilter{})
|
||||
respone["hosts"] = h.hostService.Hosts()
|
||||
containers, errors := h.hostService.ListAllContainers(container.ContainerLabels{})
|
||||
respone["containers"] = containers
|
||||
respone["errors"] = errors
|
||||
|
||||
|
||||
+10
-18
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -23,11 +22,11 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
usersFilter := h.config.Filter
|
||||
userLabels := h.config.Labels
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerFilter.Exists() {
|
||||
usersFilter = user.ContainerFilter
|
||||
if user.ContainerLabels.Exists() {
|
||||
userLabels = user.ContainerLabels
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
host := parts[0]
|
||||
id := parts[1]
|
||||
containerService, err := h.multiHostService.FindContainer(host, id, usersFilter)
|
||||
containerService, err := h.hostService.FindContainer(host, id, userLabels)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("error finding container %s", id)
|
||||
http.Error(w, fmt.Sprintf("error finding container %s: %v", id, err), http.StatusBadRequest)
|
||||
@@ -90,19 +89,12 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Copy logs directly to zip entry
|
||||
if containerService.Container.Tty {
|
||||
if _, err := io.Copy(f, reader); err != nil {
|
||||
log.Error().Err(err).Msgf("error copying logs for container %s", id)
|
||||
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err := stdcopy.StdCopy(f, f, reader); err != nil {
|
||||
log.Error().Err(err).Msgf("error copying logs for container %s", id)
|
||||
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Copy logs to zip file
|
||||
_, err = io.Copy(f, reader)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("error copying logs for container %s", id)
|
||||
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-18
@@ -24,18 +24,18 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
stats := make(chan container.ContainerStat)
|
||||
availableHosts := make(chan container.Host)
|
||||
|
||||
h.multiHostService.SubscribeEventsAndStats(r.Context(), events, stats)
|
||||
h.multiHostService.SubscribeAvailableHosts(r.Context(), availableHosts)
|
||||
h.hostService.SubscribeEventsAndStats(r.Context(), events, stats)
|
||||
h.hostService.SubscribeAvailableHosts(r.Context(), availableHosts)
|
||||
|
||||
usersFilter := h.config.Filter
|
||||
userLabels := h.config.Labels
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerFilter.Exists() {
|
||||
usersFilter = user.ContainerFilter
|
||||
if user.ContainerLabels.Exists() {
|
||||
userLabels = user.ContainerLabels
|
||||
}
|
||||
}
|
||||
|
||||
allContainers, errors := h.multiHostService.ListAllContainers(usersFilter)
|
||||
allContainers, errors := h.hostService.ListAllContainers(userLabels)
|
||||
|
||||
for _, err := range errors {
|
||||
log.Warn().Err(err).Msg("error listing containers")
|
||||
@@ -68,12 +68,12 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Debug().Str("event", event.Name).Str("id", event.ActorID).Msg("container event from store")
|
||||
switch event.Name {
|
||||
case "start", "die", "destroy", "rename":
|
||||
if event.Name == "start" || event.Name == "rename" {
|
||||
log.Debug().Str("action", event.Name).Str("id", event.ActorID).Msg("container event")
|
||||
|
||||
if containers, err := h.multiHostService.ListContainersForHost(event.Host, usersFilter); err == nil {
|
||||
if containers, err := h.hostService.ListContainersForHost(event.Host, userLabels); err == nil {
|
||||
log.Debug().Str("host", event.Host).Int("count", len(containers)).Msg("updating containers for host")
|
||||
if err := sseWriter.Event("containers-changed", containers); err != nil {
|
||||
log.Error().Err(err).Msg("error writing containers to event stream")
|
||||
return
|
||||
@@ -87,15 +87,11 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
case "update":
|
||||
log.Debug().Str("id", event.ActorID).Msg("container updated")
|
||||
if containerService, err := h.multiHostService.FindContainer(event.Host, event.ActorID, usersFilter); err == nil {
|
||||
if err := sseWriter.Event("container-updated", containerService.Container); err != nil {
|
||||
log.Error().Err(err).Msg("error writing event to event stream")
|
||||
return
|
||||
}
|
||||
if err := sseWriter.Event("container-updated", event.Container); err != nil {
|
||||
log.Error().Err(err).Msg("error writing event to event stream")
|
||||
return
|
||||
}
|
||||
case "health_status: healthy", "health_status: unhealthy":
|
||||
log.Debug().Str("container", event.ActorID).Str("health", event.Name).Msg("container health status")
|
||||
healthy := "unhealthy"
|
||||
if event.Name == "health_status: healthy" {
|
||||
healthy = "healthy"
|
||||
@@ -123,7 +119,7 @@ func sendBeaconEvent(h *handler, r *http.Request, runningContainers int) {
|
||||
b := types.BeaconEvent{
|
||||
AuthProvider: string(h.config.Authorization.Provider),
|
||||
Browser: r.Header.Get("User-Agent"),
|
||||
Clients: h.multiHostService.TotalClients(),
|
||||
Clients: len(h.hostService.Hosts()),
|
||||
HasActions: h.config.EnableActions,
|
||||
HasCustomAddress: h.config.Addr != ":8080",
|
||||
HasCustomBase: h.config.Base != "/",
|
||||
@@ -133,7 +129,7 @@ func sendBeaconEvent(h *handler, r *http.Request, runningContainers int) {
|
||||
Version: h.config.Version,
|
||||
}
|
||||
|
||||
local, err := h.multiHostService.LocalHost()
|
||||
local, err := h.hostService.LocalHost()
|
||||
if err == nil {
|
||||
b.ServerID = local.ID
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func Test_handler_streamEvents_happy(t *testing.T) {
|
||||
})
|
||||
|
||||
// This is needed so that the server is initialized for store
|
||||
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(mockedClient, container.ContainerFilter{}))
|
||||
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(mockedClient, container.ContainerLabels{}))
|
||||
multiHostService := docker_support.NewMultiHostService(manager, 3*time.Second)
|
||||
|
||||
server := CreateServer(multiHostService, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug().Msg("Executing healthcheck")
|
||||
|
||||
clients := h.multiHostService.LocalClients()
|
||||
clients := h.hostService.LocalClients()
|
||||
for _, client := range clients {
|
||||
if err := client.Ping(r.Context()); err != nil {
|
||||
log.Error().Err(err).Str("host", client.Host().Name).Msg("error pinging host")
|
||||
|
||||
@@ -43,7 +43,7 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
base = h.config.Base
|
||||
}
|
||||
|
||||
hosts := h.multiHostService.Hosts()
|
||||
hosts := h.hostService.Hosts()
|
||||
sort.Slice(hosts, func(i, j int) bool {
|
||||
return hosts[i].Name < hosts[j].Name
|
||||
})
|
||||
|
||||
+14
-13
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
"github.com/amir20/dozzle/internal/support/search"
|
||||
support_web "github.com/amir20/dozzle/internal/support/web"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
@@ -47,15 +48,15 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
usersFilter := h.config.Filter
|
||||
usersLabels := h.config.Labels
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerFilter.Exists() {
|
||||
usersFilter = user.ContainerFilter
|
||||
if user.ContainerLabels.Exists() {
|
||||
usersLabels = user.ContainerLabels
|
||||
}
|
||||
}
|
||||
|
||||
containerService, err := h.multiHostService.FindContainer(hostKey(r), id, usersFilter)
|
||||
containerService, err := h.hostService.FindContainer(hostKey(r), id, usersLabels)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
@@ -227,7 +228,7 @@ func (h *handler) streamHostLogs(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request, containerFilter ContainerFilter) {
|
||||
func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request, containerFilter container_support.ContainerFilter) {
|
||||
var stdTypes container.StdType
|
||||
if r.URL.Query().Has("stdout") {
|
||||
stdTypes |= container.STDOUT
|
||||
@@ -248,15 +249,15 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
usersFilter := h.config.Filter
|
||||
userLabels := h.config.Labels
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerFilter.Exists() {
|
||||
usersFilter = user.ContainerFilter
|
||||
if user.ContainerLabels.Exists() {
|
||||
userLabels = user.ContainerLabels
|
||||
}
|
||||
}
|
||||
|
||||
existingContainers, errs := h.multiHostService.ListAllContainersFiltered(usersFilter, containerFilter)
|
||||
existingContainers, errs := h.hostService.ListAllContainersFiltered(userLabels, containerFilter)
|
||||
if len(errs) > 0 {
|
||||
log.Warn().Err(errs[0]).Msg("error while listing containers")
|
||||
}
|
||||
@@ -290,7 +291,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
||||
events := make([]*container.LogEvent, 0)
|
||||
stillRunning := false
|
||||
for _, container := range existingContainers {
|
||||
containerService, err := h.multiHostService.FindContainer(container.Host, container.ID, usersFilter)
|
||||
containerService, err := h.hostService.FindContainer(container.Host, container.ID, userLabels)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error while finding container")
|
||||
@@ -337,7 +338,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
streamLogs := func(c container.Container) {
|
||||
containerService, err := h.multiHostService.FindContainer(c.Host, c.ID, usersFilter)
|
||||
containerService, err := h.hostService.FindContainer(c.Host, c.ID, userLabels)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error while finding container")
|
||||
return
|
||||
@@ -369,7 +370,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
newContainers := make(chan container.Container)
|
||||
h.multiHostService.SubscribeContainersStarted(r.Context(), newContainers, containerFilter)
|
||||
h.hostService.SubscribeContainersStarted(r.Context(), newContainers, containerFilter)
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
sseWriter.Ping()
|
||||
@@ -388,7 +389,7 @@ loop:
|
||||
}
|
||||
sseWriter.Message(logEvent)
|
||||
case c := <-newContainers:
|
||||
if _, err := h.multiHostService.FindContainer(c.Host, c.ID, usersFilter); err == nil {
|
||||
if _, err := h.hostService.FindContainer(c.Host, c.ID, userLabels); err == nil {
|
||||
events <- &container.ContainerEvent{ActorID: c.ID, Name: "container-started", Host: c.Host}
|
||||
go streamLogs(c)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func Test_handler_streamLogs_happy(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
id := "123456"
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil)
|
||||
|
||||
+23
-12
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
docker_support "github.com/amir20/dozzle/internal/support/docker"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
@@ -34,7 +35,7 @@ type Config struct {
|
||||
Dev bool
|
||||
Authorization Authorization
|
||||
EnableActions bool
|
||||
Filter container.ContainerFilter
|
||||
Labels container.ContainerLabels
|
||||
}
|
||||
|
||||
type Authorization struct {
|
||||
@@ -48,20 +49,30 @@ type Authorizer interface {
|
||||
CreateToken(string, string) (string, error)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
content fs.FS
|
||||
config *Config
|
||||
multiHostService *docker_support.MultiHostService
|
||||
type HostService interface {
|
||||
FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error)
|
||||
ListContainersForHost(host string, labels container.ContainerLabels) ([]container.Container, error)
|
||||
ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error)
|
||||
ListAllContainersFiltered(userFilter container.ContainerLabels, filter container_support.ContainerFilter) ([]container.Container, []error)
|
||||
SubscribeEventsAndStats(ctx context.Context, events chan<- container.ContainerEvent, stats chan<- container.ContainerStat)
|
||||
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter)
|
||||
Hosts() []container.Host
|
||||
LocalHost() (container.Host, error)
|
||||
SubscribeAvailableHosts(ctx context.Context, hosts chan<- container.Host)
|
||||
LocalClients() []container.Client
|
||||
}
|
||||
|
||||
type MultiHostService = docker_support.MultiHostService
|
||||
type ContainerFilter = docker_support.ContainerFilter
|
||||
type handler struct {
|
||||
content fs.FS
|
||||
config *Config
|
||||
hostService HostService
|
||||
}
|
||||
|
||||
func CreateServer(multiHostService *MultiHostService, content fs.FS, config Config) *http.Server {
|
||||
func CreateServer(hostService HostService, content fs.FS, config Config) *http.Server {
|
||||
handler := &handler{
|
||||
content: content,
|
||||
config: &config,
|
||||
multiHostService: multiHostService,
|
||||
content: content,
|
||||
config: &config,
|
||||
hostService: hostService,
|
||||
}
|
||||
|
||||
return &http.Server{Addr: config.Addr, Handler: createRouter(handler)}
|
||||
|
||||
@@ -38,8 +38,8 @@ func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- contai
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerFilter) ([]container.Container, error) {
|
||||
args := m.Called(ctx, filter)
|
||||
func (m *MockedClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
args := m.Called(ctx, labels)
|
||||
return args.Get(0).([]container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
@@ -86,12 +86,12 @@ func createHandler(client container.Client, content fs.FS, config Config) *chi.M
|
||||
content = afero.NewIOFS(fs)
|
||||
}
|
||||
|
||||
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(client, container.ContainerFilter{}))
|
||||
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(client, container.ContainerLabels{}))
|
||||
multiHostService := docker_support.NewMultiHostService(manager, 3*time.Second)
|
||||
return createRouter(&handler{
|
||||
multiHostService: multiHostService,
|
||||
content: content,
|
||||
config: &config,
|
||||
hostService: multiHostService,
|
||||
content: content,
|
||||
config: &config,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+5
-20
@@ -13,14 +13,9 @@ label:
|
||||
containers: Containere
|
||||
container: Ingen containere | 1 container | {count} containere
|
||||
service: Ingen tjenester | 1 tjeneste | {count} tjenester
|
||||
services: Tjenester
|
||||
running-containers: Kørende Containere
|
||||
all-containers: Alle Containere
|
||||
total-containers: Samlede Containere
|
||||
running: Kører
|
||||
total-cpu-usage: Samlet CPU Forbrug
|
||||
total-mem-usage: Samlet Hukommelsesforbrug
|
||||
dozzle-version: Dozzle Version
|
||||
all: Alle
|
||||
host: Vært
|
||||
hosts: Værter
|
||||
password: Kodeord
|
||||
@@ -32,12 +27,9 @@ label:
|
||||
avg-mem: Gns. MEM (%)
|
||||
pinned: Fastgjort
|
||||
per-page: Rækker per side
|
||||
swarm-mode: Swarm Tilstand
|
||||
services: Tjenester
|
||||
host-menu: Værter og Containere
|
||||
swarm-menu: Tjenester og Stacks
|
||||
group-menu: Brugerdefinerede Grupper
|
||||
|
||||
tooltip:
|
||||
search: Søg containere (⌘ + k, ⌃k)
|
||||
pin-column: Fastgør som kolonne
|
||||
@@ -52,13 +44,11 @@ error:
|
||||
events-stream:
|
||||
title: Uventet fejl
|
||||
message: >-
|
||||
Dozzle UI kunne ikke forbinde til API. Tjek venligst dine netværks indstillinger.
|
||||
Hvis du bruger en reverse proxy, vær sikker på at den er konfigureret korrekt.
|
||||
Dozzle UI kunne ikke forbinde til API. Tjek venligst dine netværks indstillinger. Hvis du bruger en reverse proxy, vær sikker på at den er konfigureret korrekt.
|
||||
events-timeout:
|
||||
title: Der er noget galt
|
||||
message: >-
|
||||
Dozzle UI timed ud inden der blev forbundet til API. Tjek venligst din
|
||||
internet forbindelse og prøv igen.
|
||||
Dozzle UI timed ud inden der blev forbundet til API. Tjek venligst din internet forbindelse og prøv igen.
|
||||
alert:
|
||||
redirected:
|
||||
title: Omdirigeret til ny container
|
||||
@@ -66,8 +56,7 @@ alert:
|
||||
similar-container-found:
|
||||
title: Lignende container fundet
|
||||
message: >-
|
||||
Dozzle fandt en lignende container {containerId} som kører på den samme
|
||||
host og vil automatisk skifte til den, med mindre du trykker 'Cancel'.
|
||||
Dozzle fandt en lignende container {containerId} som kører på den samme host og vil automatisk skifte til den, med mindre du trykker 'Cancel'.
|
||||
title:
|
||||
page-not-found: Side ikke fundet
|
||||
login: Godkendelse Krævet
|
||||
@@ -89,9 +78,6 @@ settings:
|
||||
show-timesamps: Vis tids stempler
|
||||
soft-wrap: Blødt bræk på linjer
|
||||
datetime-format: Overskriv dato og tids format
|
||||
12-24-format: >-
|
||||
Som standard vil Dozzle bruge din browsers lokalitet til at formatere tiden. Du kan
|
||||
tvinge 12 eller 24 timers format.
|
||||
font-size: Tekststørrelse at bruge til logs
|
||||
color-scheme: Farve skema
|
||||
options: Muligheder
|
||||
@@ -100,8 +86,7 @@ settings:
|
||||
search: Aktiver søgning med Dozzle ved brug af
|
||||
using-version: Du bruger Dozzle {version}.
|
||||
update-available: >-
|
||||
Ny version er tilgængelig! Opdater til <a href="{href}" target="_blank"
|
||||
rel="noreferrer noopener">{nextVersion}</a>.
|
||||
Ny version er tilgængelig! Opdater til <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Vis stdout og stderr mærker
|
||||
automatic-redirect: Automatisk omdiriger til nye containere med det samme navn
|
||||
compact: Slå kompakt tilstand til for logs
|
||||
|
||||
+4
-17
@@ -12,8 +12,8 @@ toolbar:
|
||||
label:
|
||||
containers: Container
|
||||
container: Keine Container | 1 Container | {count} Container
|
||||
total-containers: Gesamte Container
|
||||
service: Kein Service | 1 Service | {count} Services
|
||||
services: Service
|
||||
running-containers: Laufende Container
|
||||
all-containers: Alle Container
|
||||
host: Host
|
||||
@@ -27,16 +27,9 @@ label:
|
||||
avg-mem: Avg. MEM (%)
|
||||
pinned: Angepinnt
|
||||
per-page: Zeilen pro Seite
|
||||
swarm-mode: Swarm Modus
|
||||
services: Service
|
||||
running: Laufende
|
||||
host-menu: Hosts und Container
|
||||
swarm-menu: Services und Stacks
|
||||
group-menu: Benutzerdefinierte Gruppen
|
||||
total-cpu-usage: Gesamte CPU Auslastung
|
||||
total-mem-usage: Gesamte Speicher Auslastung
|
||||
dozzle-version: Dozzle Version
|
||||
all: Alle
|
||||
tooltip:
|
||||
search: Suche Container (⌘ + k, ⌃k)
|
||||
pin-column: Als Spalte anheften
|
||||
@@ -51,13 +44,11 @@ error:
|
||||
events-stream:
|
||||
title: Unerwarteter Fehler
|
||||
message: >-
|
||||
Das Dozzle UI konnte keine Verbindung zur API herstellen. Bitte überprüfen Sie Ihre Netzwerkeinstellungen.
|
||||
Wenn Sie einen Reverse-Proxy verwenden, stellen Sie sicher, dass er korrekt konfiguriert ist.
|
||||
Das Dozzle UI konnte keine Verbindung zur API herstellen. Bitte überprüfen Sie Ihre Netzwerkeinstellungen. Wenn Sie einen Reverse-Proxy verwenden, stellen Sie sicher, dass er korrekt konfiguriert ist.
|
||||
events-timeout:
|
||||
title: Etwas stimmt nicht
|
||||
message: >-
|
||||
Das Dozzle UI hat beim Verbinden mit der API eine Zeitüberschreitung. Bitte überprüfen Sie die
|
||||
Netzwerkverbindung und versuchen Sie es erneut.
|
||||
Das Dozzle UI hat beim Verbinden mit der API eine Zeitüberschreitung. Bitte überprüfen Sie die Netzwerkverbindung und versuchen Sie es erneut.
|
||||
alert:
|
||||
redirected:
|
||||
title: Zu neuem Container umgeleitet
|
||||
@@ -65,8 +56,7 @@ alert:
|
||||
similar-container-found:
|
||||
title: Ähnlicher Container gefunden
|
||||
message: >-
|
||||
Dozzle hat einen ähnlichen Container {containerId} gefunden, der auf dem
|
||||
gleichen Host ausgeführt wird. Möchtest Du zu ihm wechseln?
|
||||
Dozzle hat einen ähnlichen Container {containerId} gefunden, der auf dem gleichen Host ausgeführt wird. Möchtest Du zu ihm wechseln?
|
||||
title:
|
||||
page-not-found: Seite nicht gefunden
|
||||
login: Authentifizierung erforderlich
|
||||
@@ -88,9 +78,6 @@ settings:
|
||||
show-timesamps: Zeige Zeitstempel
|
||||
soft-wrap: Zeilenumbruch
|
||||
datetime-format: Datums- und Zeitformat
|
||||
12-24-format: >-
|
||||
Standardmäßig verwendet Dozzle die Spracheinstellungen des Browsers, um die
|
||||
Zeit anzuzeigen. Sie können das 12- oder 24-Stunden-Format erzwingen.
|
||||
font-size: Schriftgröße für Logs
|
||||
color-scheme: Farbschema
|
||||
options: Optionen
|
||||
|
||||
+3
-15
@@ -13,14 +13,9 @@ label:
|
||||
containers: Contenedores
|
||||
container: No contenedores | 1 contenedor | {count} contenedores
|
||||
service: Sin servicios | 1 servicio | {count} servicios
|
||||
services: Servicios
|
||||
running-containers: Contenedores en ejecución
|
||||
all-containers: Todos los contenedores
|
||||
total-containers: Contenedores Totales
|
||||
running: En ejecución
|
||||
total-cpu-usage: Uso total del Procesador
|
||||
total-mem-usage: Uso total de la Memoria
|
||||
dozzle-version: Versión de Dozzle
|
||||
all: Todo
|
||||
host: Host
|
||||
hosts: Hosts
|
||||
password: Contraseña
|
||||
@@ -32,8 +27,6 @@ label:
|
||||
avg-mem: Promedio de MEM (%)
|
||||
pinned: Fijado
|
||||
per-page: Filas por página
|
||||
swarm-mode: Modo Swarm
|
||||
services: Servicios
|
||||
host-menu: Hosts y Contenedores
|
||||
swarm-menu: Servicios y Stacks
|
||||
group-menu: Grupos Personalizados
|
||||
@@ -51,8 +44,7 @@ error:
|
||||
events-stream:
|
||||
title: Error inesperado
|
||||
message: >-
|
||||
Dozzle UI no pudo conectar con la API. Por favor, compruebe la configuración de su red.
|
||||
Si está utilizando un proxy inverso, por favor, asegúrese de que está configurado correctamente.
|
||||
Dozzle UI no pudo conectar con la API. Por favor, compruebe la configuración de su red. Si está utilizando un proxy inverso, por favor, asegúrese de que está configurado correctamente.
|
||||
events-timeout:
|
||||
title: Algo no está bien
|
||||
message: >-
|
||||
@@ -86,9 +78,6 @@ settings:
|
||||
show-timesamps: Mostrar marcas de tiempo
|
||||
soft-wrap: Líneas de texto con ajuste suave
|
||||
datetime-format: Sobrescribir el formato de fecha y hora
|
||||
12-24-format: >-
|
||||
Por defecto, Dozzle utilizará la configuración regional de tu navegador para formatear la hora. Usted puede
|
||||
forzar el estilo de 12 o 24 horas.
|
||||
font-size: Tamaño de letra a utilizar para los registros
|
||||
color-scheme: Esquema de colores
|
||||
options: Opciones
|
||||
@@ -97,8 +86,7 @@ settings:
|
||||
search: Activar la búsqueda con Dozzle mediante
|
||||
using-version: Estás usando Dozzle {version}.
|
||||
update-available: >-
|
||||
¡La nueva versión está disponible! Actualizar a la
|
||||
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
¡La nueva versión está disponible! Actualizar a la <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Mostrar etiquetas de salida estándar y salida de error estándar
|
||||
automatic-redirect: Redireccionar automáticamente a nuevos contenedores con el mismo nombre
|
||||
compact: Modo compacto
|
||||
|
||||
+11
-22
@@ -7,10 +7,13 @@ toolbar:
|
||||
stop: Stop
|
||||
start: Démarrer
|
||||
restart: Redémarrer
|
||||
show-hostname: Show hostname
|
||||
show-container-name: Show container name
|
||||
label:
|
||||
containers: Conteneurs
|
||||
container: Pas de conteneur | 1 conteneur | {count} conteneurs
|
||||
service: Pas de service | 1 service | {count} services
|
||||
containers: Conteneurs
|
||||
services: Services
|
||||
running-containers: Conteneurs en execution
|
||||
all-containers: Tous les conteneurs
|
||||
host: Hôte
|
||||
@@ -24,12 +27,9 @@ label:
|
||||
avg-mem: Moyenne MEM (%)
|
||||
pinned: Epinglé
|
||||
per-page: Colonnes par page
|
||||
swarm-mode: Mode Swarm
|
||||
services: Services
|
||||
host-menu: Hôtes et Conteneurs
|
||||
swarm-menu: Services et Stacks
|
||||
group-menu: Groupes Personnalisés
|
||||
|
||||
tooltip:
|
||||
search: Recherche de conteneurs (⌘ + k, ⌃k)
|
||||
pin-column: Epinglé en colonne
|
||||
@@ -44,13 +44,11 @@ error:
|
||||
events-stream:
|
||||
title: Erreur inattendue
|
||||
message: >-
|
||||
L'interface utilisateur de Dozzle n'a pas pu se connecter à l'API. Veuillez vérifier les paramètres de votre réseau.
|
||||
Si vous utilisez un proxy inversé, assurez-vous qu'il est configuré correctement.
|
||||
L'interface utilisateur de Dozzle n'a pas pu se connecter à l'API. Veuillez vérifier les paramètres de votre réseau. Si vous utilisez un proxy inversé, assurez-vous qu'il est configuré correctement.
|
||||
events-timeout:
|
||||
title: Quelque chose ne va pas
|
||||
message: >-
|
||||
La connexion de l'interface utilisateur de Dozzle à l'API a mis trop de temps. Veuillez vérifier la connexion
|
||||
et réessayez.
|
||||
La connexion de l'interface utilisateur de Dozzle à l'API a mis trop de temps. Veuillez vérifier la connexion et réessayez.
|
||||
alert:
|
||||
redirected:
|
||||
title: Redirigé vers le nouveau conteneur
|
||||
@@ -58,8 +56,7 @@ alert:
|
||||
similar-container-found:
|
||||
title: Conteneur similaire trouvé
|
||||
message: >-
|
||||
Dozzle a trouvé un conteneur similaire {containerId} qui s'exécute sur le même hôte
|
||||
et basculera automatiquement vers lui à moins que vous ne cliquiez sur "Annuler".
|
||||
Dozzle a trouvé un conteneur similaire {containerId} qui s'exécute sur le même hôte et basculera automatiquement vers lui à moins que vous ne cliquiez sur "Annuler".
|
||||
title:
|
||||
page-not-found: Page non trouvée
|
||||
login: Authentification requise
|
||||
@@ -69,6 +66,8 @@ button:
|
||||
logout: Déconnexion
|
||||
login: Connexion
|
||||
settings: Paramètres
|
||||
cancel: Cancel
|
||||
redirect: Redirect
|
||||
placeholder:
|
||||
search-containers: Recherche de conteneurs (⌘ + k, ⌃k)
|
||||
search: Recherche
|
||||
@@ -87,11 +86,10 @@ settings:
|
||||
search: Activer la recherche avec Dozzle en utilisant
|
||||
using-version: Vous utilisez Dozzle {version}.
|
||||
update-available: >-
|
||||
Nouvelle version disponible! Mise à jour de <a href="{href}" target="_blank"
|
||||
rel="noreferrer noopener">{nextVersion}</a>.
|
||||
Nouvelle version disponible! Mise à jour de <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Afficher les étiquettes stdout et stderr
|
||||
automatic-redirect: Redirection automatique vers de nouveaux conteneurs portant le même nom
|
||||
cancel: Annuler
|
||||
compact: Activer le mode compact pour les journaux
|
||||
log:
|
||||
preview: Voici un aperçu des logs
|
||||
warning: Un log d'avertissement ressemble à ceci
|
||||
@@ -101,15 +99,6 @@ settings:
|
||||
start-line: Ceci est un message d'erreur multiligne
|
||||
middle-line: avec une deuxième ligne
|
||||
end-line: et enfin une troisième ligne.
|
||||
compact: Activer le mode compact pour les journaux
|
||||
releases:
|
||||
features: une nouvelle fonctionnalité | {count} fonctionnalités
|
||||
bugFixes: un correctif | {count} correctifs
|
||||
breaking: un changement majeur | {count} changements majeurs
|
||||
three_parts: "{first}, {second} et {third}"
|
||||
two_parts: "{first} avec {second}"
|
||||
latest: Plus récent
|
||||
no_releases: Vous avez la dernière version
|
||||
releases:
|
||||
features: une nouvelle fonctionnalité | {count} fonctionnalités
|
||||
bugFixes: correction d'un bug | {count} bugs
|
||||
|
||||
+19
-32
@@ -11,14 +11,11 @@ toolbar:
|
||||
show-container-name: Mostra nome container
|
||||
label:
|
||||
containers: Container
|
||||
container: No containers | 1 container | {count} containers
|
||||
service: No services | 1 service | {count} services
|
||||
services: Services
|
||||
running-containers: Container in esecuzione
|
||||
all-containers: Tutti i Container
|
||||
total-containers: Container Totali
|
||||
running: In esecuzione
|
||||
total-cpu-usage: Utilizzo Totale della CPU
|
||||
total-mem-usage: Utilizzo Totale della Memoria
|
||||
dozzle-version: Versione Dozzle
|
||||
all: Tutto
|
||||
host: Host
|
||||
hosts: Hosts
|
||||
password: Password
|
||||
@@ -29,15 +26,16 @@ label:
|
||||
avg-cpu: CPU (%)
|
||||
avg-mem: MEM (%)
|
||||
pinned: Spuntato
|
||||
per-page: Rows per page
|
||||
host-menu: Host e Container
|
||||
swarm-menu: Servizi e Stack
|
||||
group-menu: Gruppi Personalizzati
|
||||
tooltip:
|
||||
search: Ricerca container (⌘ + k, ⌃k)
|
||||
pin-column: Blocca come colonna
|
||||
merge-services: Unisci tutti i servizi in una vista
|
||||
merge-containers: Unisci tutti i container in una vista
|
||||
merge-hosts: Unisci tutti i container su questo host in una vista
|
||||
search: Ricerca container (⌘ + k, ⌃k)
|
||||
pin-column: Blocca come colonna
|
||||
error:
|
||||
page-not-found: Questa pagina non esiste
|
||||
invalid-auth: Username o password non valide
|
||||
@@ -46,29 +44,8 @@ error:
|
||||
events-stream:
|
||||
title: Errore inaspettato
|
||||
message: >-
|
||||
Dozzle UI non è stato in grado di connettersi alle API. Controlla la tua configurazione di rete.
|
||||
Se stai utilizzando un reverse proxy, assicurati di averlo configurato correttamente.
|
||||
Dozzle UI non è stato in grado di connettersi alle API. Controlla la tua configurazione di rete. Se stai utilizzando un reverse proxy, assicurati di averlo configurato correttamente.
|
||||
events-timeout:
|
||||
alert:
|
||||
redirected:
|
||||
title: Reindirizzato a nuovo container
|
||||
message: Dozzle ti ha automaticamente reindirizzato al nuovo container {containerId}.
|
||||
similar-container-found:
|
||||
title: Trovato container simile
|
||||
message: >-
|
||||
Dozzle ha trovato un container simile {containerId} in esecuzione sullo stesso
|
||||
host e passerà automaticamente ad esso a meno che non si faccia clic su 'Annulla'.
|
||||
|
||||
settings:
|
||||
log:
|
||||
preview: Questa è un'anteprima dei log
|
||||
warning: Un log di avviso appare così
|
||||
complex: Questa è una voce di log complessa in formato json
|
||||
simple: Questo è un messaggio molto lungo che normalmente andrebbe a capo. La disattivazione del soft wrap disabiliterebbe questo comportamento. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
multi-line-error:
|
||||
start-line: Questo è un messaggio di errore multilinea
|
||||
middle-line: con una seconda riga
|
||||
end-line: e infine una terza riga.
|
||||
title: Qualcosa non è corretto
|
||||
message: >-
|
||||
Dozzle UI timed out durante la connessione alle API. Controlla la configurazione di rete e riprova.
|
||||
@@ -89,6 +66,8 @@ button:
|
||||
logout: Logout
|
||||
login: Login
|
||||
settings: Configurazione
|
||||
cancel: Cancel
|
||||
redirect: Redirect
|
||||
placeholder:
|
||||
search-containers: Ricerca container (⌘ + k, ⌃k)
|
||||
search: Cerca
|
||||
@@ -107,11 +86,19 @@ settings:
|
||||
search: Abilita la ricerca attraverso Dozzle
|
||||
using-version: Stai utilizzando Dozzle {version}.
|
||||
update-available: >-
|
||||
Nuova versione disponibile! Aggiorna a <a href="{href}" target="_blank"
|
||||
rel="noreferrer noopener">{nextVersion}</a>.
|
||||
Nuova versione disponibile! Aggiorna a <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Visualizza stdout e stderr
|
||||
automatic-redirect: Automaticamente reindirizza al nuovo container con lo stesso nome
|
||||
compact: Abilita il compact mode per i log
|
||||
log:
|
||||
preview: This is a preview of the logs
|
||||
warning: A warning log looks like this
|
||||
complex: This is a complex log entry as json
|
||||
simple: This is a very very long message which would wrap by default. Disabling soft wraps would disable this. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
multi-line-error:
|
||||
start-line: This is a multi line error message
|
||||
middle-line: with a second line
|
||||
end-line: and finally third line.
|
||||
releases:
|
||||
features: nuova funzione | {count} funzioni
|
||||
bugFixes: nuovo bug fix | {count} fix
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
toolbar:
|
||||
clear: Limpar
|
||||
download: Baixar
|
||||
search: Pesquisar
|
||||
show: Mostrar {std}
|
||||
show-all: Mostrar tudo
|
||||
stop: Parar
|
||||
start: Iniciar
|
||||
restart: Reiniciar
|
||||
show-hostname: Mostrar nome do host
|
||||
show-container-name: Mostrar nome do container
|
||||
label:
|
||||
containers: Contêineres
|
||||
container: Nenhum container | 1 container | {count} containers
|
||||
service: Nenhum serviço | 1 serviço | {count} serviços
|
||||
services: Serviços
|
||||
running-containers: Containers em Execução
|
||||
all-containers: Todos os Containers
|
||||
host: Host
|
||||
hosts: Hosts
|
||||
password: Senha
|
||||
username: Usuário
|
||||
container-name: Nome do Container
|
||||
status: Status
|
||||
created: Criado
|
||||
avg-cpu: Média CPU (%)
|
||||
avg-mem: Média MEM (%)
|
||||
pinned: Fixado
|
||||
per-page: Linhas por página
|
||||
host-menu: Hosts e Containers
|
||||
swarm-menu: Serviços e Stacks
|
||||
group-menu: Grupos Personalizados
|
||||
tooltip:
|
||||
search: Pesquisar containers (⌘ + k, ⌃k)
|
||||
pin-column: Fixar como coluna
|
||||
merge-services: Mesclar todos os serviços em uma visualização
|
||||
merge-containers: Mesclar todos os containers em uma visualização
|
||||
merge-hosts: Mesclar todos os containers neste host em uma visualização
|
||||
error:
|
||||
page-not-found: Esta página não existe
|
||||
invalid-auth: Usuário ou senha inválidos
|
||||
logs-skipped: \{total} entradas ignoradas
|
||||
container-not-found: Container não encontrado
|
||||
events-stream:
|
||||
title: Erro Inesperado
|
||||
message: >-
|
||||
A interface do Dozzle não conseguiu se conectar à API. Verifique suas configurações de rede. Se você estiver usando um proxy reverso, certifique-se de que ele esteja configurado corretamente.
|
||||
events-timeout:
|
||||
title: Algo não está certo
|
||||
message: >-
|
||||
A interface do Dozzle atingiu o tempo limite ao conectar-se à API. Verifique a conexão de rede e tente novamente.
|
||||
alert:
|
||||
redirected:
|
||||
title: Redirecionado para novo container
|
||||
message: O Dozzle redirecionou você automaticamente para o novo container {containerId}.
|
||||
similar-container-found:
|
||||
title: Container similar encontrado
|
||||
message: >-
|
||||
O Dozzle encontrou um container similar {containerId} que está em execução no mesmo host e mudará automaticamente para ele, a menos que você clique em 'Cancelar'.
|
||||
title:
|
||||
page-not-found: Página não encontrada
|
||||
login: Autenticação Requerida
|
||||
dashboard: 1 container | {count} containers
|
||||
settings: Configurações
|
||||
button:
|
||||
logout: Sair
|
||||
login: Entrar
|
||||
settings: Configurações
|
||||
cancel: Cancelar
|
||||
redirect: Redirecionar
|
||||
placeholder:
|
||||
search-containers: Pesquisar containers (⌘ + k, ⌃k)
|
||||
search: Pesquisar
|
||||
settings:
|
||||
display: Exibição
|
||||
locale: Sobrescrever idioma
|
||||
small-scrollbars: Usar barras de rolagem menores
|
||||
show-timesamps: Mostrar marcas de tempo
|
||||
soft-wrap: Quebrar linhas automaticamente
|
||||
datetime-format: Sobrescrever formato de data e hora
|
||||
font-size: Tamanho da fonte para logs
|
||||
color-scheme: Esquema de cores
|
||||
options: Opções
|
||||
show-stopped-containers: Mostrar containers parados
|
||||
about: Sobre
|
||||
search: Habilitar pesquisa com Dozzle usando
|
||||
using-version: Você está usando Dozzle {version}.
|
||||
update-available: >-
|
||||
Nova versão disponível! Atualize para <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Mostrar rótulos stdout e stderr
|
||||
automatic-redirect: Redirecionar automaticamente para novos containers com o mesmo nome
|
||||
compact: Ativar modo compacto para logs
|
||||
log:
|
||||
preview: Esta é uma prévia dos logs
|
||||
warning: Um log de aviso se parece com isto
|
||||
complex: Esta é uma entrada de log complexa como json
|
||||
simple: Esta é uma mensagem muito longa que seria quebrada por padrão. Desabilitar quebras suaves desabilitaria isso. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
multi-line-error:
|
||||
start-line: Esta é uma mensagem de erro multi-linha
|
||||
middle-line: com uma segunda linha
|
||||
end-line: e finalmente a terceira linha.
|
||||
releases:
|
||||
features: uma nova funcionalidade | {count} funcionalidades
|
||||
bugFixes: uma correção de bug | {count} correções
|
||||
breaking: uma mudança incompatível | {count} mudanças incompatíveis
|
||||
three_parts: "{first}, {second} e {third}"
|
||||
two_parts: "{first} com {second}"
|
||||
latest: Mais recente
|
||||
no_releases: Você tem a versão mais recente
|
||||
log_actions:
|
||||
copy_log: Copiar log
|
||||
jump_to_context: Ir para contexto
|
||||
toasts:
|
||||
copied:
|
||||
title: Copiado
|
||||
message: Log copiado para a área de transferência
|
||||
+32
-13
@@ -1,21 +1,21 @@
|
||||
toolbar:
|
||||
clear: Очистить
|
||||
show-hostname: Показать имя хоста
|
||||
show-container-name: Показать имя контейнера
|
||||
download: Скачать
|
||||
search: Поиск
|
||||
show: Показать только {std}
|
||||
show-all: Показать все потоки
|
||||
stop: Stop
|
||||
start: Start
|
||||
restart: Restart
|
||||
show-hostname: Показать имя хоста
|
||||
show-container-name: Показать имя контейнера
|
||||
label:
|
||||
containers: Контейнеры
|
||||
container: No containers | 1 container | {count} containers
|
||||
service: No services | 1 service | {count} services
|
||||
services: Services
|
||||
running-containers: Запущенные контейнеры
|
||||
all-containers: Все контейнеры
|
||||
total-containers: Всего Контейнеров
|
||||
running: Запущенные
|
||||
total-cpu-usage: Использование процессора
|
||||
total-mem-usage: Использование памяти
|
||||
dozzle-version: Версия Dozzle
|
||||
all: Все
|
||||
host: Хост
|
||||
hosts: Хосты
|
||||
password: Пароль
|
||||
@@ -25,12 +25,17 @@ label:
|
||||
created: Создан
|
||||
avg-cpu: средний процессор (%)
|
||||
avg-mem: средняя память (%)
|
||||
pinned: Pinned
|
||||
per-page: Rows per page
|
||||
host-menu: Хосты и Контейнеры
|
||||
swarm-menu: Сервисы и Стеки
|
||||
group-menu: Пользовательские Группы
|
||||
tooltip:
|
||||
search: Поиск контейнеров (⌘ + k, ⌃k)
|
||||
pin-column: Закрепить столбец
|
||||
merge-services: Merge all services into one view
|
||||
merge-containers: Merge all containers into one view
|
||||
merge-hosts: Merge all containers on this host into one view
|
||||
error:
|
||||
page-not-found: Эта страница не доступна.
|
||||
invalid-auth: Имя пользователя или пароль неверны.
|
||||
@@ -39,8 +44,7 @@ error:
|
||||
events-stream:
|
||||
title: Неожиданная ошибка
|
||||
message: >-
|
||||
Dozzle UI не смог подключиться к API. Пожалуйста, проверьте настройки сети.
|
||||
Если вы используете обратный прокси, убедитесь, что он настроен правильно.
|
||||
Dozzle UI не смог подключиться к API. Пожалуйста, проверьте настройки сети. Если вы используете обратный прокси, убедитесь, что он настроен правильно.
|
||||
events-timeout:
|
||||
title: Что-то не так
|
||||
message: >-
|
||||
@@ -62,6 +66,8 @@ button:
|
||||
logout: Выйти
|
||||
login: Войти
|
||||
settings: Настройки
|
||||
cancel: Cancel
|
||||
redirect: Redirect
|
||||
placeholder:
|
||||
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
|
||||
search: Поиск
|
||||
@@ -71,13 +77,11 @@ settings:
|
||||
small-scrollbars: Уменьшенная полоса прокрутки
|
||||
show-timesamps: Показывать временные метки
|
||||
soft-wrap: Плавный перенос текста
|
||||
12-24-format: >-
|
||||
Формат времени. По умолчанию, Dozzle будет использовать локализацию вашего браузера.
|
||||
datetime-format: Формат даты и времени
|
||||
font-size: Размер шрифта
|
||||
color-scheme: Цветовая схема
|
||||
options: Опции
|
||||
show-stopped-containers: Показывать остановленные контейнеры
|
||||
datetime-format: Формат даты и времени
|
||||
about: Информация
|
||||
search: Включить поиск с помощью Dozzle, используя
|
||||
using-version: Вы используете версию Dozzle {version}.
|
||||
@@ -95,3 +99,18 @@ settings:
|
||||
start-line: Это многострочное сообщение об ошибке
|
||||
middle-line: со второй строкой
|
||||
end-line: и наконец третьей строкой.
|
||||
releases:
|
||||
features: one new feature | {count} features
|
||||
bugFixes: one bug fix | {count} fixes
|
||||
breaking: one breaking change | {count} breaking changes
|
||||
three_parts: "{first}, {second} and {third}"
|
||||
two_parts: "{first} with {second}"
|
||||
latest: Latest
|
||||
no_releases: You have the latest version
|
||||
log_actions:
|
||||
copy_log: Copy log
|
||||
jump_to_context: Jump to context
|
||||
toasts:
|
||||
copied:
|
||||
title: Copied
|
||||
message: Log copied to clipboard
|
||||
|
||||
+4
-12
@@ -10,17 +10,12 @@ toolbar:
|
||||
show-hostname: 显示主机名
|
||||
show-container-name: 显示容器名称
|
||||
label:
|
||||
service: 无服务 | 1 个服务 | {count} 个服务
|
||||
total-containers: 容器总数
|
||||
total-cpu-usage: CPU总使用率
|
||||
total-mem-usage: 内存总使用率
|
||||
containers: 容器
|
||||
container: 无容器 | 1 容器 | {count} 容器
|
||||
running: 运行中
|
||||
dozzle-version: Dozzle 版本
|
||||
service: 无服务 | 1 个服务 | {count} 个服务
|
||||
services: 服务
|
||||
running-containers: 运行中的容器
|
||||
all-containers: 所有容器
|
||||
all: 全部
|
||||
host: 主机
|
||||
hosts: 主机
|
||||
password: 密码
|
||||
@@ -32,9 +27,6 @@ label:
|
||||
avg-mem: 平均MEM (%)
|
||||
pinned: 固定
|
||||
per-page: 每页行数
|
||||
swarm-mode: 集群模式
|
||||
services: 服务
|
||||
custom-groups: 自定义组
|
||||
host-menu: 主机和容器
|
||||
swarm-menu: 服务和堆栈
|
||||
group-menu: 自定义组
|
||||
@@ -43,6 +35,7 @@ tooltip:
|
||||
pin-column: 固定为列
|
||||
merge-services: 合并所有服务到一个视图
|
||||
merge-containers: 合并所有容器到一个视图
|
||||
merge-hosts: Merge all containers on this host into one view
|
||||
error:
|
||||
page-not-found: 此页面不存在。
|
||||
invalid-auth: 用户名和密码无效。
|
||||
@@ -51,8 +44,7 @@ error:
|
||||
events-stream:
|
||||
title: 意外错误
|
||||
message: >-
|
||||
Dozzle UI无法连接到API。请检查您的网络设置。
|
||||
如果您使用反向代理,请确保其正确配置。
|
||||
Dozzle UI无法连接到API。请检查您的网络设置。 如果您使用反向代理,请确保其正确配置。
|
||||
events-timeout:
|
||||
title: 出了点问题
|
||||
message: >-
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"net"
|
||||
@@ -16,11 +15,11 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/healthcheck"
|
||||
"github.com/amir20/dozzle/internal/k8s"
|
||||
"github.com/amir20/dozzle/internal/support/cli"
|
||||
docker_support "github.com/amir20/dozzle/internal/support/docker"
|
||||
k8s_support "github.com/amir20/dozzle/internal/support/k8s"
|
||||
"github.com/amir20/dozzle/internal/web"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -36,119 +35,13 @@ func main() {
|
||||
cli.ValidateEnvVars(cli.Args{}, cli.AgentCmd{})
|
||||
args, subcommand := cli.ParseArgs()
|
||||
if subcommand != nil {
|
||||
switch subcommand.(type) {
|
||||
case *cli.AgentCmd:
|
||||
client, err := docker.NewLocalClient(args.Hostname)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not create docker client")
|
||||
}
|
||||
certs, err := cli.ReadCertificates(certs)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not read certificates")
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", args.Agent.Addr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to listen")
|
||||
}
|
||||
tempFile, err := os.CreateTemp("./", "agent-*.addr")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to create temp file")
|
||||
}
|
||||
io.WriteString(tempFile, listener.Addr().String())
|
||||
go cli.StartEvent(args, "", client, "agent")
|
||||
server, err := agent.NewServer(client, certs, args.Version(), args.Filter)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to create agent server")
|
||||
}
|
||||
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())
|
||||
|
||||
if err := server.Serve(listener); err != nil {
|
||||
log.Error().Err(err).Msg("failed to serve")
|
||||
}
|
||||
}()
|
||||
<-ctx.Done()
|
||||
stop()
|
||||
log.Info().Msg("Shutting down agent")
|
||||
server.Stop()
|
||||
log.Debug().Str("file", tempFile.Name()).Msg("Removing temp file")
|
||||
os.Remove(tempFile.Name())
|
||||
|
||||
case *cli.HealthcheckCmd:
|
||||
files, err := os.ReadDir(".")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to read directory")
|
||||
}
|
||||
|
||||
agentAddress := ""
|
||||
for _, file := range files {
|
||||
if match, _ := filepath.Match("agent-*.addr", file.Name()); match {
|
||||
data, err := os.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to read file")
|
||||
}
|
||||
agentAddress = string(data)
|
||||
break
|
||||
}
|
||||
}
|
||||
if agentAddress == "" {
|
||||
if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to make request")
|
||||
}
|
||||
} else {
|
||||
certs, err := cli.ReadCertificates(certs)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not read certificates")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
|
||||
defer cancel()
|
||||
if err := healthcheck.RPCRequest(ctx, agentAddress, certs); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to make request")
|
||||
}
|
||||
}
|
||||
|
||||
case *cli.GenerateCmd:
|
||||
cli.StartEvent(args, "", nil, "generate")
|
||||
if args.Generate.Username == "" || args.Generate.Password == "" {
|
||||
log.Fatal().Msg("Username and password are required")
|
||||
}
|
||||
|
||||
buffer := auth.GenerateUsers(auth.User{
|
||||
Username: args.Generate.Username,
|
||||
Password: args.Generate.Password,
|
||||
Name: args.Generate.Name,
|
||||
Email: args.Generate.Email,
|
||||
Filter: args.Generate.Filter,
|
||||
}, true)
|
||||
|
||||
if _, err := os.Stdout.Write(buffer.Bytes()); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to write to stdout")
|
||||
}
|
||||
|
||||
case *cli.AgentTestCmd:
|
||||
certs, err := cli.ReadCertificates(certs)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not read certificates")
|
||||
}
|
||||
|
||||
log.Info().Str("endpoint", args.AgentTest.Address).Msg("Connecting to agent")
|
||||
|
||||
agent, err := agent.NewClient(args.AgentTest.Address, certs)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Str("endpoint", args.AgentTest.Address).Msg("error connecting to agent")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
|
||||
defer cancel()
|
||||
host, err := agent.Host(ctx)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Str("endpoint", args.AgentTest.Address).Msg("error fetching host info for agent")
|
||||
}
|
||||
|
||||
log.Info().Str("endpoint", args.AgentTest.Address).Str("version", host.AgentVersion).Str("name", host.Name).Str("id", host.ID).Msg("Successfully connected to agent")
|
||||
runnable, ok := subcommand.(cli.Runnable)
|
||||
if !ok {
|
||||
log.Fatal().Msg("Invalid command")
|
||||
}
|
||||
err := runnable.Run(args, certs)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to run command")
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
@@ -160,17 +53,16 @@ func main() {
|
||||
|
||||
log.Info().Msgf("Dozzle version %s", args.Version())
|
||||
|
||||
var multiHostService *docker_support.MultiHostService
|
||||
var hostService web.HostService
|
||||
if args.Mode == "server" {
|
||||
var localClient container.Client
|
||||
localClient, multiHostService = cli.CreateMultiHostService(certs, args)
|
||||
|
||||
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")
|
||||
}
|
||||
go cli.StartEvent(args, "server", localClient, "")
|
||||
|
||||
hostService = multiHostService
|
||||
} else if args.Mode == "swarm" {
|
||||
localClient, err := docker.NewLocalClient("")
|
||||
if err != nil {
|
||||
@@ -182,7 +74,7 @@ func main() {
|
||||
}
|
||||
agentManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs)
|
||||
manager := docker_support.NewSwarmClientManager(localClient, certs, args.Timeout, agentManager, args.Filter)
|
||||
multiHostService = docker_support.NewMultiHostService(manager, args.Timeout)
|
||||
hostService = docker_support.NewMultiHostService(manager, args.Timeout)
|
||||
log.Info().Msg("Starting in swarm mode")
|
||||
listener, err := net.Listen("tcp", ":7007")
|
||||
if err != nil {
|
||||
@@ -194,16 +86,29 @@ func main() {
|
||||
}
|
||||
go cli.StartEvent(args, "swarm", localClient, "")
|
||||
go func() {
|
||||
log.Info().Msgf("Dozzle agent version %s", args.Version())
|
||||
log.Info().Msgf("Dozzle agent version in swarm mode %s", args.Version())
|
||||
if err := server.Serve(listener); err != nil {
|
||||
log.Error().Err(err).Msg("failed to serve")
|
||||
}
|
||||
}()
|
||||
} else if args.Mode == "k8s" {
|
||||
localClient, err := k8s.NewK8sClient(args.Namespace)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not create k8s client")
|
||||
}
|
||||
|
||||
clusterService, err := k8s_support.NewK8sClusterService(localClient, args.Timeout)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not create k8s cluster service")
|
||||
}
|
||||
|
||||
go cli.StartEvent(args, "k8s", localClient, "")
|
||||
hostService = clusterService
|
||||
} else {
|
||||
log.Fatal().Str("mode", args.Mode).Msg("Invalid mode")
|
||||
}
|
||||
|
||||
srv := createServer(args, multiHostService)
|
||||
srv := createServer(args, hostService)
|
||||
go func() {
|
||||
log.Info().Msgf("Accepting connections on %s", args.Addr)
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
@@ -224,7 +129,7 @@ func main() {
|
||||
log.Debug().Msg("shut down complete")
|
||||
}
|
||||
|
||||
func createServer(args cli.Args, multiHostService *docker_support.MultiHostService) *http.Server {
|
||||
func createServer(args cli.Args, hostService web.HostService) *http.Server {
|
||||
_, dev := os.LookupEnv("DEV")
|
||||
|
||||
var provider web.AuthProvider = web.NONE
|
||||
@@ -286,7 +191,7 @@ func createServer(args cli.Args, multiHostService *docker_support.MultiHostServi
|
||||
TTL: authTTL,
|
||||
},
|
||||
EnableActions: args.EnableActions,
|
||||
Filter: args.Filter,
|
||||
Labels: args.Filter,
|
||||
}
|
||||
|
||||
assets, err := fs.Sub(content, "dist")
|
||||
@@ -313,5 +218,5 @@ func createServer(args cli.Args, multiHostService *docker_support.MultiHostServi
|
||||
}
|
||||
}
|
||||
|
||||
return web.CreateServer(multiHostService, assets, config)
|
||||
return web.CreateServer(hostService, assets, config)
|
||||
}
|
||||
|
||||
+30
-30
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "8.10.7",
|
||||
"version": "8.11.7",
|
||||
"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.2.1",
|
||||
"packageManager": "pnpm@10.5.2",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -30,22 +30,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@duckdb/duckdb-wasm": "1.29.0",
|
||||
"@iconify-json/carbon": "^1.2.6",
|
||||
"@iconify-json/carbon": "^1.2.8",
|
||||
"@iconify-json/cil": "^1.2.2",
|
||||
"@iconify-json/ic": "^1.2.2",
|
||||
"@iconify-json/material-symbols": "^1.2.14",
|
||||
"@iconify-json/material-symbols": "^1.2.15",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/mdi-light": "^1.2.2",
|
||||
"@iconify-json/octicon": "^1.2.4",
|
||||
"@iconify-json/octicon": "^1.2.5",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||
"@oddbird/css-anchor-positioning": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "4.0.5",
|
||||
"@vueuse/components": "^12.5.0",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/integrations": "^12.5.0",
|
||||
"@vueuse/router": "^12.5.0",
|
||||
"@tailwindcss/vite": "4.0.12",
|
||||
"@vueuse/components": "^12.8.2",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@vueuse/integrations": "^12.8.2",
|
||||
"@vueuse/router": "^12.8.2",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-ease": "^3.0.1",
|
||||
@@ -53,34 +53,34 @@
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"daisyui": "5.0.0-beta.7",
|
||||
"daisyui": "5.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"entities": "^6.0.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"pinia": "^2.3.1",
|
||||
"pinia": "^3.0.1",
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "^3.1.8",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tailwindcss": "4.0.5",
|
||||
"unplugin-auto-import": "^19.0.0",
|
||||
"unplugin-icons": "^22.0.0",
|
||||
"unplugin-vue-components": "^28.0.0",
|
||||
"unplugin-vue-macros": "^2.14.2",
|
||||
"unplugin-vue-router": "^0.11.2",
|
||||
"vite": "6.1.0",
|
||||
"tailwindcss": "4.0.12",
|
||||
"unplugin-auto-import": "^19.1.1",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"unplugin-vue-macros": "^2.14.5",
|
||||
"unplugin-vue-router": "^0.12.0",
|
||||
"vite": "6.2.1",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitepress": "1.6.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apache-arrow/esnext-esm": "^19.0.0",
|
||||
"@pinia/testing": "^0.1.7",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@apache-arrow/esnext-esm": "^19.0.1",
|
||||
"@pinia/testing": "^1.0.0",
|
||||
"@playwright/test": "^1.51.0",
|
||||
"@types/d3-array": "^3.2.1",
|
||||
"@types/d3-ease": "^3.0.2",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
@@ -88,24 +88,24 @@
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/d3-transition": "^3.0.9",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.9",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"bumpp": "^10.0.2",
|
||||
"bumpp": "^10.0.3",
|
||||
"c8": "^10.1.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lint-staged": "^15.4.3",
|
||||
"prettier": "^3.5.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.5",
|
||||
"vue-component-type-helpers": "^2.2.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.8",
|
||||
"vue-component-type-helpers": "^2.2.8",
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css,ts,html,md}": [
|
||||
|
||||
Generated
+1177
-941
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user