Compare commits
129 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 | |||
| 6da36c4c0d | |||
| d9d93a8147 | |||
| eba87fa49e | |||
| 7ae409981c | |||
| 20fb7984d2 | |||
| 17c15b65a2 | |||
| 9ce736a448 | |||
| 10c95124e4 | |||
| 5e02142f77 | |||
| f9585cc656 | |||
| 6da7f6dffd | |||
| 2de739141d | |||
| 585e1edfee | |||
| bf81852003 | |||
| 5f73b41c57 | |||
| 9f7b17f4ec | |||
| 6c292be943 | |||
| 21ae6eda8b | |||
| b732844e7b | |||
| 1b38f37ac2 | |||
| c20ce29f5c | |||
| 47e64bb57a | |||
| b9a5262257 | |||
| 47204b33c4 | |||
| 162f9ea580 | |||
| 2381a8b8a3 | |||
| 8fbbfaf043 | |||
| 3d077b9503 | |||
| 68ea86c55b | |||
| ad5c1c4452 | |||
| 0efd98e999 | |||
| ff72431a72 | |||
| 6d2ed53629 | |||
| 992e1a27ae | |||
| 2378a1f115 | |||
| 3b122564d9 | |||
| a783f7060c | |||
| 76e5c63436 | |||
| 5a17a07b8f | |||
| d1fc9b750b | |||
| 87a59939dd | |||
| 3a159cb89b | |||
| 84ec46c400 | |||
| 0c4ca6d605 | |||
| db5d36c89b | |||
| 8cdacf608a | |||
| 78487a553c | |||
| 8da67482d8 | |||
| 60914f3208 | |||
| 0dc2a391df | |||
| 4cfd3d0cfa | |||
| 9752504097 |
@@ -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@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.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.11.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@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.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.11.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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:23.6.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.4-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
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ declare global {
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||
const useAnnouncements: typeof import('./stores/announcements')['useAnnouncements']
|
||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||
@@ -192,6 +193,7 @@ declare global {
|
||||
const useContainerActions: typeof import('./composable/containerActions')['useContainerActions']
|
||||
const useContainerStore: typeof import('./stores/container')['useContainerStore']
|
||||
const useContainerStream: typeof import('./composable/eventStreams')['useContainerStream']
|
||||
const useCountdown: typeof import('@vueuse/core')['useCountdown']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
@@ -287,7 +289,6 @@ declare global {
|
||||
const useProfileStorage: typeof import('./composable/profileStorage')['useProfileStorage']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useReleases: typeof import('./stores/releases')['useReleases']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
@@ -551,6 +552,7 @@ declare module 'vue' {
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useAnnouncements: UnwrapRef<typeof import('./stores/announcements')['useAnnouncements']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
@@ -581,6 +583,7 @@ declare module 'vue' {
|
||||
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
|
||||
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
|
||||
readonly useContainerStream: UnwrapRef<typeof import('./composable/eventStreams')['useContainerStream']>
|
||||
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
@@ -676,7 +679,6 @@ declare module 'vue' {
|
||||
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Announcements: typeof import('./components/Announcements.vue')['default']
|
||||
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
|
||||
'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']
|
||||
@@ -39,6 +42,7 @@ declare module 'vue' {
|
||||
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
|
||||
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
|
||||
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
|
||||
GroupMenu: typeof import('./components/GroupMenu.vue')['default']
|
||||
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
|
||||
HostList: typeof import('./components/HostList.vue')['default']
|
||||
HostLog: typeof import('./components/HostViewer/HostLog.vue')['default']
|
||||
@@ -99,7 +103,6 @@ declare module 'vue' {
|
||||
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
|
||||
Popup: typeof import('./components/Popup.vue')['default']
|
||||
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
|
||||
Releases: typeof import('./components/Releases.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<Dropdown class="dropdown-end" @closed="releaseSeen = mostRecent?.tag ?? config.version">
|
||||
<template #trigger>
|
||||
<mdi:announcement class="size-6 -rotate-12" />
|
||||
<template v-if="announcements.length > 0 && releaseSeen != mostRecent?.tag">
|
||||
<span class="bg-red absolute top-0 right-px size-2 animate-ping rounded-full opacity-75"></span>
|
||||
<span class="bg-red absolute top-0 right-px size-2 rounded-full"></span>
|
||||
</template>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="w-72">
|
||||
<ul class="space-y-4 p-2">
|
||||
<li v-for="release in announcements" v-if="announcements.length > 0">
|
||||
<template v-if="release.announcement">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<carbon:information class="text-info self-center" />
|
||||
<a
|
||||
:href="release.htmlUrl"
|
||||
class="link-primary text-lg font-bold"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ release.name }}
|
||||
</a>
|
||||
<span class="ml-1 text-xs"><distance-time :date="release.createdAt" /></span>
|
||||
</div>
|
||||
<div class="text-base-content/80 text-sm">
|
||||
{{ release.body }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<carbon:warning class="stroke-orange self-center" v-if="release.breaking > 0" />
|
||||
<a
|
||||
:href="release.htmlUrl"
|
||||
class="link-primary text-lg font-bold"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ release.name }}
|
||||
</a>
|
||||
<span class="ml-1 text-xs"><distance-time :date="release.createdAt" /></span>
|
||||
<Tag class="bg-red ml-auto px-1 py-1 text-xs" v-if="release.latest">
|
||||
{{ $t("releases.latest") }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="text-base-content/80 text-sm">
|
||||
{{ summary(release) }}
|
||||
</div>
|
||||
</template>
|
||||
</li>
|
||||
<li v-else>
|
||||
<div class="text-base-content/80 text-sm">
|
||||
{{ $t("releases.no_releases") }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAnnouncements } from "@/stores/announcements";
|
||||
|
||||
const { announcements, mostRecent } = useAnnouncements();
|
||||
const { t } = useI18n();
|
||||
|
||||
const releaseSeen = useProfileStorage("releaseSeen", config.version);
|
||||
|
||||
function summary(release: { features: number; bugFixes: number; breaking: number }) {
|
||||
if (release.features > 0 && release.bugFixes > 0 && release.breaking > 0) {
|
||||
return t("releases.three_parts", {
|
||||
first: t("releases.breaking", { count: release.breaking }),
|
||||
second: t("releases.features", { count: release.features }),
|
||||
third: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0 && release.bugFixes > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.features", { count: release.features }),
|
||||
second: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0 && release.breaking > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.features", { count: release.features }),
|
||||
second: t("releases.breaking", { count: release.breaking }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.bugFixes > 0 && release.breaking > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
second: t("releases.breaking", { count: release.breaking }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0) {
|
||||
return t("releases.features", { count: release.features });
|
||||
}
|
||||
|
||||
if (release.bugFixes > 0) {
|
||||
return t("releases.bugFixes", { count: release.bugFixes });
|
||||
}
|
||||
|
||||
if (release.breaking > 0) {
|
||||
return t("releases.breaking", { count: release.breaking });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -169,7 +169,7 @@ const downloadParams = computed(() =>
|
||||
|
||||
const downloadUrl = computed(() =>
|
||||
withBase(
|
||||
`/api/containers/${container.host}:${container.id}/download?${new URLSearchParams(downloadParams.value).toString()}`,
|
||||
`/api/containers/${container.host}~${container.id}/download?${new URLSearchParams(downloadParams.value).toString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<ul class="menu w-full p-0 text-[0.95rem]">
|
||||
<li v-if="customGroups.length > 0">
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:bounding-box-fill />
|
||||
{{ $t("label.group-menu") }}
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="group in customGroups" :key="group.name">
|
||||
<router-link :to="{ name: '/group/[name]', params: { name: group.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">
|
||||
{{ group.name }}
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const store = useSwarmStore();
|
||||
|
||||
const { customGroups } = storeToRefs(store);
|
||||
</script>
|
||||
<style scoped></style>
|
||||
@@ -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,
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<slot name="more-items"></slot>
|
||||
<Dropdown class="dropdown-end" @closed="latestTag = latest?.tag ?? config.version">
|
||||
<template #trigger>
|
||||
<mdi:announcement class="size-6 -rotate-12" />
|
||||
<span
|
||||
class="bg-red absolute top-0 right-px size-2 rounded-full"
|
||||
v-if="hasUpdate && latestTag != latest?.tag"
|
||||
></span>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="w-72">
|
||||
<Releases />
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Announcements />
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/settings' }"
|
||||
@@ -58,7 +45,4 @@ async function logout() {
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
const { hasUpdate, latest } = useReleases();
|
||||
const latestTag = useProfileStorage("releaseSeen", config.version);
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
|
||||
<LogStd :std="logEntry.std" class="shrink-0 select-none" v-if="showStd" />
|
||||
<RandomColorTag class="shrink-0 select-none" :value="host.name" v-if="showHostname" />
|
||||
<RandomColorTag class="shrink-0 select-none" :value="container.name" v-if="showContainerName" />
|
||||
<RandomColorTag class="shrink-0 select-none" :value="container.name" v-if="showContainerName" truncateRight />
|
||||
<LogDate :date="logEntry.date" v-if="showTimestamp" class="shrink-0 select-none" />
|
||||
<LogLevel
|
||||
class="flex select-none"
|
||||
|
||||
@@ -97,7 +97,7 @@ const downloadParams = computed(() =>
|
||||
|
||||
const downloadUrl = computed(() =>
|
||||
withBase(
|
||||
`/api/containers/${containers.value.map((c) => c.host + ":" + c.id).join(",")}/download?${new URLSearchParams(downloadParams.value).toString()}`,
|
||||
`/api/containers/${containers.value.map((c) => c.host + "~" + c.id).join(",")}/download?${new URLSearchParams(downloadParams.value).toString()}`,
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div class="tag grid w-40 overflow-hidden rounded-sm text-center text-sm text-white">
|
||||
<div class="random-color col-start-1 row-start-1 brightness-75"></div>
|
||||
<div class="col-start-1 row-start-1 truncate px-2 brightness-100 [direction:rtl]">{{ value }}</div>
|
||||
<div class="col-start-1 row-start-1 truncate px-2 brightness-100" :class="truncateRight ? '[direction:rtl]' : ''">
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
@@ -29,8 +31,9 @@ const colors = [
|
||||
] as const;
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
const { value } = defineProps<{
|
||||
const { value, truncateRight = false } = defineProps<{
|
||||
value: string;
|
||||
truncateRight?: boolean;
|
||||
}>();
|
||||
|
||||
const color = computed(() => colors[Math.abs(hashCode(value)) % colors.length]);
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<ul class="space-y-4 p-2">
|
||||
<li v-for="release in releases" v-if="releases?.length">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<carbon:warning class="stroke-orange self-center" v-if="release.breaking > 0" />
|
||||
<a :href="release.htmlUrl" class="link-primary text-lg font-bold" target="_blank" rel="noreferrer noopener">
|
||||
{{ release.name }}
|
||||
</a>
|
||||
<span class="ml-1 text-xs"><distance-time :date="new Date(release.createdAt)" /></span>
|
||||
<Tag class="bg-red ml-auto px-1 py-1 text-xs" v-if="release.tag === latest?.tag">
|
||||
{{ $t("releases.latest") }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="text-base-content/80 text-sm">
|
||||
{{ summary(release) }}
|
||||
</div>
|
||||
</li>
|
||||
<li v-else>
|
||||
<div class="text-base-content/80 text-sm">
|
||||
{{ $t("releases.no_releases") }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { releases, latest } = useReleases();
|
||||
const { t } = useI18n();
|
||||
|
||||
function summary(release: { features: number; bugFixes: number; breaking: number }) {
|
||||
if (release.features > 0 && release.bugFixes > 0 && release.breaking > 0) {
|
||||
return t("releases.three_parts", {
|
||||
first: t("releases.breaking", { count: release.breaking }),
|
||||
second: t("releases.features", { count: release.features }),
|
||||
third: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0 && release.bugFixes > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.features", { count: release.features }),
|
||||
second: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0 && release.breaking > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.features", { count: release.features }),
|
||||
second: t("releases.breaking", { count: release.breaking }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.bugFixes > 0 && release.breaking > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
second: t("releases.breaking", { count: release.breaking }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0) {
|
||||
return t("releases.features", { count: release.features });
|
||||
}
|
||||
|
||||
if (release.bugFixes > 0) {
|
||||
return t("releases.bugFixes", { count: release.bugFixes });
|
||||
}
|
||||
|
||||
if (release.breaking > 0) {
|
||||
return t("releases.breaking", { count: release.breaking });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<div v-if="ready" data-testid="side-menu" class="flex min-h-0 flex-col">
|
||||
<Carousel v-model="selectedCard" class="flex-1">
|
||||
<CarouselItem title="Hosts and Containers" id="host">
|
||||
<CarouselItem :title="$t('label.host-menu')" id="host">
|
||||
<HostMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem title="Services and Stacks" v-if="services.length > 0" id="swarm">
|
||||
<CarouselItem :title="$t('label.group-menu')" v-if="customGroups.length > 0" id="group">
|
||||
<GroupMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem :title="$t('label.swarm-menu')" v-if="services.length > 0" id="swarm">
|
||||
<SwarmMenu />
|
||||
</CarouselItem>
|
||||
</Carousel>
|
||||
@@ -20,16 +23,14 @@ const containerStore = useContainerStore();
|
||||
const { ready } = storeToRefs(containerStore);
|
||||
const route = useRoute();
|
||||
const swarmStore = useSwarmStore();
|
||||
const { services } = storeToRefs(swarmStore);
|
||||
const selectedCard = ref<"host" | "swarm">("host");
|
||||
const { services, customGroups } = storeToRefs(swarmStore);
|
||||
const selectedCard = ref<"host" | "swarm" | "group">("host");
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
if (route.meta.swarmMode) {
|
||||
selectedCard.value = "swarm";
|
||||
} else if (route.meta.containerMode) {
|
||||
selectedCard.value = "host";
|
||||
if (route.meta.menu && ["host", "swarm", "group"].includes(route.meta.menu as string)) {
|
||||
selectedCard.value = route.meta.menu as "host" | "swarm" | "group";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
||||
@@ -46,37 +46,13 @@
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<li v-if="customGroups.length > 0">
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:bounding-box-fill />
|
||||
{{ $t("label.custom-groups") }}
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="group in customGroups" :key="group.name">
|
||||
<router-link :to="{ name: '/group/[name]', params: { name: group.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">
|
||||
{{ group.name }}
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const store = useSwarmStore();
|
||||
|
||||
const { stacks, services, customGroups } = storeToRefs(store);
|
||||
const { stacks, services } = storeToRefs(store);
|
||||
|
||||
const servicesWithoutStacks = computed(() => services.value.filter((service) => !service.stack));
|
||||
</script>
|
||||
<style scoped>
|
||||
.menu {
|
||||
@apply text-[0.95rem];
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
@utility menu-active {
|
||||
&,
|
||||
&:active {
|
||||
--color-neutral: var(--color-primary);
|
||||
--menu-active-bg: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,5 +78,5 @@ watchEffect(() => {
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
containerMode: true
|
||||
menu: host
|
||||
</route>
|
||||
|
||||
@@ -22,5 +22,5 @@ watchEffect(() => {
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
swarmMode: true
|
||||
menu: group
|
||||
</route>
|
||||
|
||||
@@ -26,5 +26,5 @@ watchEffect(() => {
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
containerMode: true
|
||||
menu: host
|
||||
</route>
|
||||
|
||||
@@ -22,5 +22,5 @@ watchEffect(() => {
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
containerMode: true
|
||||
menu: host
|
||||
</route>
|
||||
|
||||
@@ -28,5 +28,5 @@ watchEffect(() => {
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
swarmMode: true
|
||||
menu: swarm
|
||||
</route>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<div>
|
||||
<span v-html="$t('settings.using-version', { version: config.version })"></span>
|
||||
<div
|
||||
v-if="hasUpdate"
|
||||
v-html="$t('settings.update-available', { nextVersion: latest?.name, href: latest?.htmlUrl })"
|
||||
v-if="hasRelease"
|
||||
v-html="$t('settings.update-available', { nextVersion: latestRelease?.name, href: latestRelease?.htmlUrl })"
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -154,7 +154,7 @@ import { availableLocales, i18n } from "@/modules/i18n";
|
||||
const { t } = useI18n();
|
||||
|
||||
setTitle(t("title.settings"));
|
||||
const { latest, hasUpdate } = useReleases();
|
||||
const { latestRelease, hasRelease } = useAnnouncements();
|
||||
|
||||
const now = new Date();
|
||||
const hoursAgo = (hours: number) => {
|
||||
|
||||
@@ -28,5 +28,5 @@ watchEffect(() => {
|
||||
</script>
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
swarmMode: true
|
||||
menu: swarm
|
||||
</route>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
type Announcement = {
|
||||
name: string;
|
||||
announcement: boolean;
|
||||
createdAt: Date;
|
||||
body: string;
|
||||
tag: string;
|
||||
htmlUrl: string;
|
||||
latest: boolean;
|
||||
mentionsCount: number;
|
||||
features: number;
|
||||
bugFixes: number;
|
||||
breaking: number;
|
||||
};
|
||||
|
||||
const { data: releases } = useFetch(withBase("/api/releases")).get().json<Announcement[]>();
|
||||
|
||||
const otherAnnouncements = [] as Announcement[];
|
||||
|
||||
const announcements = computed(() => {
|
||||
const newReleases =
|
||||
releases.value?.map((release) => ({ ...release, createdAt: new Date(release.createdAt), announcement: false })) ??
|
||||
[];
|
||||
return [...newReleases, ...otherAnnouncements].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
});
|
||||
|
||||
const mostRecent = computed(() => announcements.value?.[0]);
|
||||
const latestRelease = computed(() => announcements.value?.find((release) => release.latest && !release.announcement));
|
||||
const hasRelease = computed(() => latestRelease.value !== undefined);
|
||||
|
||||
export function useAnnouncements() {
|
||||
return {
|
||||
mostRecent,
|
||||
announcements,
|
||||
latestRelease,
|
||||
hasRelease,
|
||||
};
|
||||
}
|
||||
@@ -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,29 +0,0 @@
|
||||
const { data: releases } = useFetch(withBase("/api/releases")).get().json<
|
||||
{
|
||||
name: string;
|
||||
mentionsCount: number;
|
||||
createdAt: string;
|
||||
body: string;
|
||||
tag: string;
|
||||
htmlUrl: string;
|
||||
latest: boolean;
|
||||
features: number;
|
||||
bugFixes: number;
|
||||
breaking: number;
|
||||
}[]
|
||||
>();
|
||||
|
||||
const hasUpdate = computed(() => {
|
||||
if (!releases.value?.length) return false;
|
||||
return releases.value[0].tag !== config.version;
|
||||
});
|
||||
|
||||
const latest = computed(() => releases.value?.find((release) => release.latest));
|
||||
|
||||
export function useReleases() {
|
||||
return {
|
||||
hasUpdate,
|
||||
latest,
|
||||
releases,
|
||||
};
|
||||
}
|
||||
@@ -30,6 +30,7 @@ services:
|
||||
- DOZZLE_FILTER=name=dozzle
|
||||
- DOZZLE_NO_ANALYTICS=1
|
||||
- DOZZLE_HOSTNAME=localhost
|
||||
- DOZZLE_LEVEL=debug
|
||||
ports:
|
||||
- 7070:8080
|
||||
build:
|
||||
@@ -94,7 +95,7 @@ services:
|
||||
|
||||
playwright:
|
||||
container_name: playwright
|
||||
image: mcr.microsoft.com/playwright:v1.49.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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div
|
||||
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>K8s users, I need your help! I have added K8s support to Dozzle and I need
|
||||
your feedback! 🙏
|
||||
<a
|
||||
href="https://github.com/amir20/dozzle/discussions/3614"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-primary text-white!"
|
||||
>
|
||||
See the discussion
|
||||
</a>
|
||||
</div>
|
||||
<button class="btn btn-circle btn-ghost btn-xs ml-auto" @click="dismiss">
|
||||
<svg class="swap-on fill-current" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512">
|
||||
<polygon
|
||||
points="400 145.49 366.51 112 256 222.51 145.49 112 112 145.49 222.51 256 112 366.51 145.49 400 256 289.49 366.51 400 400 366.51 289.49 256 400 145.49"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
const dismiss = () => {
|
||||
localStorage.setItem("banner-dismissed", (Date.now() + 8.64e7 * 1).toString());
|
||||
document.documentElement.classList.add("banner-dismissed");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (new Date(+localStorage.getItem("banner-dismissed")) > Date.now()) {
|
||||
document.documentElement.classList.add("banner-dismissed");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.banner-dismissed {
|
||||
--vp-layout-top-height: 0px !important;
|
||||
}
|
||||
@media (min-width: 375px) {
|
||||
html {
|
||||
--vp-layout-top-height: 100px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
html {
|
||||
--vp-layout-top-height: 63px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.banner-dismissed .banner {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="stats mt-10">
|
||||
<div class="stats stats-horizontal mt-10">
|
||||
<div class="stat w-40 border-0!">
|
||||
<div class="stat-title">Github Stars</div>
|
||||
<div class="stat-title text-(--vp-c-text-1)">Github Stars</div>
|
||||
<div class="stat-value">
|
||||
<Counter :start="0" :end="data.stars" :duration="1000" :formatter="(value) => (value / 1e3).toFixed(1) + 'K'" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Docker Pulls</div>
|
||||
<div class="stat-title text-(--vp-c-text-1)">Docker Pulls</div>
|
||||
<div class="stat-value">
|
||||
<Counter :start="0" :end="data.pulls" :duration="1000" :formatter="(value) => (value / 1e6).toFixed(0) + 'M'" />
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,8 @@ import "./style.css";
|
||||
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,
|
||||
@@ -15,6 +17,8 @@ export default {
|
||||
"home-hero-image": () => h(HeroVideo),
|
||||
"sidebar-nav-after": () => h(BuyMeCoffee),
|
||||
"home-hero-actions-after": () => h(Stats),
|
||||
// "layout-top": () => h(Banner),
|
||||
"home-hero-after": () => h(Supported),
|
||||
});
|
||||
},
|
||||
enhanceApp(ctx) {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
@import "tailwindcss";
|
||||
@variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@plugin "daisyui" {
|
||||
include: stat;
|
||||
include: stat, button;
|
||||
themes: light --default;
|
||||
}
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
--depth: 0;
|
||||
--noise: 0;
|
||||
--radius-field: 2rem;
|
||||
--color-primary: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Banner: typeof import('./.vitepress/theme/components/Banner.vue')['default']
|
||||
BuyMeCoffee: typeof import('./.vitepress/theme/components/BuyMeCoffee.vue')['default']
|
||||
Counter: typeof import('./.vitepress/theme/components/Counter.vue')['default']
|
||||
HeroVideo: typeof import('./.vitepress/theme/components/HeroVideo.vue')['default']
|
||||
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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"dozzle": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/playfair-display": "^5.1.1"
|
||||
"@fontsource-variable/playfair-display": "^5.2.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': {}
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -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
|
||||
@@ -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.4.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/sys v0.29.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/go-chi/chi/v5 v5.2.0
|
||||
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.4.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/samber/lo v1.47.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.32.0
|
||||
golang.org/x/sync v0.10.0
|
||||
google.golang.org/grpc v1.69.2
|
||||
google.golang.org/protobuf v1.36.2
|
||||
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.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // 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
|
||||
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.4
|
||||
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,32 +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 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/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/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.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=
|
||||
@@ -60,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=
|
||||
@@ -95,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=
|
||||
@@ -111,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=
|
||||
@@ -120,59 +146,66 @@ 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/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.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=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
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/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/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/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=
|
||||
@@ -182,14 +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=
|
||||
@@ -205,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=
|
||||
@@ -222,10 +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=
|
||||
@@ -237,27 +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=
|
||||
@@ -266,12 +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/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/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.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=
|
||||
@@ -281,36 +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/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/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/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=
|
||||
@@ -319,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=
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent/pb"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
@@ -89,7 +89,7 @@ func rpcErrToErr(err error) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since time.Time, until time.Time, std docker.StdType) (<-chan *docker.LogEvent, error) {
|
||||
func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since time.Time, until time.Time, std container.StdType) (<-chan *container.LogEvent, error) {
|
||||
stream, err := c.client.LogsBetweenDates(ctx, &pb.LogsBetweenDatesRequest{
|
||||
ContainerId: containerID,
|
||||
Since: timestamppb.New(since),
|
||||
@@ -101,7 +101,7 @@ func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := make(chan *docker.LogEvent)
|
||||
events := make(chan *container.LogEvent)
|
||||
|
||||
go func() {
|
||||
sendLogs(stream, events)
|
||||
@@ -111,7 +111,7 @@ func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, since time.Time, std docker.StdType, events chan<- *docker.LogEvent) error {
|
||||
func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, since time.Time, std container.StdType, events chan<- *container.LogEvent) error {
|
||||
stream, err := c.client.StreamLogs(ctx, &pb.StreamLogsRequest{
|
||||
ContainerId: containerID,
|
||||
Since: timestamppb.New(since),
|
||||
@@ -125,7 +125,7 @@ func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, si
|
||||
return sendLogs(stream, events)
|
||||
}
|
||||
|
||||
func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *docker.LogEvent) error {
|
||||
func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *container.LogEvent) error {
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
@@ -151,19 +151,19 @@ func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *docker.Log
|
||||
continue
|
||||
}
|
||||
|
||||
events <- &docker.LogEvent{
|
||||
events <- &container.LogEvent{
|
||||
Id: resp.Event.Id,
|
||||
ContainerID: resp.Event.ContainerId,
|
||||
Message: message,
|
||||
Timestamp: resp.Event.Timestamp.AsTime().Unix(),
|
||||
Position: docker.LogPosition(resp.Event.Position),
|
||||
Position: container.LogPosition(resp.Event.Position),
|
||||
Level: resp.Event.Level,
|
||||
Stream: resp.Event.Stream,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) StreamRawBytes(ctx context.Context, containerID string, since time.Time, until time.Time, std docker.StdType) (io.ReadCloser, error) {
|
||||
func (c *Client) StreamRawBytes(ctx context.Context, containerID string, since time.Time, until time.Time, std container.StdType) (io.ReadCloser, error) {
|
||||
out, err := c.client.StreamRawBytes(context.Background(), &pb.StreamRawBytesRequest{
|
||||
ContainerId: containerID,
|
||||
Since: timestamppb.New(since),
|
||||
@@ -199,7 +199,7 @@ func (c *Client) StreamRawBytes(ctx context.Context, containerID string, since t
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (c *Client) StreamStats(ctx context.Context, stats chan<- docker.ContainerStat) error {
|
||||
func (c *Client) StreamStats(ctx context.Context, stats chan<- container.ContainerStat) error {
|
||||
stream, err := c.client.StreamStats(ctx, &pb.StreamStatsRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -211,7 +211,7 @@ func (c *Client) StreamStats(ctx context.Context, stats chan<- docker.ContainerS
|
||||
return rpcErrToErr(err)
|
||||
}
|
||||
|
||||
stats <- docker.ContainerStat{
|
||||
stats <- container.ContainerStat{
|
||||
CPUPercent: resp.Stat.CpuPercent,
|
||||
MemoryPercent: resp.Stat.MemoryPercent,
|
||||
MemoryUsage: resp.Stat.MemoryUsage,
|
||||
@@ -220,7 +220,7 @@ func (c *Client) StreamStats(ctx context.Context, stats chan<- docker.ContainerS
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) StreamEvents(ctx context.Context, events chan<- docker.ContainerEvent) error {
|
||||
func (c *Client) StreamEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
|
||||
stream, err := c.client.StreamEvents(ctx, &pb.StreamEventsRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -232,7 +232,7 @@ func (c *Client) StreamEvents(ctx context.Context, events chan<- docker.Containe
|
||||
return rpcErrToErr(err)
|
||||
}
|
||||
|
||||
events <- docker.ContainerEvent{
|
||||
events <- container.ContainerEvent{
|
||||
ActorID: resp.Event.ActorId,
|
||||
Name: resp.Event.Name,
|
||||
Host: resp.Event.Host,
|
||||
@@ -241,7 +241,7 @@ func (c *Client) StreamEvents(ctx context.Context, events chan<- docker.Containe
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- docker.Container) error {
|
||||
func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- container.Container) error {
|
||||
stream, err := c.client.StreamContainerStarted(ctx, &pb.StreamContainerStartedRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -253,7 +253,7 @@ func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- dock
|
||||
return rpcErrToErr(err)
|
||||
}
|
||||
|
||||
containers <- docker.Container{
|
||||
containers <- container.Container{
|
||||
ID: resp.Container.Id,
|
||||
Name: resp.Container.Name,
|
||||
Image: resp.Container.Image,
|
||||
@@ -271,16 +271,16 @@ func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- dock
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) FindContainer(ctx context.Context, containerID string) (docker.Container, error) {
|
||||
func (c *Client) FindContainer(ctx context.Context, containerID string) (container.Container, error) {
|
||||
response, err := c.client.FindContainer(ctx, &pb.FindContainerRequest{ContainerId: containerID})
|
||||
if err != nil {
|
||||
return docker.Container{}, err
|
||||
return container.Container{}, err
|
||||
}
|
||||
|
||||
var stats []docker.ContainerStat
|
||||
var stats []container.ContainerStat
|
||||
|
||||
for _, stat := range response.Container.Stats {
|
||||
stats = append(stats, docker.ContainerStat{
|
||||
stats = append(stats, container.ContainerStat{
|
||||
ID: stat.Id,
|
||||
CPUPercent: stat.CpuPercent,
|
||||
MemoryPercent: stat.MemoryPercent,
|
||||
@@ -288,7 +288,7 @@ func (c *Client) FindContainer(ctx context.Context, containerID string) (docker.
|
||||
})
|
||||
}
|
||||
|
||||
return docker.Container{
|
||||
return container.Container{
|
||||
ID: response.Container.Id,
|
||||
Name: response.Container.Name,
|
||||
Image: response.Container.Image,
|
||||
@@ -306,12 +306,12 @@ func (c *Client) FindContainer(ctx context.Context, containerID string) (docker.
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListContainers(ctx context.Context, filter docker.ContainerFilter) ([]docker.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}
|
||||
}
|
||||
}
|
||||
@@ -321,11 +321,11 @@ func (c *Client) ListContainers(ctx context.Context, filter docker.ContainerFilt
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containers := make([]docker.Container, 0)
|
||||
for _, container := range response.Containers {
|
||||
var stats []docker.ContainerStat
|
||||
for _, stat := range container.Stats {
|
||||
stats = append(stats, docker.ContainerStat{
|
||||
containers := make([]container.Container, 0)
|
||||
for _, c := range response.Containers {
|
||||
var stats []container.ContainerStat
|
||||
for _, stat := range c.Stats {
|
||||
stats = append(stats, container.ContainerStat{
|
||||
ID: stat.Id,
|
||||
CPUPercent: stat.CpuPercent,
|
||||
MemoryPercent: stat.MemoryPercent,
|
||||
@@ -333,20 +333,20 @@ func (c *Client) ListContainers(ctx context.Context, filter docker.ContainerFilt
|
||||
})
|
||||
}
|
||||
|
||||
containers = append(containers, docker.Container{
|
||||
ID: container.Id,
|
||||
Name: container.Name,
|
||||
Image: container.Image,
|
||||
Labels: container.Labels,
|
||||
Group: container.Group,
|
||||
Created: container.Created.AsTime(),
|
||||
State: container.State,
|
||||
Health: container.Health,
|
||||
Host: container.Host,
|
||||
Tty: container.Tty,
|
||||
Command: container.Command,
|
||||
StartedAt: container.Started.AsTime(),
|
||||
FinishedAt: container.Finished.AsTime(),
|
||||
containers = append(containers, container.Container{
|
||||
ID: c.Id,
|
||||
Name: c.Name,
|
||||
Image: c.Image,
|
||||
Labels: c.Labels,
|
||||
Group: c.Group,
|
||||
Created: c.Created.AsTime(),
|
||||
State: c.State,
|
||||
Health: c.Health,
|
||||
Host: c.Host,
|
||||
Tty: c.Tty,
|
||||
Command: c.Command,
|
||||
StartedAt: c.Started.AsTime(),
|
||||
FinishedAt: c.Finished.AsTime(),
|
||||
Stats: utils.RingBufferFrom(300, stats),
|
||||
})
|
||||
}
|
||||
@@ -354,17 +354,17 @@ func (c *Client) ListContainers(ctx context.Context, filter docker.ContainerFilt
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (c *Client) Host(ctx context.Context) (docker.Host, error) {
|
||||
func (c *Client) Host(ctx context.Context) (container.Host, error) {
|
||||
info, err := c.client.HostInfo(ctx, &pb.HostInfoRequest{})
|
||||
if err != nil {
|
||||
return docker.Host{
|
||||
return container.Host{
|
||||
Endpoint: c.endpoint,
|
||||
Type: "agent",
|
||||
Available: false,
|
||||
}, err
|
||||
}
|
||||
|
||||
return docker.Host{
|
||||
return container.Host{
|
||||
ID: info.Host.Id,
|
||||
Name: info.Host.Name,
|
||||
NCPU: int(info.Host.CpuCores),
|
||||
@@ -376,16 +376,16 @@ func (c *Client) Host(ctx context.Context) (docker.Host, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) ContainerAction(ctx context.Context, containerId string, action docker.ContainerAction) error {
|
||||
func (c *Client) ContainerAction(ctx context.Context, containerId string, action container.ContainerAction) error {
|
||||
var containerAction pb.ContainerAction
|
||||
switch action {
|
||||
case docker.Start:
|
||||
case container.Start:
|
||||
containerAction = pb.ContainerAction_Start
|
||||
|
||||
case docker.Stop:
|
||||
case container.Stop:
|
||||
containerAction = pb.ContainerAction_Stop
|
||||
|
||||
case docker.Restart:
|
||||
case container.Restart:
|
||||
containerAction = pb.ContainerAction_Restart
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -28,46 +28,46 @@ var client *MockedClient
|
||||
|
||||
type MockedClient struct {
|
||||
mock.Mock
|
||||
docker.Client
|
||||
container.Client
|
||||
}
|
||||
|
||||
func (m *MockedClient) FindContainer(ctx context.Context, id string) (docker.Container, error) {
|
||||
func (m *MockedClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(docker.Container), args.Error(1)
|
||||
return args.Get(0).(container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerActions(ctx context.Context, action docker.ContainerAction, containerID string) error {
|
||||
func (m *MockedClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
|
||||
args := m.Called(ctx, action, containerID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- docker.ContainerEvent) error {
|
||||
func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
|
||||
args := m.Called(ctx, events)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ListContainers(ctx context.Context, filter docker.ContainerFilter) ([]docker.Container, error) {
|
||||
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
|
||||
args := m.Called(ctx, filter)
|
||||
return args.Get(0).([]docker.Container), args.Error(1)
|
||||
return args.Get(0).([]container.Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType docker.StdType) (io.ReadCloser, error) {
|
||||
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, id, since, stdType)
|
||||
return args.Get(0).(io.ReadCloser), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerStats(context.Context, string, chan<- docker.ContainerStat) error {
|
||||
func (m *MockedClient) ContainerStats(context.Context, string, chan<- container.ContainerStat) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType docker.StdType) (io.ReadCloser, error) {
|
||||
func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, id, from, to, stdType)
|
||||
return args.Get(0).(io.ReadCloser), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) Host() docker.Host {
|
||||
func (m *MockedClient) Host() container.Host {
|
||||
args := m.Called()
|
||||
return args.Get(0).(docker.Host)
|
||||
return args.Get(0).(container.Host)
|
||||
}
|
||||
|
||||
func (m *MockedClient) IsSwarmMode() bool {
|
||||
@@ -92,7 +92,7 @@ func init() {
|
||||
}
|
||||
|
||||
client = &MockedClient{}
|
||||
client.On("ListContainers", mock.Anything, mock.Anything).Return([]docker.Container{
|
||||
client.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
|
||||
{
|
||||
ID: "123456",
|
||||
Name: "test",
|
||||
@@ -101,17 +101,17 @@ func init() {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
client.On("Host").Return(docker.Host{
|
||||
client.On("Host").Return(container.Host{
|
||||
ID: "localhost",
|
||||
Endpoint: "local",
|
||||
Name: "local",
|
||||
})
|
||||
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
|
||||
time.Sleep(5 * time.Second)
|
||||
})
|
||||
|
||||
client.On("FindContainer", mock.Anything, "123456").Return(docker.Container{
|
||||
client.On("FindContainer", mock.Anything, "123456").Return(container.Container{
|
||||
ID: "123456",
|
||||
Name: "test",
|
||||
Host: "localhost",
|
||||
@@ -124,13 +124,13 @@ func init() {
|
||||
Labels: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
Stats: utils.NewRingBuffer[docker.ContainerStat](300),
|
||||
Stats: utils.NewRingBuffer[container.ContainerStat](300),
|
||||
Created: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
StartedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
FinishedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}, nil)
|
||||
|
||||
server, _ := NewServer(client, certs, "test", docker.ContainerFilter{})
|
||||
server, _ := NewServer(client, certs, "test", container.ContainerLabels{})
|
||||
|
||||
go server.Serve(lis)
|
||||
}
|
||||
@@ -145,9 +145,9 @@ func TestFindContainer(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
container, _ := rpc.FindContainer(context.Background(), "123456")
|
||||
c, _ := rpc.FindContainer(context.Background(), "123456")
|
||||
|
||||
assert.Equal(t, container, docker.Container{
|
||||
assert.Equal(t, c, container.Container{
|
||||
ID: "123456",
|
||||
Name: "test",
|
||||
Host: "localhost",
|
||||
@@ -160,7 +160,7 @@ func TestFindContainer(t *testing.T) {
|
||||
Labels: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
Stats: utils.NewRingBuffer[docker.ContainerStat](300),
|
||||
Stats: utils.NewRingBuffer[container.ContainerStat](300),
|
||||
Created: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
StartedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
FinishedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
@@ -173,9 +173,9 @@ func TestListContainers(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
containers, _ := rpc.ListContainers(context.Background(), docker.ContainerFilter{})
|
||||
containers, _ := rpc.ListContainers(context.Background(), container.ContainerLabels{})
|
||||
|
||||
assert.Equal(t, containers, []docker.Container{
|
||||
assert.Equal(t, containers, []container.Container{
|
||||
{
|
||||
ID: "123456",
|
||||
Name: "test",
|
||||
@@ -189,7 +189,7 @@ func TestListContainers(t *testing.T) {
|
||||
Labels: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
Stats: utils.NewRingBuffer[docker.ContainerStat](300),
|
||||
Stats: utils.NewRingBuffer[container.ContainerStat](300),
|
||||
Created: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
StartedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
FinishedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
@@ -25,19 +26,20 @@ import (
|
||||
)
|
||||
|
||||
type server struct {
|
||||
client docker.Client
|
||||
store *docker.ContainerStore
|
||||
client container.Client
|
||||
store *container.ContainerStore
|
||||
version string
|
||||
|
||||
pb.UnimplementedAgentServiceServer
|
||||
}
|
||||
|
||||
func newServer(client docker.Client, dozzleVersion string, filter docker.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: docker.NewContainerStore(context.Background(), client, filter),
|
||||
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,17 +49,18 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
since = in.Since.AsTime()
|
||||
}
|
||||
|
||||
container, err := s.store.FindContainer(in.ContainerId, docker.ContainerFilter{})
|
||||
c, err := s.store.FindContainer(in.ContainerId, container.ContainerLabels{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader, err := s.client.ContainerLogs(out.Context(), in.ContainerId, since, docker.StdType(in.StreamTypes))
|
||||
reader, err := s.client.ContainerLogs(out.Context(), in.ContainerId, since, container.StdType(in.StreamTypes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g := docker.NewEventGenerator(out.Context(), reader, container)
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(out.Context(), dockerReader, c)
|
||||
|
||||
for event := range g.Events {
|
||||
out.Send(&pb.StreamLogsResponse{
|
||||
@@ -74,17 +77,18 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
|
||||
}
|
||||
|
||||
func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentService_LogsBetweenDatesServer) error {
|
||||
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), docker.StdType(in.StreamTypes))
|
||||
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
container, err := s.client.FindContainer(out.Context(), in.ContainerId)
|
||||
c, err := s.client.FindContainer(out.Context(), in.ContainerId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g := docker.NewEventGenerator(out.Context(), reader, container)
|
||||
dockerReader := docker.NewLogReader(reader, c.Tty)
|
||||
g := container.NewEventGenerator(out.Context(), dockerReader, c)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -101,7 +105,7 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
|
||||
}
|
||||
|
||||
func (s *server) StreamRawBytes(in *pb.StreamRawBytesRequest, out pb.AgentService_StreamRawBytesServer) error {
|
||||
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), docker.StdType(in.StreamTypes))
|
||||
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -129,7 +133,7 @@ func (s *server) StreamRawBytes(in *pb.StreamRawBytesRequest, out pb.AgentServic
|
||||
}
|
||||
|
||||
func (s *server) StreamEvents(in *pb.StreamEventsRequest, out pb.AgentService_StreamEventsServer) error {
|
||||
events := make(chan docker.ContainerEvent)
|
||||
events := make(chan container.ContainerEvent)
|
||||
|
||||
s.store.SubscribeEvents(out.Context(), events)
|
||||
|
||||
@@ -151,7 +155,7 @@ func (s *server) StreamEvents(in *pb.StreamEventsRequest, out pb.AgentService_St
|
||||
}
|
||||
|
||||
func (s *server) StreamStats(in *pb.StreamStatsRequest, out pb.AgentService_StreamStatsServer) error {
|
||||
stats := make(chan docker.ContainerStat)
|
||||
stats := make(chan container.ContainerStat)
|
||||
|
||||
s.store.SubscribeStats(out.Context(), stats)
|
||||
|
||||
@@ -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(docker.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(docker.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
|
||||
}
|
||||
@@ -268,7 +272,7 @@ func (s *server) HostInfo(ctx context.Context, in *pb.HostInfoRequest) (*pb.Host
|
||||
}
|
||||
|
||||
func (s *server) StreamContainerStarted(in *pb.StreamContainerStartedRequest, out pb.AgentService_StreamContainerStartedServer) error {
|
||||
containers := make(chan docker.Container)
|
||||
containers := make(chan container.Container)
|
||||
|
||||
go s.store.SubscribeNewContainers(out.Context(), containers)
|
||||
|
||||
@@ -298,16 +302,16 @@ func (s *server) StreamContainerStarted(in *pb.StreamContainerStartedRequest, ou
|
||||
}
|
||||
|
||||
func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequest) (*pb.ContainerActionResponse, error) {
|
||||
var action docker.ContainerAction
|
||||
var action container.ContainerAction
|
||||
switch in.Action {
|
||||
case pb.ContainerAction_Start:
|
||||
action = docker.Start
|
||||
action = container.Start
|
||||
|
||||
case pb.ContainerAction_Stop:
|
||||
action = docker.Stop
|
||||
action = container.Stop
|
||||
|
||||
case pb.ContainerAction_Restart:
|
||||
action = docker.Restart
|
||||
action = container.Restart
|
||||
|
||||
default:
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid action")
|
||||
@@ -322,7 +326,7 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
|
||||
return &pb.ContainerActionResponse{}, nil
|
||||
}
|
||||
|
||||
func NewServer(client docker.Client, certificates tls.Certificate, dozzleVersion string, filter docker.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,12 +345,12 @@ func NewServer(client docker.Client, certificates tls.Certificate, dozzleVersion
|
||||
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
|
||||
}
|
||||
|
||||
func logEventToPb(event *docker.LogEvent) *pb.LogEvent {
|
||||
func logEventToPb(event *container.LogEvent) *pb.LogEvent {
|
||||
var message *anypb.Any
|
||||
|
||||
if event.Message == nil {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ func NewForwardProxyAuth(userHeader, emailHeader, nameHeader, filterHeader strin
|
||||
func (p *proxyAuthContext) AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get(p.headerUser) != "" {
|
||||
containerFilter, err := docker.ParseContainerFilter(r.Header.Get(p.headerFilter))
|
||||
containerFilter, err := container.ParseContainerFilter(r.Header.Get(p.headerFilter))
|
||||
if err != nil {
|
||||
log.Fatal().Str("filter", r.Header.Get(p.headerFilter)).Msg("Failed to parse container filter")
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -19,12 +19,12 @@ import (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username" yaml:"-"`
|
||||
Email string `json:"email" yaml:"email"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Password string `json:"-" yaml:"password"`
|
||||
Filter string `json:"-" yaml:"filter"`
|
||||
ContainerFilter docker.ContainerFilter `json:"-" yaml:"-"`
|
||||
Username string `json:"username" yaml:"-"`
|
||||
Email string `json:"email" yaml:"email"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Password string `json:"-" yaml:"password"`
|
||||
Filter string `json:"-" yaml:"filter"`
|
||||
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 docker.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,9 +197,9 @@ func UserFromContext(ctx context.Context) *User {
|
||||
}
|
||||
email := claims["email"].(string)
|
||||
name := claims["name"].(string)
|
||||
containerFilter := docker.ContainerFilter{}
|
||||
containerFilter := container.ContainerLabels{}
|
||||
if filter, ok := claims["filter"].(string); ok {
|
||||
containerFilter, err = docker.ParseContainerFilter(filter)
|
||||
containerFilter, err = container.ParseContainerFilter(filter)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Str("filter", filter).Msg("Failed to parse container filter")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StdType int
|
||||
|
||||
const (
|
||||
UNKNOWN StdType = 1 << iota
|
||||
STDOUT
|
||||
STDERR
|
||||
)
|
||||
const STDALL = STDOUT | STDERR
|
||||
|
||||
func (s StdType) String() string {
|
||||
switch s {
|
||||
case STDOUT:
|
||||
return "stdout"
|
||||
case STDERR:
|
||||
return "stderr"
|
||||
case STDALL:
|
||||
return "all"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
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
|
||||
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
|
||||
ContainerStats(context.Context, string, chan<- ContainerStat) error
|
||||
Ping(context.Context) error
|
||||
Host() Host
|
||||
ContainerActions(ctx context.Context, action ContainerAction, containerID string) error
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,32 +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
|
||||
}
|
||||
|
||||
func NewContainerStore(ctx context.Context, client Client, filter ContainerFilter) *ContainerStore {
|
||||
log.Debug().Str("host", client.Host().Name).Interface("filter", filter).Msg("initializing container store")
|
||||
const defaultTimeout = 10 * time.Second
|
||||
|
||||
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)
|
||||
@@ -64,9 +72,9 @@ func (s *ContainerStore) checkConnectivity() error {
|
||||
s.connected.Store(false)
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s is enough to fetch all containers
|
||||
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()
|
||||
@@ -76,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)
|
||||
@@ -88,7 +96,7 @@ func (s *ContainerStore) checkConnectivity() error {
|
||||
}
|
||||
go func(c Container, i int) {
|
||||
defer sem.Release(1)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 2s is hardcoded timeout for fetching container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
if container, err := s.client.FindContainer(ctx, c.ID); err == nil {
|
||||
s.containers.Store(c.ID, &container)
|
||||
@@ -107,38 +115,48 @@ 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validContainers, err := s.client.ListContainers(s.ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validIDMap := lo.KeyBy(validContainers, func(item Container) string {
|
||||
return item.ID
|
||||
})
|
||||
|
||||
containers := make([]Container, 0)
|
||||
s.containers.Range(func(_ string, c *Container) bool {
|
||||
if _, ok := validIDMap[c.ID]; ok {
|
||||
containers = append(containers, *c)
|
||||
if labels.Exists() {
|
||||
validContainers, err := s.client.ListContainers(s.ctx, labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
s.containers.Range(func(_ string, c *Container) bool {
|
||||
if _, ok := validIDMap[c.ID]; ok {
|
||||
containers = append(containers, *c)
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
s.containers.Range(func(_ string, c *Container) bool {
|
||||
containers = append(containers, *c)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -154,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(), 3*time.Second)
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -234,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":
|
||||
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 {
|
||||
@@ -248,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 {
|
||||
@@ -264,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 {
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestContainerStore_List(t *testing.T) {
|
||||
Name: "test",
|
||||
},
|
||||
}, nil)
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
|
||||
ctx := args.Get(0).(context.Context)
|
||||
<-ctx.Done()
|
||||
})
|
||||
@@ -63,11 +63,9 @@ 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")
|
||||
}
|
||||
@@ -83,7 +81,7 @@ func TestContainerStore_die(t *testing.T) {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(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)
|
||||
@@ -98,7 +96,7 @@ func TestContainerStore_die(t *testing.T) {
|
||||
ID: "localhost",
|
||||
})
|
||||
|
||||
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- docker.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",
|
||||
@@ -107,15 +105,19 @@ func TestContainerStore_die(t *testing.T) {
|
||||
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)
|
||||
store.SubscribeEvents(t.Context(), events)
|
||||
<-events
|
||||
|
||||
containers, _ := store.ListContainers(ContainerFilter{})
|
||||
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,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"html"
|
||||
@@ -30,7 +30,6 @@ func escape(logEvent *LogEvent) {
|
||||
}
|
||||
|
||||
func escapeAnyMap(orderedMap *orderedmap.OrderedMap[string, any]) {
|
||||
|
||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||
switch value := pair.Value.(type) {
|
||||
case string:
|
||||
@@ -1,14 +1,10 @@
|
||||
package docker
|
||||
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 docker
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -25,6 +25,7 @@ type Host struct {
|
||||
AgentVersion string `json:"agentVersion,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Available bool `json:"available"`
|
||||
Swarm bool `json:"-"`
|
||||
}
|
||||
|
||||
func (h Host) String() string {
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -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](
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,25 +7,25 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -43,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
|
||||
}
|
||||
@@ -66,21 +67,10 @@ func ParseContainerFilter(commaValues string) (ContainerFilter, error) {
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func (f ContainerFilter) Exists() bool {
|
||||
func (f ContainerLabels) Exists() bool {
|
||||
return len(f) > 0
|
||||
}
|
||||
|
||||
func (f ContainerFilter) asArgs() filters.Args {
|
||||
filterArgs := filters.NewArgs()
|
||||
for key, values := range f {
|
||||
for _, value := range values {
|
||||
filterArgs.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filterArgs
|
||||
}
|
||||
|
||||
type LogPosition string
|
||||
|
||||
const (
|
||||
@@ -11,112 +11,76 @@ import (
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
docker "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type StdType int
|
||||
|
||||
const (
|
||||
UNKNOWN StdType = 1 << iota
|
||||
STDOUT
|
||||
STDERR
|
||||
)
|
||||
const STDALL = STDOUT | STDERR
|
||||
|
||||
func (s StdType) String() string {
|
||||
switch s {
|
||||
case STDOUT:
|
||||
return "stdout"
|
||||
case STDERR:
|
||||
return "stderr"
|
||||
case STDALL:
|
||||
return "all"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type DockerCLI interface {
|
||||
ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
|
||||
ContainerLogs(context.Context, string, container.LogsOptions) (io.ReadCloser, 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)
|
||||
ContainerStats(ctx context.Context, containerID string, stream bool) (container.StatsResponseReader, 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 container.StartOptions) error
|
||||
ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
ContainerStart(ctx context.Context, containerID string, options docker.StartOptions) error
|
||||
ContainerStop(ctx context.Context, containerID string, options docker.StopOptions) error
|
||||
ContainerRestart(ctx context.Context, containerID string, options docker.StopOptions) error
|
||||
Info(ctx context.Context) (system.Info, error)
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
ListContainers(context.Context, ContainerFilter) ([]Container, error)
|
||||
FindContainer(context.Context, string) (Container, error)
|
||||
ContainerLogs(context.Context, string, time.Time, StdType) (io.ReadCloser, error)
|
||||
ContainerEvents(context.Context, chan<- ContainerEvent) error
|
||||
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
|
||||
ContainerStats(context.Context, string, chan<- ContainerStat) error
|
||||
Ping(context.Context) (types.Ping, error)
|
||||
Host() Host
|
||||
ContainerActions(ctx context.Context, action ContainerAction, containerID string) error
|
||||
IsSwarmMode() bool
|
||||
SystemInfo() system.Info
|
||||
}
|
||||
|
||||
type httpClient struct {
|
||||
type DockerClient struct {
|
||||
cli DockerCLI
|
||||
host Host
|
||||
host container.Host
|
||||
info system.Info
|
||||
}
|
||||
|
||||
func NewClient(cli DockerCLI, host Host) 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")
|
||||
}
|
||||
|
||||
host.NCPU = info.NCPU
|
||||
host.MemTotal = info.MemTotal
|
||||
host.DockerVersion = info.ServerVersion
|
||||
|
||||
return &httpClient{
|
||||
cli: cli,
|
||||
host: host,
|
||||
info: info,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientWithFilters creates a new instance of Client with docker filters
|
||||
func NewLocalClient(hostname string) (Client, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := cli.Info(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := info.ID
|
||||
if info.Swarm.NodeID != "" {
|
||||
id = info.Swarm.NodeID
|
||||
}
|
||||
|
||||
host := Host{
|
||||
ID: id,
|
||||
host.ID = id
|
||||
host.NCPU = info.NCPU
|
||||
host.MemTotal = info.MemTotal
|
||||
host.DockerVersion = info.ServerVersion
|
||||
host.Swarm = info.Swarm.NodeID != ""
|
||||
|
||||
return &DockerClient{
|
||||
cli: cli,
|
||||
host: host,
|
||||
info: info,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := cli.Info(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
host := container.Host{
|
||||
Name: info.Name,
|
||||
MemTotal: info.MemTotal,
|
||||
NCPU: info.NCPU,
|
||||
Endpoint: "local",
|
||||
Type: "local",
|
||||
}
|
||||
@@ -128,7 +92,7 @@ func NewLocalClient(hostname string) (Client, error) {
|
||||
return NewClient(cli, host), nil
|
||||
}
|
||||
|
||||
func NewRemoteClient(host Host) (Client, error) {
|
||||
func NewRemoteClient(host container.Host) (*DockerClient, error) {
|
||||
if host.URL.Scheme != "tcp" {
|
||||
return nil, fmt.Errorf("invalid scheme: %s", host.URL.Scheme)
|
||||
}
|
||||
@@ -144,7 +108,7 @@ func NewRemoteClient(host Host) (Client, error) {
|
||||
log.Debug().Msg("Not using TLS for remote client")
|
||||
}
|
||||
|
||||
opts = append(opts, client.WithAPIVersionNegotiation())
|
||||
opts = append(opts, client.WithAPIVersionNegotiation(), client.WithUserAgent("Docker-Client/Dozzle"))
|
||||
|
||||
cli, err := client.NewClientWithOpts(opts...)
|
||||
|
||||
@@ -158,33 +122,39 @@ func NewRemoteClient(host Host) (Client, error) {
|
||||
}
|
||||
|
||||
// Finds a container by id, skipping the filters
|
||||
func (d *httpClient) FindContainer(ctx context.Context, id string) (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
|
||||
} else {
|
||||
return Container{}, err
|
||||
return container.Container{}, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerActions(ctx context.Context, action ContainerAction, containerID string) error {
|
||||
func (d *DockerClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
|
||||
switch action {
|
||||
case Start:
|
||||
return d.cli.ContainerStart(ctx, containerID, container.StartOptions{})
|
||||
case Stop:
|
||||
return d.cli.ContainerStop(ctx, containerID, container.StopOptions{})
|
||||
case Restart:
|
||||
return d.cli.ContainerRestart(ctx, containerID, container.StopOptions{})
|
||||
case container.Start:
|
||||
return d.cli.ContainerStart(ctx, containerID, docker.StartOptions{})
|
||||
case container.Stop:
|
||||
return d.cli.ContainerStop(ctx, containerID, docker.StopOptions{})
|
||||
case container.Restart:
|
||||
return d.cli.ContainerRestart(ctx, containerID, docker.StopOptions{})
|
||||
default:
|
||||
return fmt.Errorf("unknown action: %s", action)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *httpClient) ListContainers(ctx context.Context, filter ContainerFilter) ([]Container, error) {
|
||||
log.Debug().Interface("filter", filter).Str("host", d.host.Name).Msg("Listing containers")
|
||||
containerListOptions := container.ListOptions{
|
||||
Filters: filter.asArgs(),
|
||||
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 labels {
|
||||
for _, value := range values {
|
||||
filterArgs.Add(key, value)
|
||||
}
|
||||
}
|
||||
containerListOptions := docker.ListOptions{
|
||||
Filters: filterArgs,
|
||||
All: true,
|
||||
}
|
||||
list, err := d.cli.ContainerList(ctx, containerListOptions)
|
||||
@@ -192,7 +162,7 @@ func (d *httpClient) ListContainers(ctx context.Context, filter ContainerFilter)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers = make([]Container, 0, len(list))
|
||||
var containers = make([]container.Container, 0, len(list))
|
||||
for _, c := range list {
|
||||
containers = append(containers, newContainer(c, d.host.ID))
|
||||
}
|
||||
@@ -204,7 +174,7 @@ func (d *httpClient) ListContainers(ctx context.Context, filter ContainerFilter)
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<- 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 {
|
||||
@@ -213,7 +183,7 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
|
||||
|
||||
defer response.Body.Close()
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var v *container.StatsResponse
|
||||
var v *docker.StatsResponse
|
||||
for {
|
||||
if err := decoder.Decode(&v); err != nil {
|
||||
return err
|
||||
@@ -243,7 +213,7 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case stats <- ContainerStat{
|
||||
case stats <- container.ContainerStat{
|
||||
ID: id,
|
||||
CPUPercent: cpuPercent,
|
||||
MemoryPercent: memPercent,
|
||||
@@ -254,13 +224,13 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
|
||||
}
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType 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)
|
||||
options := container.LogsOptions{
|
||||
ShowStdout: stdType&STDOUT != 0,
|
||||
ShowStderr: stdType&STDERR != 0,
|
||||
options := docker.LogsOptions{
|
||||
ShowStdout: stdType&container.STDOUT != 0,
|
||||
ShowStderr: stdType&container.STDERR != 0,
|
||||
Follow: true,
|
||||
Tail: strconv.Itoa(100),
|
||||
Timestamps: true,
|
||||
@@ -275,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<- ContainerEvent) error {
|
||||
func (d *DockerClient) ContainerEvents(ctx context.Context, messages chan<- container.ContainerEvent) error {
|
||||
dockerMessages, err := d.cli.Events(ctx, events.ListOptions{})
|
||||
|
||||
for {
|
||||
@@ -287,7 +257,7 @@ func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- Contai
|
||||
|
||||
case message := <-dockerMessages:
|
||||
if message.Type == events.ContainerEventType && len(message.Actor.ID) > 0 {
|
||||
messages <- ContainerEvent{
|
||||
messages <- container.ContainerEvent{
|
||||
ActorID: message.Actor.ID[:12],
|
||||
Name: string(message.Action),
|
||||
Host: d.host.ID,
|
||||
@@ -299,11 +269,11 @@ 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 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 := container.LogsOptions{
|
||||
ShowStdout: stdType&STDOUT != 0,
|
||||
ShowStderr: stdType&STDERR != 0,
|
||||
options := docker.LogsOptions{
|
||||
ShowStdout: stdType&container.STDOUT != 0,
|
||||
ShowStderr: stdType&container.STDERR != 0,
|
||||
Timestamps: true,
|
||||
Since: from.Format(time.RFC3339Nano),
|
||||
Until: to.Format(time.RFC3339Nano),
|
||||
@@ -317,24 +287,17 @@ func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, f
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (d *httpClient) Ping(ctx context.Context) (types.Ping, error) {
|
||||
return d.cli.Ping(ctx)
|
||||
func (d *DockerClient) Ping(ctx context.Context) error {
|
||||
_, err := d.cli.Ping(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *httpClient) Host() 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 (d *httpClient) SystemInfo() system.Info {
|
||||
return d.info
|
||||
}
|
||||
|
||||
func newContainer(c types.Container, host string) 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"]
|
||||
@@ -346,7 +309,7 @@ func newContainer(c types.Container, host string) Container {
|
||||
if c.Labels["dev.dozzle.group"] != "" {
|
||||
group = c.Labels["dev.dozzle.group"]
|
||||
}
|
||||
return Container{
|
||||
return container.Container{
|
||||
ID: c.ID[:12],
|
||||
Name: name,
|
||||
Image: c.Image,
|
||||
@@ -355,12 +318,12 @@ func newContainer(c types.Container, host string) Container {
|
||||
State: c.State,
|
||||
Host: host,
|
||||
Labels: c.Labels,
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
|
||||
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
|
||||
Group: group,
|
||||
}
|
||||
}
|
||||
|
||||
func newContainerFromJSON(c types.ContainerJSON, host string) 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"]
|
||||
@@ -373,17 +336,18 @@ func newContainerFromJSON(c types.ContainerJSON, host string) Container {
|
||||
group = c.Config.Labels["dev.dozzle.group"]
|
||||
}
|
||||
|
||||
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[ContainerStat](300), // 300 seconds of stats
|
||||
Group: group,
|
||||
Tty: c.Config.Tty,
|
||||
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,
|
||||
FullyLoaded: true,
|
||||
}
|
||||
|
||||
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
docker "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -23,17 +23,16 @@ type mockedProxy struct {
|
||||
DockerCLI
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerList(context.Context, container.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 container.LogsOptions) (io.ReadCloser, error) {
|
||||
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options docker.LogsOptions) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
reader, ok := args.Get(0).(io.ReadCloser)
|
||||
if !ok && args.Get(0) != nil {
|
||||
@@ -42,16 +41,16 @@ func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options cont
|
||||
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) (container.StatsResponseReader, error) {
|
||||
return container.StatsResponseReader{}, nil
|
||||
func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, stream bool) (docker.StatsResponseReader, error) {
|
||||
return docker.StatsResponseReader{}, nil
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error {
|
||||
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options docker.StartOptions) error {
|
||||
|
||||
args := m.Called(ctx, containerID, options)
|
||||
err := args.Get(0)
|
||||
@@ -63,7 +62,7 @@ func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, op
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
func (m *mockedProxy) ContainerStop(ctx context.Context, containerID string, options docker.StopOptions) error {
|
||||
args := m.Called(ctx, containerID, options)
|
||||
err := args.Get(0)
|
||||
|
||||
@@ -74,7 +73,7 @@ func (m *mockedProxy) ContainerStop(ctx context.Context, containerID string, opt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string, options docker.StopOptions) error {
|
||||
|
||||
args := m.Called(ctx, containerID, options)
|
||||
err := args.Get(0)
|
||||
@@ -89,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, Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers(context.Background(), 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.")
|
||||
|
||||
@@ -101,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, Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers(context.Background(), ContainerFilter{})
|
||||
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
|
||||
assert.Nil(t, list, "list should be nil")
|
||||
require.Error(t, err, "test.")
|
||||
|
||||
@@ -111,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"},
|
||||
@@ -124,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, Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers(context.Background(), ContainerFilter{})
|
||||
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
|
||||
Ids := []string{"1234567890_a", "abcdefghijkl"}
|
||||
@@ -149,7 +148,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
||||
|
||||
reader := io.NopCloser(bytes.NewReader(b))
|
||||
since := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
options := container.LogsOptions{
|
||||
options := docker.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
@@ -158,8 +157,8 @@ 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, Host{ID: "localhost"}, system.Info{}}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, since, STDALL)
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, since, container.STDALL)
|
||||
|
||||
actual, _ := io.ReadAll(logReader)
|
||||
assert.Equal(t, string(b), string(actual), "message doesn't match expected")
|
||||
@@ -172,9 +171,9 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
|
||||
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
|
||||
|
||||
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
|
||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
reader, err := client.ContainerLogs(context.Background(), id, time.Time{}, STDALL)
|
||||
reader, err := client.ContainerLogs(context.Background(), id, time.Time{}, container.STDALL)
|
||||
|
||||
assert.Nil(t, reader, "reader should be nil")
|
||||
assert.Error(t, err, "error should have been returned")
|
||||
@@ -184,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: &container.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, 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")
|
||||
@@ -200,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, 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")
|
||||
@@ -211,24 +210,24 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
|
||||
func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
client := &httpClient{proxy, 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: &container.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)
|
||||
proxy.On("ContainerStop", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
|
||||
proxy.On("ContainerRestart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
|
||||
|
||||
container, err := client.FindContainer(context.Background(), "abcdefghijkl")
|
||||
c, err := client.FindContainer(context.Background(), "abcdefghijkl")
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
|
||||
assert.Equal(t, container.ID, "abcdefghijkl")
|
||||
assert.Equal(t, c.ID, "abcdefghijkl")
|
||||
|
||||
actions := []string{"start", "stop", "restart"}
|
||||
for _, action := range actions {
|
||||
err := client.ContainerActions(context.Background(), ContainerAction(action), container.ID)
|
||||
err := client.ContainerActions(context.Background(), container.ContainerAction(action), c.ID)
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
assert.Equal(t, err, nil)
|
||||
}
|
||||
@@ -239,18 +238,18 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
func Test_dockerClient_ContainerActions_error(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
client := &httpClient{proxy, 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"))
|
||||
|
||||
container, err := client.FindContainer(context.Background(), "random-id")
|
||||
c, err := client.FindContainer(context.Background(), "random-id")
|
||||
require.Error(t, err, "error should be thrown")
|
||||
|
||||
actions := []string{"start", "stop", "restart"}
|
||||
for _, action := range actions {
|
||||
err := client.ContainerActions(context.Background(), ContainerAction(action), container.ID)
|
||||
err := client.ContainerActions(context.Background(), container.ContainerAction(action), c.ID)
|
||||
require.Error(t, err, "error should be thrown")
|
||||
assert.Error(t, err, "error should have been returned")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -100,7 +101,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
|
||||
sc.mu.Unlock()
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(parentCtx, 3*time.Second) // 3 seconds to list containers is hard limit
|
||||
if containers, err := sc.client.ListContainers(timeoutCtx, sc.filter); err == nil {
|
||||
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():
|
||||
|
||||
@@ -4,39 +4,70 @@ 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",
|
||||
State: "running",
|
||||
},
|
||||
}, nil)
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).
|
||||
Return(nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
ctx := args.Get(0).(context.Context)
|
||||
<-ctx.Done()
|
||||
})
|
||||
client.On("ContainerStats", mock.Anything, mock.Anything, mock.AnythingOfType("chan<- docker.ContainerStat")).
|
||||
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)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -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, docker.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
|
||||
}
|
||||
@@ -2,12 +2,12 @@ package cli
|
||||
|
||||
import (
|
||||
"github.com/amir20/dozzle/internal/analytics"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func StartEvent(args Args, mode string, client docker.Client, subCommand string) {
|
||||
func StartEvent(args Args, mode string, client container.Client, subCommand string) {
|
||||
if args.NoAnalytics {
|
||||
return
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func StartEvent(args Args, mode string, client docker.Client, subCommand string)
|
||||
host := client.Host()
|
||||
event.ServerID = host.ID
|
||||
event.ServerVersion = host.DockerVersion
|
||||
event.IsSwarmMode = client.SystemInfo().Swarm.NodeID != ""
|
||||
event.IsSwarmMode = host.Swarm
|
||||
} else {
|
||||
event.ServerID = "n/a"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -27,31 +28,17 @@ type Args struct {
|
||||
RemoteAgent []string `arg:"env:DOZZLE_REMOTE_AGENT,--remote-agent,separate" help:"list of agents to connect remotely"`
|
||||
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
|
||||
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:"3s" help:"sets the timeout for docker client"`
|
||||
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 {
|
||||
|
||||
@@ -4,19 +4,21 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
|
||||
"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) (docker.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`)
|
||||
}
|
||||
|
||||
for _, remoteHost := range args.RemoteHost {
|
||||
host, err := docker.ParseConnection(remoteHost)
|
||||
host, err := container.ParseConnection(remoteHost)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Interface("host", remoteHost).Msg("Could not parse remote host")
|
||||
}
|
||||
@@ -46,6 +48,7 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (docker.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)
|
||||
@@ -54,5 +57,5 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (docker.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
|
||||
}
|
||||