Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f81f355bb | |||
| 25cb3c4c53 | |||
| 1d4c162fba | |||
| 19a83b7b62 | |||
| 454156ae2c | |||
| 2d8b263df4 | |||
| fc8b67a902 | |||
| b8869784d4 | |||
| 099c053341 | |||
| bd86b2289b | |||
| 8677a34087 | |||
| 2c398cc227 | |||
| bb0ca30b81 | |||
| f9eb8186d1 | |||
| d767be3757 | |||
| 86f9839211 | |||
| 67f25b7bb0 | |||
| 025ce28134 | |||
| 25fced7bb9 | |||
| ef636c86fc | |||
| 808abd57bb | |||
| d07dc2e983 | |||
| 099f778ca2 | |||
| 9a760a2738 | |||
| b53c6c8275 | |||
| 8dbe9a5774 | |||
| 5d28917d11 | |||
| 586d717aff | |||
| 8aba7debe8 | |||
| c6647133dd | |||
| 82765f4b68 | |||
| 0c33d0f921 | |||
| 89fe9e6b91 | |||
| ef359d339c | |||
| 9d2a8af435 | |||
| 6f96303f1d | |||
| 48d7109079 | |||
| ccd7f0dd86 | |||
| 390d1a3062 | |||
| c6c2c13404 | |||
| f80dd56bdf | |||
| 89babeab9a | |||
| 9abab4ce77 | |||
| 19c3daf2f7 | |||
| 04fb4f3fa4 | |||
| c5680a9b3f |
@@ -60,6 +60,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Go Tests with Coverage
|
||||
run: make test SKIP_ASSET=1
|
||||
- name: Stactic checker
|
||||
uses: dominikh/staticcheck-action@v1.3.0
|
||||
with:
|
||||
install-go: false
|
||||
int-test:
|
||||
name: Integration Tests
|
||||
timeout-minutes: 60
|
||||
|
||||
@@ -1 +1 @@
|
||||
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run main.go --level debug
|
||||
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run -race main.go --level debug
|
||||
|
||||
@@ -14,7 +14,7 @@ COPY package.json ./
|
||||
RUN pnpm install --offline --ignore-scripts --no-optional
|
||||
|
||||
# Copy assets and translations to build
|
||||
COPY .* *.config.ts *.config.js *.config.cts ./
|
||||
COPY .* *.config.ts *.config.js *.config.cjs ./
|
||||
COPY assets ./assets
|
||||
COPY locales ./locales
|
||||
COPY public ./public
|
||||
|
||||
@@ -15,7 +15,7 @@ fake_assets:
|
||||
|
||||
.PHONY: test
|
||||
test: fake_assets
|
||||
go test -cover ./...
|
||||
go test -cover -race ./...
|
||||
|
||||
.PHONY: build
|
||||
build: dist
|
||||
|
||||
@@ -31,7 +31,7 @@ Dozzle is a small container (4 MB compressed). Pull the latest release with:
|
||||
|
||||
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
|
||||
|
||||
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
|
||||
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock:ro -p 8888:8080 amir20/dozzle:latest
|
||||
|
||||
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/).
|
||||
|
||||
@@ -43,7 +43,7 @@ Here is the Docker Compose file:
|
||||
container_name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
ports:
|
||||
- 8888:8080
|
||||
|
||||
@@ -76,7 +76,7 @@ If it's not enabled please follow [this tutorial](https://github.com/containers/
|
||||
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 8888:8080 amir20/dozzle:latest
|
||||
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock:ro -d -p 8888:8080 amir20/dozzle:latest
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
@@ -20,6 +20,7 @@ declare global {
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const automaticRedirect: typeof import('./stores/settings')['automaticRedirect']
|
||||
const collapseNav: typeof import('./stores/settings')['collapseNav']
|
||||
const compact: typeof import('./stores/settings')['compact']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
@@ -69,6 +70,7 @@ declare global {
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const lightTheme: typeof import('./stores/settings')['lightTheme']
|
||||
const locale: typeof import('./stores/settings')['locale']
|
||||
const logSearchContext: typeof import('./composable/logSearchContext')['logSearchContext']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
@@ -288,6 +290,7 @@ declare global {
|
||||
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSimpleRefHistory: typeof import('./utils/index')['useSimpleRefHistory']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
@@ -358,6 +361,7 @@ declare global {
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
|
||||
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
|
||||
@@ -374,6 +378,7 @@ declare module 'vue' {
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
|
||||
readonly compact: UnwrapRef<typeof import('./stores/settings')['compact']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
@@ -423,6 +428,7 @@ declare module 'vue' {
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
|
||||
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
@@ -641,6 +647,7 @@ declare module 'vue' {
|
||||
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
@@ -704,6 +711,7 @@ declare module 'vue' {
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
|
||||
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
|
||||
@@ -720,6 +728,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
|
||||
readonly compact: UnwrapRef<typeof import('./stores/settings')['compact']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
@@ -769,6 +778,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
|
||||
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
@@ -987,6 +997,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
!
|
||||
<template>
|
||||
<div class="text-right">
|
||||
Show per page
|
||||
<dropdown-menu
|
||||
class="dropdown-left btn-xs md:btn-sm"
|
||||
v-model="perPage"
|
||||
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
|
||||
v-if="containers.length > 0"
|
||||
/>
|
||||
</div>
|
||||
<table class="table table-lg bg-base">
|
||||
<thead>
|
||||
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
|
||||
@@ -95,12 +104,14 @@ const fields = {
|
||||
},
|
||||
};
|
||||
|
||||
const { containers, perPage = 15 } = defineProps<{
|
||||
const { containers } = defineProps<{
|
||||
containers: Container[];
|
||||
perPage?: number;
|
||||
}>();
|
||||
type keys = keyof typeof fields;
|
||||
|
||||
const perPage = useStorage("DOZZLE_TABLE_PAGE_SIZE", 15);
|
||||
const pageSizes = [15, 30, 50, 100];
|
||||
|
||||
const storage = useStorage<{ column: keys; direction: 1 | -1 }>("DOZZLE_TABLE_CONTAINERS_SORT", {
|
||||
column: "created",
|
||||
direction: -1,
|
||||
@@ -115,12 +126,12 @@ const sortedContainers = computedWithControl(
|
||||
},
|
||||
);
|
||||
|
||||
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage));
|
||||
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage.value));
|
||||
const isPaginated = computed(() => totalPages.value > 1);
|
||||
const currentPage = ref(1);
|
||||
const paginated = computed(() => {
|
||||
const start = (currentPage.value - 1) * perPage;
|
||||
const end = start + perPage;
|
||||
const start = (currentPage.value - 1) * perPage.value;
|
||||
const end = start + perPage.value;
|
||||
|
||||
return sortedContainers.value.slice(start, end);
|
||||
});
|
||||
|
||||
@@ -31,9 +31,9 @@ function createFuzzySearchModal() {
|
||||
initialState: {
|
||||
container: {
|
||||
containers: [
|
||||
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running"),
|
||||
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running"),
|
||||
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "exited"),
|
||||
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running", []),
|
||||
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running", []),
|
||||
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "exited", []),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -94,15 +94,15 @@ const data = computed(() => {
|
||||
return results.value
|
||||
.toSorted((a, b) => {
|
||||
if (a.score === b.score) {
|
||||
if (a.item.state === "running" && b.item.state !== "running") {
|
||||
if (a.item.state === b.item.state) {
|
||||
return b.item.created - a.item.created;
|
||||
} else if (a.item.state === "running" && b.item.state !== "running") {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else if (a.score && b.score) {
|
||||
return a.score - b.score;
|
||||
} else {
|
||||
return 0;
|
||||
return a.score - b.score;
|
||||
}
|
||||
})
|
||||
.slice(0, maxResults);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-light capitalize"> Load </span>
|
||||
<span class="font-semibold"> {{ container.stat.cpu }}% </span>
|
||||
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-light capitalize"> MEM </span>
|
||||
|
||||
@@ -11,11 +11,11 @@ const { container } = useContainerContext();
|
||||
const cpuData = computedWithControl(
|
||||
() => container.value.stat,
|
||||
() => {
|
||||
const history = container.value.statHistory;
|
||||
const history = container.value.statsHistory;
|
||||
const points: Point<unknown>[] = history.map((stat, i) => ({
|
||||
x: i,
|
||||
y: Math.max(0, stat.snapshot.cpu),
|
||||
value: Math.max(0, stat.snapshot.cpu).toFixed(2) + "%",
|
||||
y: Math.max(0, stat.cpu),
|
||||
value: Math.max(0, stat.cpu).toFixed(2) + "%",
|
||||
}));
|
||||
return points;
|
||||
},
|
||||
@@ -24,11 +24,11 @@ const cpuData = computedWithControl(
|
||||
const memoryData = computedWithControl(
|
||||
() => container.value.stat,
|
||||
() => {
|
||||
const history = container.value.statHistory;
|
||||
const history = container.value.statsHistory;
|
||||
const points: Point<string>[] = history.map((stat, i) => ({
|
||||
x: i,
|
||||
y: stat.snapshot.memory,
|
||||
value: formatBytes(stat.snapshot.memoryUsage),
|
||||
y: stat.memory,
|
||||
value: formatBytes(stat.memoryUsage),
|
||||
}));
|
||||
return points;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true }">
|
||||
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }">
|
||||
<li
|
||||
v-for="item in messages"
|
||||
:key="item.id"
|
||||
@@ -57,5 +57,11 @@ defineProps<{
|
||||
&.large {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
&.compact {
|
||||
> li {
|
||||
@apply py-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { area, curveStep } from "d3-shape";
|
||||
|
||||
const d3 = { extent, scaleLinear, area, curveStep };
|
||||
const { data, width = 150, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
|
||||
const x = d3.scaleLinear().range([width, 0]);
|
||||
const x = d3.scaleLinear().range([0, width]);
|
||||
const y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
const selectedPoint = defineEmit<[value: Point<unknown>]>();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="show && (delayedShow || glopbalShow)"
|
||||
class="fixed z-50 rounded border border-secondary/50 bg-base-lighter p-4 shadow"
|
||||
class="fixed z-50 rounded border border-base-content/20 bg-base-lighter p-4 shadow"
|
||||
ref="content"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<template>
|
||||
<transition name="slide">
|
||||
<div class="fixed right-4 top-2 z-10 flex items-center" v-show="showSearch" v-if="search">
|
||||
<div
|
||||
class="fixed z-10 flex w-full justify-end p-2"
|
||||
v-show="showSearch"
|
||||
v-if="search"
|
||||
ref="container"
|
||||
:style="style"
|
||||
>
|
||||
<div class="input input-primary flex h-auto items-center !shadow-lg">
|
||||
<mdi:magnify />
|
||||
<input
|
||||
@@ -19,8 +25,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
const input = ref<HTMLInputElement>();
|
||||
const container = ref<HTMLDivElement>();
|
||||
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
|
||||
|
||||
const { style } = useDraggable(container);
|
||||
|
||||
onKeyStroke("f", (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
showSearch.value = true;
|
||||
|
||||
@@ -20,14 +20,11 @@
|
||||
<transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
|
||||
<li
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
:class="isLabel(item) ? 'menu-title' : item.state"
|
||||
:data-testid="item.id"
|
||||
:key="isContainer(item) ? item.id : item.keyLabel"
|
||||
:class="isContainer(item) ? item.state : 'menu-title'"
|
||||
:data-testid="isContainer(item) ? null : item.keyLabel"
|
||||
>
|
||||
<template v-if="isLabel(item)">
|
||||
{{ item.label }}
|
||||
</template>
|
||||
<popup v-else>
|
||||
<popup v-if="isContainer(item)">
|
||||
<router-link
|
||||
:to="{ name: 'container-id', params: { id: item.id } }"
|
||||
active-class="active-primary"
|
||||
@@ -51,6 +48,9 @@
|
||||
<container-popup :container="item"></container-popup>
|
||||
</template>
|
||||
</popup>
|
||||
<template v-else>
|
||||
{{ $t(item.keyLabel) }}
|
||||
</template>
|
||||
</li>
|
||||
</transition-group>
|
||||
</transition>
|
||||
@@ -65,8 +65,6 @@
|
||||
import { Container } from "@/models/Container";
|
||||
import { sessionHost } from "@/composable/storage";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const store = useContainerStore();
|
||||
|
||||
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
|
||||
@@ -104,15 +102,13 @@ const groupedContainers = computed(() =>
|
||||
),
|
||||
);
|
||||
|
||||
type MenuLabel = { label: string; id: string; state: string };
|
||||
const pinnedLabel = { label: t("label.pinned"), id: "pinned", state: "label" } as MenuLabel;
|
||||
const allLabel = { label: t("label.containers"), id: "containers", state: "label" } as MenuLabel;
|
||||
|
||||
function isLabel(item: Container | MenuLabel): item is MenuLabel {
|
||||
return (item as MenuLabel).label !== undefined;
|
||||
function isContainer(item: any): item is Container {
|
||||
return item.hasOwnProperty("image");
|
||||
}
|
||||
|
||||
const menuItems = computed(() => {
|
||||
const pinnedLabel = { keyLabel: "label.pinned" };
|
||||
const allLabel = { keyLabel: showAllContainers.value ? "label.all-containers" : "label.running-containers" };
|
||||
if (groupedContainers.value.pinned.length > 0) {
|
||||
return [pinnedLabel, ...groupedContainers.value.pinned, allLabel, ...groupedContainers.value.unpinned];
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</label>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="min-w-52 dropdown-content rounded-box z-50 mt-1 border border-base-content/20 bg-base p-2 shadow"
|
||||
class="dropdown-content z-50 mt-1 min-w-52 rounded-box border border-base-content/20 bg-base p-2 shadow"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<details class="dropdown" ref="details" v-on-click-outside="close">
|
||||
<summary class="btn btn-primary flex-nowrap" v-bind="$attrs">
|
||||
<slot name="trigger"> {{ values[modelValue] ?? defaultLabel }} <carbon:caret-down /></slot>
|
||||
<slot name="trigger"> {{ label }} <carbon:caret-down /></slot>
|
||||
</summary>
|
||||
<ul class="menu dropdown-content rounded-box z-50 mt-1 w-52 border border-base-content/20 bg-base p-2 shadow">
|
||||
<ul class="menu dropdown-content z-50 mt-1 w-52 rounded-box border border-base-content/20 bg-base p-2 shadow">
|
||||
<slot>
|
||||
<li v-for="item in options">
|
||||
<a @click="modelValue = item.value">
|
||||
@@ -17,27 +17,23 @@
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="T">
|
||||
import { vOnClickOutside } from "@vueuse/components";
|
||||
type DropdownItem = {
|
||||
label: string;
|
||||
value: string;
|
||||
value: T;
|
||||
};
|
||||
const { options = [], defaultLabel = "" } = defineProps<{ options?: DropdownItem[]; defaultLabel?: string }>();
|
||||
|
||||
const { modelValue } = defineModels<{
|
||||
modelValue: string;
|
||||
modelValue: T;
|
||||
}>();
|
||||
|
||||
const values = computed(() =>
|
||||
options.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.value] = curr.label;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
),
|
||||
);
|
||||
const { options = [], defaultLabel = "" } = defineProps<{
|
||||
options?: DropdownItem[];
|
||||
defaultLabel?: string;
|
||||
}>();
|
||||
|
||||
const label = computed(() => options.find((item) => item.value === modelValue.value)?.label ?? defaultLabel);
|
||||
const details = ref<HTMLElement | null>(null);
|
||||
const close = () => details.value?.removeAttribute("open");
|
||||
watch(modelValue, () => close());
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("Container", () => {
|
||||
];
|
||||
|
||||
test.each(names)("name %s should be %s and %s", (name, expectedName, expectedSwarmId) => {
|
||||
const c = new Container("id", new Date(), "image", name!, "command", "host", {}, "status", "created");
|
||||
const c = new Container("id", new Date(), "image", name!, "command", "host", {}, "status", "created", []);
|
||||
expect(c.name).toBe(expectedName);
|
||||
expect(c.swarmId).toBe(expectedSwarmId);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
|
||||
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
|
||||
import { useExponentialMovingAverage } from "@/utils";
|
||||
import { useExponentialMovingAverage, useSimpleRefHistory } from "@/utils";
|
||||
import { Ref } from "vue";
|
||||
|
||||
type Stat = Omit<ContainerStat, "id">;
|
||||
@@ -19,7 +18,7 @@ const hosts = computed(() =>
|
||||
|
||||
export class Container {
|
||||
private _stat: Ref<Stat>;
|
||||
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
|
||||
private readonly _statsHistory: Ref<Stat[]>;
|
||||
public readonly swarmId: string | null = null;
|
||||
public readonly isSwarm: boolean = false;
|
||||
private readonly movingAverageStat: Ref<Stat>;
|
||||
@@ -34,10 +33,11 @@ export class Container {
|
||||
public readonly labels = {} as Record<string, string>,
|
||||
public status: string,
|
||||
public state: ContainerState,
|
||||
stats: Stat[],
|
||||
public health?: ContainerHealth,
|
||||
) {
|
||||
this._stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
|
||||
this.throttledStatHistory = useThrottledRefHistory(this._stat, { capacity: 300, deep: true, throttle: 1000 });
|
||||
this._statsHistory = useSimpleRefHistory(this._stat, { capacity: 300, deep: true, initial: stats });
|
||||
this.movingAverageStat = useExponentialMovingAverage(this._stat, 0.2);
|
||||
|
||||
const match = name.match(SWARM_ID_REGEX);
|
||||
@@ -48,8 +48,8 @@ export class Container {
|
||||
}
|
||||
}
|
||||
|
||||
get statHistory() {
|
||||
return unref(this.throttledStatHistory.history);
|
||||
get statsHistory() {
|
||||
return unref(this._statsHistory);
|
||||
}
|
||||
|
||||
get movingAverage() {
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { type App } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { locale } from "@/stores/settings";
|
||||
|
||||
import messages from "@intlify/unplugin-vue-i18n/messages";
|
||||
|
||||
const locale = messages?.hasOwnProperty(navigator.language) ? navigator.language : navigator.language.slice(0, 2);
|
||||
const defaultLocale = messages?.hasOwnProperty(navigator.language)
|
||||
? navigator.language
|
||||
: navigator.language.slice(0, 2);
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: locale,
|
||||
locale: locale.value || defaultLocale,
|
||||
fallbackLocale: "en",
|
||||
messages,
|
||||
});
|
||||
|
||||
export const install = (app: App) => {
|
||||
app.use(i18n);
|
||||
};
|
||||
watch(locale, (value) => {
|
||||
i18n.global.locale.value = value || defaultLocale;
|
||||
});
|
||||
|
||||
export const install = (app: App) => app.use(i18n);
|
||||
export default i18n;
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
<section class="grid-cols-2 gap-4 @3xl:grid">
|
||||
<div class="flex flex-col gap-2 text-balance @3xl:pr-8">
|
||||
<toggle v-model="compact"> {{ $t("settings.compact") }} </toggle>
|
||||
|
||||
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
|
||||
|
||||
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
|
||||
@@ -29,6 +31,21 @@
|
||||
|
||||
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
|
||||
|
||||
<labeled-input>
|
||||
<template #label>
|
||||
{{ $t("settings.locale") }}
|
||||
</template>
|
||||
<template #input>
|
||||
<dropdown-menu
|
||||
v-model="locale"
|
||||
:options="[
|
||||
{ label: 'Auto', value: '' },
|
||||
...availableLocales.map((l) => ({ label: l.toLocaleUpperCase(), value: l })),
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</labeled-input>
|
||||
|
||||
<labeled-input>
|
||||
<template #label>
|
||||
{{ $t("settings.datetime-format") }}
|
||||
@@ -124,6 +141,7 @@ import { ComplexLogEntry, SimpleLogEntry } from "@/models/LogEntry";
|
||||
|
||||
import {
|
||||
automaticRedirect,
|
||||
compact,
|
||||
hourStyle,
|
||||
dateLocale,
|
||||
lightTheme,
|
||||
@@ -134,20 +152,26 @@ import {
|
||||
size,
|
||||
smallerScrollbars,
|
||||
softWrap,
|
||||
locale,
|
||||
} from "@/stores/settings";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, availableLocales } = useI18n();
|
||||
|
||||
setTitle(t("title.settings"));
|
||||
const { latest, hasUpdate } = useReleases();
|
||||
|
||||
const keys = ref<string[][]>([]);
|
||||
const hoursAgo = (hours: number) => {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - hours);
|
||||
return date;
|
||||
};
|
||||
|
||||
const fakeMessages = [
|
||||
new SimpleLogEntry("This is a preview of the logs", 1, new Date(), "info", undefined, "stdout"),
|
||||
new SimpleLogEntry("A warning log looks like this", 2, new Date(), "warn", undefined, "stdout"),
|
||||
new SimpleLogEntry("This is a multi line error message", 3, new Date(), "error", "start", "stderr"),
|
||||
new SimpleLogEntry("with a second line", 4, new Date(), "error", "middle", "stderr"),
|
||||
new SimpleLogEntry("This is a preview of the logs", 1, hoursAgo(16), "info", undefined, "stdout"),
|
||||
new SimpleLogEntry("A warning log looks like this", 2, hoursAgo(12), "warn", undefined, "stdout"),
|
||||
new SimpleLogEntry("This is a multi line error message", 3, hoursAgo(7), "error", "start", "stderr"),
|
||||
new SimpleLogEntry("with a second line", 4, hoursAgo(2), "error", "middle", "stderr"),
|
||||
new SimpleLogEntry("and finally third line.", 5, new Date(), "error", "end", "stderr"),
|
||||
new ComplexLogEntry(
|
||||
{
|
||||
@@ -164,7 +188,7 @@ const fakeMessages = [
|
||||
keys,
|
||||
),
|
||||
new SimpleLogEntry(
|
||||
"This is a very very long message which would wrap by default. Disabling soft wraps would disable this.",
|
||||
"This is a very very long message which would wrap by default. Disabling soft wraps would disable this. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
|
||||
7,
|
||||
new Date(),
|
||||
"debug",
|
||||
|
||||
@@ -64,7 +64,7 @@ export const useContainerStore = defineStore("container", () => {
|
||||
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
|
||||
const container = allContainersById.value[event.actorId];
|
||||
if (container) {
|
||||
container.state = "dead";
|
||||
container.state = "exited";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,6 +127,7 @@ export const useContainerStore = defineStore("container", () => {
|
||||
c.labels,
|
||||
c.status,
|
||||
c.state,
|
||||
c.stats,
|
||||
c.health,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { toRefs } from "@vueuse/core";
|
||||
export type Settings = {
|
||||
search: boolean;
|
||||
size: "small" | "medium" | "large";
|
||||
compact: boolean;
|
||||
menuWidth: number;
|
||||
smallerScrollbars: boolean;
|
||||
showTimestamp: boolean;
|
||||
@@ -14,9 +15,11 @@ export type Settings = {
|
||||
softWrap: boolean;
|
||||
collapseNav: boolean;
|
||||
automaticRedirect: boolean;
|
||||
locale: string;
|
||||
};
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
search: true,
|
||||
compact: false,
|
||||
size: "medium",
|
||||
menuWidth: 15,
|
||||
smallerScrollbars: false,
|
||||
@@ -29,12 +32,14 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
softWrap: true,
|
||||
collapseNav: false,
|
||||
automaticRedirect: true,
|
||||
locale: "",
|
||||
};
|
||||
|
||||
export const settings = useProfileStorage("settings", DEFAULT_SETTINGS);
|
||||
|
||||
export const {
|
||||
collapseNav,
|
||||
compact,
|
||||
softWrap,
|
||||
hourStyle,
|
||||
dateLocale,
|
||||
@@ -46,5 +51,6 @@ export const {
|
||||
menuWidth,
|
||||
size,
|
||||
search,
|
||||
locale,
|
||||
automaticRedirect,
|
||||
} = toRefs(settings);
|
||||
|
||||
@@ -15,6 +15,7 @@ export type ContainerJson = {
|
||||
readonly state: ContainerState;
|
||||
readonly host: string;
|
||||
readonly labels: Record<string, string>;
|
||||
readonly stats: ContainerStat[];
|
||||
readonly health?: ContainerHealth;
|
||||
};
|
||||
|
||||
|
||||
@@ -51,3 +51,27 @@ export function useExponentialMovingAverage<T extends Record<string, number>>(so
|
||||
|
||||
return ema;
|
||||
}
|
||||
|
||||
interface UseSimpleRefHistoryOptions<T> {
|
||||
capacity: number;
|
||||
deep?: boolean;
|
||||
initial?: T[];
|
||||
}
|
||||
|
||||
export function useSimpleRefHistory<T>(source: Ref<T>, options: UseSimpleRefHistoryOptions<T>) {
|
||||
const { capacity, deep = true, initial = [] as T[] } = options;
|
||||
const history = ref<T[]>(initial) as Ref<T[]>;
|
||||
|
||||
watch(
|
||||
source,
|
||||
(value) => {
|
||||
history.value.push(value);
|
||||
if (history.value.length > capacity) {
|
||||
history.value.shift();
|
||||
}
|
||||
},
|
||||
{ deep },
|
||||
);
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
custom_base:
|
||||
container_name: custom_base
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- DOZZLE_FILTER=name=custom_base
|
||||
- DOZZLE_BASE=/foobarbase
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
simple-auth:
|
||||
container_name: simple-auth
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./e2e/data:/data
|
||||
environment:
|
||||
- DOZZLE_AUTH_PROVIDER=simple
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
dozzle:
|
||||
container_name: dozzle
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- DOZZLE_FILTER=name=dozzle
|
||||
- DOZZLE_NO_ANALYTICS=1
|
||||
@@ -50,7 +50,7 @@ services:
|
||||
container_name: proxy
|
||||
image: tecnativa/docker-socket-proxy
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- CONTAINERS=1
|
||||
ports:
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
|
||||
playwright:
|
||||
container_name: playwright
|
||||
image: mcr.microsoft.com/playwright:v1.40.1-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.2-jammy
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"type": "module",
|
||||
"name": "docs",
|
||||
"devDependencies": {
|
||||
"@unocss/preset-typography": "^0.58.3",
|
||||
"@unocss/reset": "^0.58.3",
|
||||
"@unocss/transformer-directives": "^0.58.3",
|
||||
"@unocss/preset-typography": "^0.58.4",
|
||||
"@unocss/reset": "^0.58.4",
|
||||
"@unocss/transformer-directives": "^0.58.4",
|
||||
"dozzle": "workspace:*",
|
||||
"sitemap": "^7.1.1",
|
||||
"unocss": "^0.58.3"
|
||||
"unocss": "^0.58.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ settings:
|
||||
|
||||
devDependencies:
|
||||
'@unocss/preset-typography':
|
||||
specifier: ^0.58.3
|
||||
version: 0.58.3
|
||||
specifier: ^0.58.4
|
||||
version: 0.58.4
|
||||
'@unocss/reset':
|
||||
specifier: ^0.58.3
|
||||
version: 0.58.3
|
||||
specifier: ^0.58.4
|
||||
version: 0.58.4
|
||||
'@unocss/transformer-directives':
|
||||
specifier: ^0.58.3
|
||||
version: 0.58.3
|
||||
specifier: ^0.58.4
|
||||
version: 0.58.4
|
||||
dozzle:
|
||||
specifier: workspace:*
|
||||
version: link:..
|
||||
@@ -21,8 +21,8 @@ devDependencies:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.1
|
||||
unocss:
|
||||
specifier: ^0.58.3
|
||||
version: 0.58.3(postcss@8.4.32)(vite@5.0.10)
|
||||
specifier: ^0.58.4
|
||||
version: 0.58.4(postcss@8.4.32)(vite@5.0.10)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -569,15 +569,15 @@ packages:
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
dev: true
|
||||
|
||||
/@iconify/utils@2.1.14:
|
||||
resolution: {integrity: sha512-9pKIntkbLbjVVFxH32td21Am3AGGJfyI2KY2d8yDQxkZe4BBZtufJI8NgcamFn8B5QKLU9ai2VMo8OEov8jAtw==}
|
||||
/@iconify/utils@2.1.20:
|
||||
resolution: {integrity: sha512-t8TeKlYK/5i9yTY9VAGAE4P0qQHd/0vH+VSRO+bdpxlt8wqB6f2I0/IrciRsdeFZPMoL8IICgP7lgl2ZtbG8Tw==}
|
||||
dependencies:
|
||||
'@antfu/install-pkg': 0.1.1
|
||||
'@antfu/utils': 0.7.7
|
||||
'@iconify/types': 2.0.0
|
||||
debug: 4.3.4
|
||||
kolorist: 1.8.0
|
||||
local-pkg: 0.4.3
|
||||
local-pkg: 0.5.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -769,212 +769,212 @@ packages:
|
||||
'@types/node': 17.0.45
|
||||
dev: true
|
||||
|
||||
/@unocss/astro@0.58.3(vite@5.0.10):
|
||||
resolution: {integrity: sha512-qJL+XkWYJhEIX4AmOtbfb2Zu4holTDpRscfvVci4T+2VWjyE3mgtsyNzi9ZChe/hdEPRa7g26gSpNQeMhjh/Kw==}
|
||||
/@unocss/astro@0.58.4(vite@5.0.10):
|
||||
resolution: {integrity: sha512-feS8+f3oPmCeR1XF7isQjs3Z9ojM5Ssv0vCNR/dexPFdROfccK/7sIu1YnHWtVg1trPc1kMfI8XJRqfrHMdd5w==}
|
||||
peerDependencies:
|
||||
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/reset': 0.58.3
|
||||
'@unocss/vite': 0.58.3(vite@5.0.10)
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/reset': 0.58.4
|
||||
'@unocss/vite': 0.58.4(vite@5.0.10)
|
||||
vite: 5.0.10
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/@unocss/cli@0.58.3:
|
||||
resolution: {integrity: sha512-veGdHhwm7GRvruXCMdqoFu3wVozr7ELEVWsFB6GpqWdGYIJ1i72M18l72UHDA2+TuDillZONnAQ5AvX9x/sYAw==}
|
||||
/@unocss/cli@0.58.4:
|
||||
resolution: {integrity: sha512-8dAs1TzzWCtb3FxoTsVtR2+JGkdmchuP0RvGlSywADf5FqYn9nbP0m3z4STtpPavHO9JaGbabStyjS0nA8Ck6w==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.2.1
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
'@unocss/config': 0.58.3
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/preset-uno': 0.58.3
|
||||
'@unocss/config': 0.58.4
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/preset-uno': 0.58.4
|
||||
cac: 6.7.14
|
||||
chokidar: 3.5.3
|
||||
colorette: 2.0.20
|
||||
consola: 3.2.3
|
||||
fast-glob: 3.3.2
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.1
|
||||
pathe: 1.1.2
|
||||
perfect-debounce: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/@unocss/config@0.58.3:
|
||||
resolution: {integrity: sha512-8BQDoLzf/BkyfnkQsjnXI84oj+Spqkr7Bf2AbOGcX14vof0qqHSDvJXQV1e0u7jv2QETe2D1+PI4fnkJCumaRw==}
|
||||
/@unocss/config@0.58.4:
|
||||
resolution: {integrity: sha512-b/inDCOnhUPzobhj+SxRI7xeSwoAQU+3D3J5JoWZNPBI4OaJdwpuz40uLXl9VsPz5N6dc8/qa+FrSsgCjUcLTg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
unconfig: 0.3.11
|
||||
dev: true
|
||||
|
||||
/@unocss/core@0.58.3:
|
||||
resolution: {integrity: sha512-9hTxzsrSLh+07ql/lGhE+8ZbE9MTTeZeMx131cPf2jDJUxAZooLE5pBCoK0k77ZJGcribRrwPGkUScBNOK0cYQ==}
|
||||
/@unocss/core@0.58.4:
|
||||
resolution: {integrity: sha512-wh4pxXUCkhC+C/0ct74PMYeZgX+oWuHj2mnvr09nM0WmvOm1HeeISrDCCBGcxmKbErUk+D1v3JgzXF7b6j2l4g==}
|
||||
dev: true
|
||||
|
||||
/@unocss/extractor-arbitrary-variants@0.58.3:
|
||||
resolution: {integrity: sha512-QszC2atLcvzyoZFsjgtMBbILN4lrYI60iVRWdii+GGiKVtoIaKRWiA/3WERkvYGVPseVWOMflUpfxNeq+s9zUw==}
|
||||
/@unocss/extractor-arbitrary-variants@0.58.4:
|
||||
resolution: {integrity: sha512-TQlVtSQk8YdBU9KuTA5k6JBJ8d/aPrIsCWjDPfBvAqDvrll/dzwBCLoLTQMdCHFjCXCOYWKPum1ET+Wn9R+XbA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/inspector@0.58.3:
|
||||
resolution: {integrity: sha512-FqkoHiO23lGGcQ+qJbE1Kb8+kPJWc/LxBz3B4Ehml1vQryncNh4p+3sczVn5YVTfPDGBXBCkP05Q+PJRKabPXQ==}
|
||||
/@unocss/inspector@0.58.4:
|
||||
resolution: {integrity: sha512-BbsaIMqP6dqa3qCfkC0nkvjCg11JRigvFPU4319imTPcTHQNQSg35t2mVFPGZ+QYHlU/3ouBeogUQfJDHBmtVQ==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/rule-utils': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/rule-utils': 0.58.4
|
||||
gzip-size: 6.0.0
|
||||
sirv: 2.0.4
|
||||
dev: true
|
||||
|
||||
/@unocss/postcss@0.58.3(postcss@8.4.32):
|
||||
resolution: {integrity: sha512-y1WQNvLUidypCu/tr6oJfaV4pjd8Lsk1N27ASEVsvockOH3MekRYpHtJfTl2fMk+1Y98AHv7hPAVjM2NlvhDow==}
|
||||
/@unocss/postcss@0.58.4(postcss@8.4.32):
|
||||
resolution: {integrity: sha512-pg2qCGakV1TyMApPdvuvqqmPDhgogPWF14J97BT5zIfGYITAJSmBsm7d3+06w6EuqIS+vcYRw+qCV3oX6qTeiA==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
postcss: ^8.4.21
|
||||
dependencies:
|
||||
'@unocss/config': 0.58.3
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/rule-utils': 0.58.3
|
||||
'@unocss/config': 0.58.4
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/rule-utils': 0.58.4
|
||||
css-tree: 2.3.1
|
||||
fast-glob: 3.3.2
|
||||
magic-string: 0.30.5
|
||||
postcss: 8.4.32
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-attributify@0.58.3:
|
||||
resolution: {integrity: sha512-iDXNfnSC0SI51UnMltHmMcPr2SYYkimo86i+SBQqc/WBGcCF7fFqFj8G2WsZfwHvU9SdAHF8tYIwNq06w1WSeg==}
|
||||
/@unocss/preset-attributify@0.58.4:
|
||||
resolution: {integrity: sha512-r7pDXgcOgGMmrMoqM4/w20KKn4PxF+/vpElatAGXW3V/tHJWFOLGzk+wYeCMgMxeC+vPzqyJkjTfoqYlZrlokQ==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-icons@0.58.3:
|
||||
resolution: {integrity: sha512-SA4Eu4rOQ9+zUgIyK6RacS01ygm0PJWkqKlD8ccrBqEyZapqiU+vLL+v6X8YVjoZjR+5CVgcMD5Km7zEQgqXQw==}
|
||||
/@unocss/preset-icons@0.58.4:
|
||||
resolution: {integrity: sha512-xdOe3PLfEWS4y+rA7Gxh/kDI82VkW0PbdoYO2FBwXH7a0JegdUDD5zuOTsTKAIymvQ7eVH53WXN9mcuhfNmdvQ==}
|
||||
dependencies:
|
||||
'@iconify/utils': 2.1.14
|
||||
'@unocss/core': 0.58.3
|
||||
'@iconify/utils': 2.1.20
|
||||
'@unocss/core': 0.58.4
|
||||
ofetch: 1.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-mini@0.58.3:
|
||||
resolution: {integrity: sha512-vPC97vZPY6J9uZ+KmK4x7atKFlZJPH4tR7+SmzTmguaGIHZJG8k1cjBCg+5M7P4MaxINRMukUQS8/mM/uWFqvQ==}
|
||||
/@unocss/preset-mini@0.58.4:
|
||||
resolution: {integrity: sha512-cpIWUYxLi4VCkkjxpNJOVqH9D32NX1ebRFaoAVnvCRsoj5TRsZJSdxs0WMN7reHT/CUHh0Wh6d3TfMCFxzbZ4g==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/extractor-arbitrary-variants': 0.58.3
|
||||
'@unocss/rule-utils': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/extractor-arbitrary-variants': 0.58.4
|
||||
'@unocss/rule-utils': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-tagify@0.58.3:
|
||||
resolution: {integrity: sha512-9CEh4p8M8zFuNFzmPIs1paExWRcwr0Gp6lSMffFnqaVToeRBgEH7VnRj6/R3ZPAmQ2rEemZ1+3eOQlsspEE6aw==}
|
||||
/@unocss/preset-tagify@0.58.4:
|
||||
resolution: {integrity: sha512-1E3vQvg5Qy4tbgovvrV0JaIXFfcdhK4NCSz+MuUk5ZYgKGVDh4IEw0WPuic534nlLm9+YFqyCSLLyIs3bT/GYQ==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-typography@0.58.3:
|
||||
resolution: {integrity: sha512-hOQa2Sjkxo5v+jMwPXYv1MpHSrirf73FKPqwwHlvEUSCq9iweGDOU/MVOc5fI9qCg0SrfWhIvrZb4ASlgAuzWQ==}
|
||||
/@unocss/preset-typography@0.58.4:
|
||||
resolution: {integrity: sha512-0Opt3Y0At4F03092iwGgz7HswRC2ElBXHR+IO9wWxSbldtpdmKzFJ/GBHEalzQjacWnyyzmRjabjA2quoQ8FQA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/preset-mini': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/preset-mini': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-uno@0.58.3:
|
||||
resolution: {integrity: sha512-E/g2BS4KXS9E/4OqyJSt0xSB6gbbk2VGjgIXrpcSXuDr2S2F29XLVlhJA5HJBADPlEfbo41z7Mk3LA3nQPWxQQ==}
|
||||
/@unocss/preset-uno@0.58.4:
|
||||
resolution: {integrity: sha512-FWBPGIt0UcQHu58l75kvhB4vF2v+s2m0OW9DnIrPm3aY7Qj3q4yA2wdahR37qSsH+6vR3JP4+mV9WBrGjIsjrQ==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/preset-mini': 0.58.3
|
||||
'@unocss/preset-wind': 0.58.3
|
||||
'@unocss/rule-utils': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/preset-mini': 0.58.4
|
||||
'@unocss/preset-wind': 0.58.4
|
||||
'@unocss/rule-utils': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-web-fonts@0.58.3:
|
||||
resolution: {integrity: sha512-g+ru8gX74uZVSfKgdSGp46XQ+wMr66Hp3wtI01yyu9wqmJRAVWQmeehFYZ0hDnGgX20veYSbG+ybZfxIKeTy6w==}
|
||||
/@unocss/preset-web-fonts@0.58.4:
|
||||
resolution: {integrity: sha512-vcy20fIK37GdhesRpiWGvCvkJDQsSiRF1jxw3dy8J5n9kFpIV8DQoPWUIE0ePF4i5ky2dHSDxKaNOP1bxHdKGA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
ofetch: 1.3.3
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-wind@0.58.3:
|
||||
resolution: {integrity: sha512-/YhvKDFGnTNvKxNaBv1dazHaqNmBM0Ulh0U9lhycGz11qsJTQSl/Y9ZP64fVC7fuo+Uiaj8AN/9gpmpVrCgt4A==}
|
||||
/@unocss/preset-wind@0.58.4:
|
||||
resolution: {integrity: sha512-e2F0HsqPXw+nD85MKIZ63mx5yUx5g8iPOtMw9/eEUeK67HrD+jFxeLq0hAM3ou3a4589QIzeg23LXAkM7gCzvg==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/preset-mini': 0.58.3
|
||||
'@unocss/rule-utils': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/preset-mini': 0.58.4
|
||||
'@unocss/rule-utils': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/reset@0.58.3:
|
||||
resolution: {integrity: sha512-Q2KiRQlam2iYsTZgKdvnXEfUN4TA2oVpGIVD9Wa0ggs0XlYj5aOo0g0+4Tgqqn+YaviZQeJKnDs/JWE+ygHhZA==}
|
||||
/@unocss/reset@0.58.4:
|
||||
resolution: {integrity: sha512-ZZTrAdl4WWmMjQdOqcOSWdgFH6kdFKZjPu4c6Ijxk7KvY2BW3nttTTBa7IYeuXFHVfcExUFqlOgRurt+NeWYyQ==}
|
||||
dev: true
|
||||
|
||||
/@unocss/rule-utils@0.58.3:
|
||||
resolution: {integrity: sha512-0Px9gIW+VOKetZuYET19uamIRpk7A9c8sCzQuGlNvCLXKEWamqXz5asLtnvPzw6SwCXEQDgWXE9i+aeoXaM0Jg==}
|
||||
/@unocss/rule-utils@0.58.4:
|
||||
resolution: {integrity: sha512-52Jp4I+joGTaDm7ehB/7uZ2kJL+9BZcYRDUVk4IDacDH5W9yxf1F75LzYT8jJVWXD/HIhiS0r9V6qhcBq2OWZw==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
magic-string: 0.30.5
|
||||
dev: true
|
||||
|
||||
/@unocss/scope@0.58.3:
|
||||
resolution: {integrity: sha512-Bkf6sk/0wry+fa5P8eLnzjC4pdrRlBY29g4F64qjsMBR0gk0stFRNzeoMOk412gmJXWjjlAQgNYiBZDHoPghZw==}
|
||||
/@unocss/scope@0.58.4:
|
||||
resolution: {integrity: sha512-JrX98xoYnv0HN41WyzlvCCU1T3jnDOry+V8mm2dB9DvByzxI484X6gtKbOXlJFAPwOlZPU5Bru/XTDaL6yQQww==}
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-attributify-jsx-babel@0.58.3:
|
||||
resolution: {integrity: sha512-ar+s1rUVHpTy5Yz31WP4DGF2IHxyD4sk/t9ayvR2nOZddAZipdLGSShG03GLkRv4h2/r0x+BIyJGdwAC0BgVZQ==}
|
||||
/@unocss/transformer-attributify-jsx-babel@0.58.4:
|
||||
resolution: {integrity: sha512-yJd+OxAdgAxSQHVrqC6z19ibPqwwWO7J0TFf2caiP3hidSJ0iOvxcC0h62YCMr9dg4MsCpXaiHmGzxXbWGJ9nQ==}
|
||||
dependencies:
|
||||
'@babel/core': 7.23.7
|
||||
'@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.7)
|
||||
'@babel/preset-typescript': 7.23.3(@babel/core@7.23.7)
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-attributify-jsx@0.58.3:
|
||||
resolution: {integrity: sha512-H6wLJ5aAdHz8K/Z9/7OfiCBpOmKM7Gah2YtooT/Vfxu66bGehZO4QF6fcla6St53HifNvZ5odhlzqVEyHvQEaQ==}
|
||||
/@unocss/transformer-attributify-jsx@0.58.4:
|
||||
resolution: {integrity: sha512-Y4YMVkgY/EF7x0llV8Y42WGs8b77hbXkPstRLNeRD5x8KBNKnjYUXADwhTBEKXjDR8/y5Ewie8u43i1nbh1XuA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-compile-class@0.58.3:
|
||||
resolution: {integrity: sha512-VmnByb3N8uGAEXjnfhra3DzKq8ZeVCL30n46GG5RTC03MK0rZmKVOmBOBIB99rmSV+D/WVrb12Gf4fHsoLca7g==}
|
||||
/@unocss/transformer-compile-class@0.58.4:
|
||||
resolution: {integrity: sha512-GPz462ogvtb64W0iweuB0Dcm1snGsGte0RlVpFUeG2eWlVqKMRvNRtYuive2Ky2zSZKefWJQBGk3Ti+7CM4wFA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-directives@0.58.3:
|
||||
resolution: {integrity: sha512-JMfeA8GJz106UqafqsCDp6BBEU7TozZHpLw414CKZjOW1CuMmaKEGrlr2UCjCYgM1vH7KEFKRMwTRUEV3NvywQ==}
|
||||
/@unocss/transformer-directives@0.58.4:
|
||||
resolution: {integrity: sha512-5g2XVhwpmu2IrGHqPwxh+S3ZtkC/AwAtLIBLWB1QNhY3HVzAAaOzcif6uarngrCTTDQSDmsjzm8TSAq26LtCmQ==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/rule-utils': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/rule-utils': 0.58.4
|
||||
css-tree: 2.3.1
|
||||
dev: true
|
||||
|
||||
/@unocss/transformer-variant-group@0.58.3:
|
||||
resolution: {integrity: sha512-/8CyzLwzpJC5cdiA/Wd5/Pg+HEIK+xxJJ3/VXoo93OPNCCbA9/h6DPwDh1ogKk15c6b5H75Ow6zKq1rYQAz2EA==}
|
||||
/@unocss/transformer-variant-group@0.58.4:
|
||||
resolution: {integrity: sha512-zpJ4TMor7aJ3SoLJm4jNBOSqikyXUdQEO1AxkILd9Zcn7JWRgudfFXiXnFP+WGJcs9mMLoUiISxzT8SKNCckfA==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/core': 0.58.4
|
||||
dev: true
|
||||
|
||||
/@unocss/vite@0.58.3(vite@5.0.10):
|
||||
resolution: {integrity: sha512-gmB2//z7lDEK7Bw5HbHTSQ3abOM0iveAY/W3L3FFXpvduoxMQyuI5dDk0hOCtzhAWeJoynnVN4MBGVmXM4Y/Mg==}
|
||||
/@unocss/vite@0.58.4(vite@5.0.10):
|
||||
resolution: {integrity: sha512-TqD5fIXv6NF3v10FFrCII//GRbkou6Dn/OzW+d4T5f0KM5+T6DutljpYUdGo0+2QXKDroUWLAspFUaZUx8iwVw==}
|
||||
peerDependencies:
|
||||
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.2.1
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
'@unocss/config': 0.58.3
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/inspector': 0.58.3
|
||||
'@unocss/scope': 0.58.3
|
||||
'@unocss/transformer-directives': 0.58.3
|
||||
'@unocss/config': 0.58.4
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/inspector': 0.58.4
|
||||
'@unocss/scope': 0.58.4
|
||||
'@unocss/transformer-directives': 0.58.4
|
||||
chokidar: 3.5.3
|
||||
fast-glob: 3.3.2
|
||||
magic-string: 0.30.5
|
||||
@@ -1333,9 +1333,12 @@ packages:
|
||||
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||
dev: true
|
||||
|
||||
/local-pkg@0.4.3:
|
||||
resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
|
||||
/local-pkg@0.5.0:
|
||||
resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
mlly: 1.4.2
|
||||
pkg-types: 1.0.3
|
||||
dev: true
|
||||
|
||||
/locate-path@6.0.0:
|
||||
@@ -1471,6 +1474,10 @@ packages:
|
||||
resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==}
|
||||
dev: true
|
||||
|
||||
/pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
dev: true
|
||||
|
||||
/perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
dev: true
|
||||
@@ -1636,11 +1643,11 @@ packages:
|
||||
mlly: 1.4.2
|
||||
dev: true
|
||||
|
||||
/unocss@0.58.3(postcss@8.4.32)(vite@5.0.10):
|
||||
resolution: {integrity: sha512-2rnvghfiIDRQ2cOrmN4P7J7xV2p3yBK+bPAt1aoUxCXcszkLczAnQzh9c7IZ+p70kSVstK45cJTYV6TMzOLF7Q==}
|
||||
/unocss@0.58.4(postcss@8.4.32)(vite@5.0.10):
|
||||
resolution: {integrity: sha512-JYeQddAIObJPr6nuxahOgku0MIzjIaQ2P73KtJr0zSuzx6kiq20jf67FgDIOP1Ks6s7iJd7Ga3yuY2h49XjDjg==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@unocss/webpack': 0.58.3
|
||||
'@unocss/webpack': 0.58.4
|
||||
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
|
||||
peerDependenciesMeta:
|
||||
'@unocss/webpack':
|
||||
@@ -1648,26 +1655,26 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@unocss/astro': 0.58.3(vite@5.0.10)
|
||||
'@unocss/cli': 0.58.3
|
||||
'@unocss/core': 0.58.3
|
||||
'@unocss/extractor-arbitrary-variants': 0.58.3
|
||||
'@unocss/postcss': 0.58.3(postcss@8.4.32)
|
||||
'@unocss/preset-attributify': 0.58.3
|
||||
'@unocss/preset-icons': 0.58.3
|
||||
'@unocss/preset-mini': 0.58.3
|
||||
'@unocss/preset-tagify': 0.58.3
|
||||
'@unocss/preset-typography': 0.58.3
|
||||
'@unocss/preset-uno': 0.58.3
|
||||
'@unocss/preset-web-fonts': 0.58.3
|
||||
'@unocss/preset-wind': 0.58.3
|
||||
'@unocss/reset': 0.58.3
|
||||
'@unocss/transformer-attributify-jsx': 0.58.3
|
||||
'@unocss/transformer-attributify-jsx-babel': 0.58.3
|
||||
'@unocss/transformer-compile-class': 0.58.3
|
||||
'@unocss/transformer-directives': 0.58.3
|
||||
'@unocss/transformer-variant-group': 0.58.3
|
||||
'@unocss/vite': 0.58.3(vite@5.0.10)
|
||||
'@unocss/astro': 0.58.4(vite@5.0.10)
|
||||
'@unocss/cli': 0.58.4
|
||||
'@unocss/core': 0.58.4
|
||||
'@unocss/extractor-arbitrary-variants': 0.58.4
|
||||
'@unocss/postcss': 0.58.4(postcss@8.4.32)
|
||||
'@unocss/preset-attributify': 0.58.4
|
||||
'@unocss/preset-icons': 0.58.4
|
||||
'@unocss/preset-mini': 0.58.4
|
||||
'@unocss/preset-tagify': 0.58.4
|
||||
'@unocss/preset-typography': 0.58.4
|
||||
'@unocss/preset-uno': 0.58.4
|
||||
'@unocss/preset-web-fonts': 0.58.4
|
||||
'@unocss/preset-wind': 0.58.4
|
||||
'@unocss/reset': 0.58.4
|
||||
'@unocss/transformer-attributify-jsx': 0.58.4
|
||||
'@unocss/transformer-attributify-jsx-babel': 0.58.4
|
||||
'@unocss/transformer-compile-class': 0.58.4
|
||||
'@unocss/transformer-directives': 0.58.4
|
||||
'@unocss/transformer-variant-group': 0.58.4
|
||||
'@unocss/vite': 0.58.4(vite@5.0.10)
|
||||
vite: 5.0.10
|
||||
transitivePeerDependencies:
|
||||
- postcss
|
||||
|
||||
@@ -34,6 +34,6 @@ test.describe("es locale", () => {
|
||||
test.use({ locale: "es" });
|
||||
|
||||
test("translated text", async ({ page }) => {
|
||||
await expect(page.getByTestId("containers")).toHaveText("Contenedores");
|
||||
await expect(page.getByTestId("label.running-containers")).toHaveText("Contenedores en ejecución");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,5 +5,5 @@ test("simple authentication", async ({ page }) => {
|
||||
await page.locator('input[name="username"]').fill("admin");
|
||||
await page.locator('input[name="password"]').fill("password");
|
||||
await page.locator('button[type="submit"]').click();
|
||||
await expect(page.getByTestId("containers")).toHaveText("Containers");
|
||||
await expect(page.getByTestId("label.running-containers")).toHaveText("Running Containers");
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -4,9 +4,8 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/alexflint/go-arg v1.4.3
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/docker v25.0.2+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
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
@@ -20,8 +19,8 @@ require (
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -29,33 +28,41 @@ require (
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/go-chi/chi/v5 v5.0.11
|
||||
github.com/go-chi/jwtauth/v5 v5.3.0
|
||||
github.com/go-logfmt/logfmt v0.6.0
|
||||
github.com/yuin/goldmark v1.6.0
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // 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.2.0 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/kr/pretty v0.2.1 // 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.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.18 // indirect
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.19 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -14,45 +14,62 @@ github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsVi
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760 h1:FvTM5NSN5HYvfKpgL+8x73U5v063vHsd7AX05eV1DnM=
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
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/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/docker v25.0.1+incompatible h1:k5TYd5rIVQRSqcTwCID+cyVA0yRg86+Pcrz1ls0/frA=
|
||||
github.com/docker/docker v25.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v25.0.2+incompatible h1:/OaKeauroa10K4Nqavw4zlhcDq/WBcPMc5DbjOGgozY=
|
||||
github.com/docker/docker v25.0.2+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/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.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
|
||||
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.0 h1:X7RKGks1lrVeIe2omGyz47pNaNjG2YmwlRN5UKhN8qg=
|
||||
github.com/go-chi/jwtauth/v5 v5.3.0/go.mod h1:2PoGm/KbnzRN9ILY6HFZAI6fTnb1gEZAKogAyqkd6fY=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/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.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
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.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/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/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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
@@ -61,9 +78,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG
|
||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.18 h1:HHZkYS5wWDDyAiNBwztEtDoX07WDhGEdixm8G06R50o=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.18/go.mod h1:fAJ+k5eTgKdDqanzCuK6DAt3W7n3cs2/FX7JhQdk83U=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU=
|
||||
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=
|
||||
@@ -107,15 +123,28 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
|
||||
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
|
||||
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
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.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
||||
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/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
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=
|
||||
@@ -133,16 +162,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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=
|
||||
@@ -150,21 +178,17 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -182,19 +206,26 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
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-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
@@ -44,29 +45,41 @@ func (s StdType) String() string {
|
||||
}
|
||||
|
||||
type DockerCLI interface {
|
||||
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
|
||||
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
|
||||
ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
|
||||
ContainerLogs(context.Context, string, container.LogsOptions) (io.ReadCloser, error)
|
||||
Events(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
|
||||
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
|
||||
ContainerStats(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error)
|
||||
Ping(ctx context.Context) (types.Ping, error)
|
||||
ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) 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
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
type Client interface {
|
||||
ListContainers() ([]Container, error)
|
||||
FindContainer(string) (Container, error)
|
||||
ContainerLogs(context.Context, string, string, StdType) (io.ReadCloser, error)
|
||||
Events(context.Context, chan<- ContainerEvent) <-chan 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(action string, containerID string) error
|
||||
}
|
||||
|
||||
type _client struct {
|
||||
cli DockerCLI
|
||||
filters filters.Args
|
||||
host *Host
|
||||
}
|
||||
|
||||
func NewClient(cli DockerCLI, filters filters.Args, host *Host) *Client {
|
||||
return &Client{cli, filters, host}
|
||||
func NewClient(cli DockerCLI, filters filters.Args, host *Host) Client {
|
||||
return &_client{cli, filters, host}
|
||||
}
|
||||
|
||||
// NewClientWithFilters creates a new instance of Client with docker filters
|
||||
func NewClientWithFilters(f map[string][]string) (*Client, error) {
|
||||
func NewClientWithFilters(f map[string][]string) (Client, error) {
|
||||
filterArgs := filters.NewArgs()
|
||||
for key, values := range f {
|
||||
for _, value := range values {
|
||||
@@ -85,7 +98,7 @@ func NewClientWithFilters(f map[string][]string) (*Client, error) {
|
||||
return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}), nil
|
||||
}
|
||||
|
||||
func NewClientWithTlsAndFilter(f map[string][]string, host Host) (*Client, error) {
|
||||
func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error) {
|
||||
filterArgs := filters.NewArgs()
|
||||
for key, values := range f {
|
||||
for _, value := range values {
|
||||
@@ -121,7 +134,7 @@ func NewClientWithTlsAndFilter(f map[string][]string, host Host) (*Client, error
|
||||
return NewClient(cli, filterArgs, &host), nil
|
||||
}
|
||||
|
||||
func (d *Client) FindContainer(id string) (Container, error) {
|
||||
func (d *_client) FindContainer(id string) (Container, error) {
|
||||
var container Container
|
||||
containers, err := d.ListContainers()
|
||||
if err != nil {
|
||||
@@ -149,10 +162,10 @@ func (d *Client) FindContainer(id string) (Container, error) {
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func (d *Client) ContainerActions(action string, containerID string) error {
|
||||
func (d *_client) ContainerActions(action string, containerID string) error {
|
||||
switch action {
|
||||
case "start":
|
||||
return d.cli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{})
|
||||
return d.cli.ContainerStart(context.Background(), containerID, container.StartOptions{})
|
||||
case "stop":
|
||||
return d.cli.ContainerStop(context.Background(), containerID, container.StopOptions{})
|
||||
case "restart":
|
||||
@@ -162,8 +175,8 @@ func (d *Client) ContainerActions(action string, containerID string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Client) ListContainers() ([]Container, error) {
|
||||
containerListOptions := types.ContainerListOptions{
|
||||
func (d *_client) ListContainers() ([]Container, error) {
|
||||
containerListOptions := container.ListOptions{
|
||||
Filters: d.filters,
|
||||
All: true,
|
||||
}
|
||||
@@ -191,6 +204,7 @@ func (d *Client) ListContainers() ([]Container, error) {
|
||||
Host: d.host.ID,
|
||||
Health: findBetweenParentheses(c.Status),
|
||||
Labels: c.Labels,
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
|
||||
}
|
||||
containers = append(containers, container)
|
||||
}
|
||||
@@ -202,68 +216,60 @@ func (d *Client) ListContainers() ([]Container, error) {
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (d *Client) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
|
||||
func (d *_client) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
|
||||
response, err := d.cli.ContainerStats(ctx, id, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Debugf("starting to stream stats for: %s", id)
|
||||
defer response.Body.Close()
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var v *types.StatsJSON
|
||||
for {
|
||||
if err := decoder.Decode(&v); err != nil {
|
||||
if err == context.Canceled || err == io.EOF {
|
||||
log.Debugf("stopping stats streaming for container %s", id)
|
||||
return
|
||||
}
|
||||
log.Errorf("decoder for stats api returned an unknown error %v", err)
|
||||
}
|
||||
log.Debugf("starting to stream stats for: %s", id)
|
||||
defer response.Body.Close()
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var v *types.StatsJSON
|
||||
for {
|
||||
if err := decoder.Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
memPercent, cpuPercent float64
|
||||
mem, memLimit float64
|
||||
previousCPU uint64
|
||||
previousSystem uint64
|
||||
)
|
||||
daemonOSType := response.OSType
|
||||
var (
|
||||
memPercent, cpuPercent float64
|
||||
mem, memLimit float64
|
||||
previousCPU uint64
|
||||
previousSystem uint64
|
||||
)
|
||||
daemonOSType := response.OSType
|
||||
|
||||
if daemonOSType != "windows" {
|
||||
previousCPU = v.PreCPUStats.CPUUsage.TotalUsage
|
||||
previousSystem = v.PreCPUStats.SystemUsage
|
||||
cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v)
|
||||
mem = calculateMemUsageUnixNoCache(v.MemoryStats)
|
||||
memLimit = float64(v.MemoryStats.Limit)
|
||||
memPercent = calculateMemPercentUnixNoCache(memLimit, mem)
|
||||
} else {
|
||||
cpuPercent = calculateCPUPercentWindows(v)
|
||||
mem = float64(v.MemoryStats.PrivateWorkingSet)
|
||||
}
|
||||
if daemonOSType != "windows" {
|
||||
previousCPU = v.PreCPUStats.CPUUsage.TotalUsage
|
||||
previousSystem = v.PreCPUStats.SystemUsage
|
||||
cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v)
|
||||
mem = calculateMemUsageUnixNoCache(v.MemoryStats)
|
||||
memLimit = float64(v.MemoryStats.Limit)
|
||||
memPercent = calculateMemPercentUnixNoCache(memLimit, mem)
|
||||
} else {
|
||||
cpuPercent = calculateCPUPercentWindows(v)
|
||||
mem = float64(v.MemoryStats.PrivateWorkingSet)
|
||||
}
|
||||
|
||||
log.Tracef("containerId = %s, cpuPercent = %f, memPercent = %f, memUsage = %f, daemonOSType = %s", id, cpuPercent, memPercent, mem, daemonOSType)
|
||||
log.Tracef("containerId = %s, cpuPercent = %f, memPercent = %f, memUsage = %f, daemonOSType = %s", id, cpuPercent, memPercent, mem, daemonOSType)
|
||||
|
||||
if cpuPercent > 0 || mem > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case stats <- ContainerStat{
|
||||
ID: id,
|
||||
CPUPercent: cpuPercent,
|
||||
MemoryPercent: memPercent,
|
||||
MemoryUsage: mem,
|
||||
}:
|
||||
}
|
||||
if cpuPercent > 0 || mem > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case stats <- ContainerStat{
|
||||
ID: id,
|
||||
CPUPercent: cpuPercent,
|
||||
MemoryPercent: memPercent,
|
||||
MemoryUsage: mem,
|
||||
}:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Client) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
|
||||
func (d *_client) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
|
||||
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
|
||||
|
||||
if since != "" {
|
||||
@@ -274,7 +280,7 @@ func (d *Client) ContainerLogs(ctx context.Context, id string, since string, std
|
||||
}
|
||||
}
|
||||
|
||||
options := types.ContainerLogsOptions{
|
||||
options := container.LogsOptions{
|
||||
ShowStdout: stdType&STDOUT != 0,
|
||||
ShowStderr: stdType&STDERR != 0,
|
||||
Follow: true,
|
||||
@@ -291,7 +297,7 @@ func (d *Client) ContainerLogs(ctx context.Context, id string, since string, std
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-chan error {
|
||||
func (d *_client) Events(ctx context.Context, messages chan<- ContainerEvent) <-chan error {
|
||||
dockerMessages, errors := d.cli.Events(ctx, types.EventsOptions{})
|
||||
|
||||
go func() {
|
||||
@@ -300,15 +306,18 @@ func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-c
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-errors:
|
||||
log.Fatalf("error while listening to docker events: %v. Exiting...", err)
|
||||
case message, ok := <-dockerMessages:
|
||||
if !ok {
|
||||
log.Errorf("docker events channel closed")
|
||||
return
|
||||
}
|
||||
|
||||
if message.Type == "container" && len(message.Actor.ID) > 0 {
|
||||
messages <- ContainerEvent{
|
||||
ActorID: message.Actor.ID[:12],
|
||||
Name: message.Action,
|
||||
Name: string(message.Action),
|
||||
Host: d.host.ID,
|
||||
}
|
||||
}
|
||||
@@ -319,8 +328,8 @@ func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-c
|
||||
return errors
|
||||
}
|
||||
|
||||
func (d *Client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
|
||||
options := types.ContainerLogsOptions{
|
||||
func (d *_client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
|
||||
options := container.LogsOptions{
|
||||
ShowStdout: stdType&STDOUT != 0,
|
||||
ShowStderr: stdType&STDERR != 0,
|
||||
Timestamps: true,
|
||||
@@ -338,11 +347,11 @@ func (d *Client) ContainerLogsBetweenDates(ctx context.Context, id string, from
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (d *Client) Ping(ctx context.Context) (types.Ping, error) {
|
||||
func (d *_client) Ping(ctx context.Context) (types.Ping, error) {
|
||||
return d.cli.Ping(ctx)
|
||||
}
|
||||
|
||||
func (d *Client) Host() *Host {
|
||||
func (d *_client) Host() *Host {
|
||||
return d.host
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ type mockedProxy struct {
|
||||
DockerCLI
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) {
|
||||
func (m *mockedProxy) ContainerList(context.Context, container.ListOptions) ([]types.Container, error) {
|
||||
args := m.Called()
|
||||
containers, ok := args.Get(0).([]types.Container)
|
||||
if !ok && args.Get(0) != nil {
|
||||
@@ -32,7 +32,7 @@ func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions)
|
||||
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
reader, ok := args.Get(0).(io.ReadCloser)
|
||||
if !ok && args.Get(0) != nil {
|
||||
@@ -50,7 +50,7 @@ func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, st
|
||||
return types.ContainerStats{}, nil
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error {
|
||||
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error {
|
||||
|
||||
args := m.Called(ctx, containerID, options)
|
||||
err := args.Get(0)
|
||||
@@ -89,7 +89,7 @@ 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 := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
assert.Empty(t, list, "list should be empty")
|
||||
@@ -101,7 +101,7 @@ 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 := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
assert.Nil(t, list, "list should be nil")
|
||||
@@ -124,25 +124,15 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
|
||||
assert.Equal(t, list, []Container{
|
||||
{
|
||||
ID: "1234567890_a",
|
||||
Name: "a_test_container",
|
||||
Names: []string{"/a_test_container"},
|
||||
Host: "localhost",
|
||||
},
|
||||
{
|
||||
ID: "abcdefghijkl",
|
||||
Name: "z_test_container",
|
||||
Names: []string{"/z_test_container"},
|
||||
Host: "localhost",
|
||||
},
|
||||
})
|
||||
Ids := []string{"1234567890_a", "abcdefghijkl"}
|
||||
for i, container := range list {
|
||||
assert.Equal(t, container.ID, Ids[i])
|
||||
}
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
@@ -158,10 +148,10 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
||||
b = append(b, []byte(expected)...)
|
||||
|
||||
reader := io.NopCloser(bytes.NewReader(b))
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
|
||||
options := container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
|
||||
|
||||
actual, _ := io.ReadAll(logReader)
|
||||
@@ -175,7 +165,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
|
||||
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
|
||||
|
||||
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
|
||||
reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
|
||||
|
||||
@@ -202,18 +192,12 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
|
||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||
|
||||
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
|
||||
container, err := client.FindContainer("abcdefghijkl")
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
|
||||
assert.Equal(t, container, Container{
|
||||
ID: "abcdefghijkl",
|
||||
Name: "z_test_container",
|
||||
Names: []string{"/z_test_container"},
|
||||
Host: "localhost",
|
||||
Tty: false,
|
||||
})
|
||||
assert.Equal(t, container.ID, "abcdefghijkl")
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
@@ -231,7 +215,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
|
||||
_, err := client.FindContainer("not_valid")
|
||||
require.Error(t, err, "error should be thrown")
|
||||
@@ -252,7 +236,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||
@@ -263,13 +247,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
container, err := client.FindContainer("abcdefghijkl")
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
|
||||
assert.Equal(t, container, Container{
|
||||
ID: "abcdefghijkl",
|
||||
Name: "z_test_container",
|
||||
Names: []string{"/z_test_container"},
|
||||
Host: "localhost",
|
||||
Tty: false,
|
||||
})
|
||||
assert.Equal(t, container.ID, "abcdefghijkl")
|
||||
|
||||
actions := []string{"start", "stop", "restart"}
|
||||
for _, action := range actions {
|
||||
@@ -294,7 +272,7 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) {
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
|
||||
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ContainerStore struct {
|
||||
containers map[string]*Container
|
||||
client Client
|
||||
statsCollector *StatsCollector
|
||||
subscribers sync.Map
|
||||
}
|
||||
|
||||
func NewContainerStore(ctx context.Context, client Client) *ContainerStore {
|
||||
s := &ContainerStore{
|
||||
containers: make(map[string]*Container),
|
||||
client: client,
|
||||
subscribers: sync.Map{},
|
||||
statsCollector: NewStatsCollector(client),
|
||||
}
|
||||
|
||||
containers, err := s.client.ListContainers()
|
||||
if err != nil {
|
||||
log.Fatalf("error while listing containers: %v", err)
|
||||
}
|
||||
|
||||
for _, c := range containers {
|
||||
c := c // create a new variable to avoid capturing the loop variable
|
||||
s.containers[c.ID] = &c
|
||||
}
|
||||
|
||||
go s.init(ctx)
|
||||
go s.statsCollector.StartCollecting(ctx)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ContainerStore) List() []Container {
|
||||
containers := make([]Container, 0, len(s.containers))
|
||||
for _, c := range s.containers {
|
||||
containers = append(containers, *c)
|
||||
}
|
||||
|
||||
return containers
|
||||
}
|
||||
|
||||
func (s *ContainerStore) Client() Client {
|
||||
return s.client
|
||||
}
|
||||
|
||||
func (s *ContainerStore) Subscribe(ctx context.Context, events chan ContainerEvent) {
|
||||
s.subscribers.Store(ctx, events)
|
||||
}
|
||||
|
||||
func (s *ContainerStore) SubscribeStats(ctx context.Context, stats chan ContainerStat) {
|
||||
s.statsCollector.Subscribe(ctx, stats)
|
||||
}
|
||||
|
||||
func (s *ContainerStore) init(ctx context.Context) {
|
||||
events := make(chan ContainerEvent)
|
||||
s.client.Events(ctx, events)
|
||||
|
||||
stats := make(chan ContainerStat)
|
||||
s.statsCollector.Subscribe(ctx, stats)
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-events:
|
||||
log.Debugf("received event: %+v", event)
|
||||
switch event.Name {
|
||||
case "start":
|
||||
if container, err := s.client.FindContainer(event.ActorID); err == nil {
|
||||
log.Debugf("container %s started", container.ID)
|
||||
s.containers[container.ID] = &container
|
||||
}
|
||||
case "destroy":
|
||||
log.Debugf("container %s destroyed", event.ActorID)
|
||||
delete(s.containers, event.ActorID)
|
||||
|
||||
case "die":
|
||||
if container, ok := s.containers[event.ActorID]; ok {
|
||||
log.Debugf("container %s died", container.ID)
|
||||
container.State = "exited"
|
||||
}
|
||||
case "health_status: healthy", "health_status: unhealthy":
|
||||
healthy := "unhealthy"
|
||||
if event.Name == "health_status: healthy" {
|
||||
healthy = "healthy"
|
||||
}
|
||||
if container, ok := s.containers[event.ActorID]; ok {
|
||||
log.Debugf("container %s is %s", container.ID, healthy)
|
||||
container.Health = healthy
|
||||
}
|
||||
}
|
||||
s.subscribers.Range(func(key, value any) bool {
|
||||
select {
|
||||
case value.(chan ContainerEvent) <- event:
|
||||
case <-key.(context.Context).Done():
|
||||
s.subscribers.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
case stat := <-stats:
|
||||
if container, ok := s.containers[stat.ID]; ok {
|
||||
stat.ID = ""
|
||||
container.Stats.Push(stat)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type mockedClient struct {
|
||||
mock.Mock
|
||||
Client
|
||||
}
|
||||
|
||||
func (m *mockedClient) ListContainers() ([]Container, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedClient) FindContainer(id string) (Container, error) {
|
||||
args := m.Called(id)
|
||||
return args.Get(0).(Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedClient) Events(ctx context.Context, events chan<- ContainerEvent) <-chan error {
|
||||
args := m.Called(ctx, events)
|
||||
return args.Get(0).(chan error)
|
||||
}
|
||||
|
||||
func (m *mockedClient) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
|
||||
args := m.Called(ctx, id, stats)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestContainerStore_List(t *testing.T) {
|
||||
|
||||
client := new(mockedClient)
|
||||
client.On("ListContainers").Return([]Container{
|
||||
{
|
||||
ID: "1234",
|
||||
Name: "test",
|
||||
},
|
||||
}, nil)
|
||||
client.On("Events", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(make(chan error))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
store := NewContainerStore(ctx, client)
|
||||
containers := store.List()
|
||||
|
||||
assert.Equal(t, containers[0].ID, "1234")
|
||||
}
|
||||
|
||||
func TestContainerStore_die(t *testing.T) {
|
||||
client := new(mockedClient)
|
||||
client.On("ListContainers").Return([]Container{
|
||||
{
|
||||
ID: "1234",
|
||||
Name: "test",
|
||||
State: "running",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
client.On("Events", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(make(chan error)).
|
||||
Run(func(args mock.Arguments) {
|
||||
ctx := args.Get(0).(context.Context)
|
||||
events := args.Get(1).(chan<- ContainerEvent)
|
||||
go func() {
|
||||
events <- ContainerEvent{
|
||||
Name: "die",
|
||||
ActorID: "1234",
|
||||
Host: "localhost",
|
||||
}
|
||||
<-ctx.Done()
|
||||
}()
|
||||
})
|
||||
|
||||
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- docker.ContainerStat")).Return(nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
store := NewContainerStore(ctx, client)
|
||||
|
||||
// Wait until we get the event
|
||||
events := make(chan ContainerEvent)
|
||||
store.Subscribe(ctx, events)
|
||||
<-events
|
||||
|
||||
containers := store.List()
|
||||
assert.Equal(t, containers[0].State, "exited")
|
||||
}
|
||||
@@ -9,10 +9,12 @@ import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-logfmt/logfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -88,7 +90,7 @@ func (g *EventGenerator) consumeReader() {
|
||||
|
||||
if readerError != nil {
|
||||
if readerError != ErrBadHeader {
|
||||
log.Debugf("reader error: %v", readerError)
|
||||
log.Tracef("reader error: %v", readerError)
|
||||
g.Errors <- readerError
|
||||
close(g.buffer)
|
||||
break
|
||||
@@ -155,6 +157,8 @@ func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var validLogFmtKey = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
|
||||
|
||||
func createEvent(message string, streamType StdType) *LogEvent {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(message))
|
||||
@@ -177,6 +181,27 @@ func createEvent(message string, streamType StdType) *LogEvent {
|
||||
} else {
|
||||
logEvent.Message = data
|
||||
}
|
||||
} else if strings.Contains(message, "=") {
|
||||
buffer := bufPool.Get().(*bytes.Buffer)
|
||||
buffer.Reset()
|
||||
defer bufPool.Put(buffer)
|
||||
buffer.WriteString(message)
|
||||
decoder := logfmt.NewDecoder(buffer)
|
||||
data := make(map[string]string)
|
||||
decoder.ScanRecord()
|
||||
allValid := true
|
||||
for decoder.ScanKeyval() {
|
||||
key := decoder.Key()
|
||||
value := decoder.Value()
|
||||
if !validLogFmtKey.Match(key) {
|
||||
allValid = false
|
||||
break
|
||||
}
|
||||
data[string(key)] = string(value)
|
||||
}
|
||||
if allValid && len(data) > 1 {
|
||||
logEvent.Message = data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
|
||||
|
||||
func Test_createEvent(t *testing.T) {
|
||||
type args struct {
|
||||
message string
|
||||
streamType StdType
|
||||
message string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -45,6 +45,11 @@ func guessLogLevel(logEvent *LogEvent) string {
|
||||
if level, ok := value["level"].(string); ok {
|
||||
return level
|
||||
}
|
||||
|
||||
case map[string]string:
|
||||
if level, ok := value["level"]; ok {
|
||||
return level
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type StatsCollector struct {
|
||||
stream chan ContainerStat
|
||||
subscribers sync.Map
|
||||
client Client
|
||||
cancelers sync.Map
|
||||
}
|
||||
|
||||
func NewStatsCollector(client Client) *StatsCollector {
|
||||
return &StatsCollector{
|
||||
stream: make(chan ContainerStat),
|
||||
subscribers: sync.Map{},
|
||||
client: client,
|
||||
cancelers: sync.Map{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StatsCollector) Subscribe(ctx context.Context, stats chan ContainerStat) {
|
||||
c.subscribers.Store(ctx, stats)
|
||||
}
|
||||
|
||||
func (sc *StatsCollector) StartCollecting(ctx context.Context) {
|
||||
if containers, err := sc.client.ListContainers(); err == nil {
|
||||
for _, c := range containers {
|
||||
if c.State == "running" {
|
||||
go func(client Client, id string) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
sc.cancelers.Store(id, cancel)
|
||||
if err := client.ContainerStats(ctx, id, sc.stream); err != nil {
|
||||
if !errors.Is(err, context.Canceled) && !errors.Is(err, io.EOF) {
|
||||
log.Errorf("unexpected error when streaming container stats: %v", err)
|
||||
}
|
||||
}
|
||||
}(sc.client, c.ID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("error while listing containers: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
events := make(chan ContainerEvent)
|
||||
sc.client.Events(ctx, events)
|
||||
for event := range events {
|
||||
switch event.Name {
|
||||
case "start":
|
||||
go func(client Client, id string) {
|
||||
if err := client.ContainerStats(ctx, id, sc.stream); err != nil {
|
||||
if !errors.Is(err, context.Canceled) && !errors.Is(err, io.EOF) {
|
||||
log.Errorf("unexpected error when streaming container stats: %v", err)
|
||||
}
|
||||
}
|
||||
}(sc.client, event.ActorID)
|
||||
|
||||
case "die":
|
||||
if cancel, ok := sc.cancelers.LoadAndDelete(event.ActorID); ok {
|
||||
cancel.(context.CancelFunc)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case stat := <-sc.stream:
|
||||
sc.subscribers.Range(func(key, value interface{}) bool {
|
||||
select {
|
||||
case value.(chan ContainerStat) <- stat:
|
||||
case <-key.(context.Context).Done():
|
||||
sc.subscribers.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,26 @@ package docker
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
)
|
||||
|
||||
// Container represents an internal representation of docker containers
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Names []string `json:"names"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
ImageID string `json:"imageId"`
|
||||
Command string `json:"command"`
|
||||
Created int64 `json:"created"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Health string `json:"health,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Tty bool `json:"-"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Names []string `json:"names"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
ImageID string `json:"imageId"`
|
||||
Command string `json:"command"`
|
||||
Created int64 `json:"created"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// ContainerStat represent stats instant for a container
|
||||
|
||||
@@ -35,5 +35,5 @@ func HttpRequest(addr string, base string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Healthcheck failed with status code %d", resp.StatusCode)
|
||||
return fmt.Errorf("healthcheck failed with status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
profileFilename = "profile.json"
|
||||
)
|
||||
|
||||
var missingProfileErr = errors.New("Profile file does not exist")
|
||||
var errMissingProfileErr = errors.New("Profile file does not exist")
|
||||
|
||||
type Settings struct {
|
||||
Search bool `json:"search"`
|
||||
@@ -30,9 +30,11 @@ type Settings struct {
|
||||
CollapseNav bool `json:"collapseNav"`
|
||||
AutomaticRedirect bool `json:"automaticRedirect"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Compact bool `json:"compact"`
|
||||
LightTheme string `json:"lightTheme,omitempty"`
|
||||
HourStyle string `json:"hourStyle,omitempty"`
|
||||
DateLocale string `json:"dateLocale,omitempty"`
|
||||
Locale string `json:"locale"`
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
@@ -64,7 +66,7 @@ func UpdateFromReader(user auth.User, reader io.Reader) error {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
existingProfile, err := Load(user)
|
||||
if err != nil && err != missingProfileErr {
|
||||
if err != nil && err != errMissingProfileErr {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -110,7 +112,7 @@ func Load(user auth.User) (Profile, error) {
|
||||
profilePath := filepath.Join(path, profileFilename)
|
||||
|
||||
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
|
||||
return Profile{}, missingProfileErr
|
||||
return Profile{}, errMissingProfileErr
|
||||
}
|
||||
|
||||
f, err := os.Open(profilePath)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type RingBuffer[T any] struct {
|
||||
Size int
|
||||
data []T
|
||||
start int
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRingBuffer[T any](size int) *RingBuffer[T] {
|
||||
return &RingBuffer[T]{
|
||||
Size: size,
|
||||
data: make([]T, 0, size),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RingBuffer[T]) Push(data T) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
if len(r.data) == r.Size {
|
||||
r.data[r.start] = data
|
||||
r.start = (r.start + 1) % r.Size
|
||||
} else {
|
||||
r.data = append(r.data, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RingBuffer[T]) Data() []T {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
if len(r.data) == r.Size {
|
||||
return append(r.data[r.start:], r.data[:r.start]...)
|
||||
} else {
|
||||
return r.data
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RingBuffer[T]) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(r.Data())
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRingBuffer(t *testing.T) {
|
||||
rb := NewRingBuffer[int](3)
|
||||
|
||||
rb.Push(1)
|
||||
rb.Push(2)
|
||||
rb.Push(3)
|
||||
|
||||
data := rb.Data()
|
||||
expectedData := []int{1, 2, 3}
|
||||
if !reflect.DeepEqual(data, expectedData) {
|
||||
t.Errorf("Expected data to be %v, got %v", expectedData, data)
|
||||
}
|
||||
|
||||
rb.Push(4)
|
||||
data = rb.Data()
|
||||
expectedData = []int{2, 3, 4}
|
||||
if !reflect.DeepEqual(data, expectedData) {
|
||||
t.Errorf("Expected data to be %v, got %v", expectedData, data)
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ data: []
|
||||
|
||||
|
||||
event: containers-changed
|
||||
data: []
|
||||
data: [{"id":"1234","names":null,"name":"test","image":"test","imageId":"","command":"","created":0,"state":"","status":"","stats":[]}]
|
||||
|
||||
|
||||
event: container-start
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/amir20/dozzle/internal/analytics"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
@@ -29,9 +26,6 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
events := make(chan docker.ContainerEvent)
|
||||
stats := make(chan docker.ContainerStat)
|
||||
|
||||
b := analytics.BeaconEvent{
|
||||
Name: "events",
|
||||
Version: h.config.Version,
|
||||
@@ -44,48 +38,22 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
HasActions: h.config.EnableActions,
|
||||
}
|
||||
|
||||
{
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(h.clients))
|
||||
results := make(chan []docker.Container, len(h.clients))
|
||||
allContainers := make([]docker.Container, 0)
|
||||
events := make(chan docker.ContainerEvent)
|
||||
stats := make(chan docker.ContainerStat)
|
||||
|
||||
for _, client := range h.clients {
|
||||
client.Events(ctx, events)
|
||||
|
||||
go func(client DockerClient) {
|
||||
defer wg.Done()
|
||||
if containers, err := client.ListContainers(); err == nil {
|
||||
results <- containers
|
||||
go func(client DockerClient) {
|
||||
for _, c := range containers {
|
||||
if c.State == "running" {
|
||||
if err := client.ContainerStats(ctx, c.ID, stats); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Errorf("error while streaming container stats: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(client)
|
||||
} else {
|
||||
log.Errorf("error while listing containers: %v", err)
|
||||
}
|
||||
}(client)
|
||||
}
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
allContainers := []docker.Container{}
|
||||
for containers := range results {
|
||||
allContainers = append(allContainers, containers...)
|
||||
}
|
||||
|
||||
if err := sendContainersJSON(allContainers, w); err != nil {
|
||||
log.Errorf("error writing containers to event stream: %v", err)
|
||||
}
|
||||
|
||||
b.RunningContainers = len(allContainers)
|
||||
f.Flush()
|
||||
for _, store := range h.stores {
|
||||
allContainers = append(allContainers, store.List()...)
|
||||
store.SubscribeStats(ctx, stats)
|
||||
store.Subscribe(ctx, events)
|
||||
}
|
||||
|
||||
if err := sendContainersJSON(allContainers, w); err != nil {
|
||||
log.Errorf("error writing containers to event stream: %v", err)
|
||||
}
|
||||
b.RunningContainers = len(allContainers)
|
||||
f.Flush()
|
||||
|
||||
if !h.config.NoAnalytics {
|
||||
go func() {
|
||||
if err := analytics.SendBeacon(b); err != nil {
|
||||
@@ -109,17 +77,9 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
switch event.Name {
|
||||
case "start", "die":
|
||||
log.Debugf("triggering docker event: %v", event.Name)
|
||||
if event.Name == "start" {
|
||||
log.Debugf("found new container with id: %v", event.ActorID)
|
||||
|
||||
if err := h.clients[event.Host].ContainerStats(ctx, event.ActorID, stats); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Errorf("error when streaming new container stats: %v", err)
|
||||
}
|
||||
containers, err := h.clients[event.Host].ListContainers()
|
||||
if err != nil {
|
||||
log.Errorf("error when listing containers: %v", err)
|
||||
}
|
||||
containers := h.stores[event.Host].List()
|
||||
if err := sendContainersJSON(containers, w); err != nil {
|
||||
log.Errorf("error encoding containers to stream: %v", err)
|
||||
return
|
||||
@@ -150,11 +110,9 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
f.Flush()
|
||||
default:
|
||||
log.Tracef("ignoring docker event: %v", event.Name)
|
||||
// do nothing
|
||||
}
|
||||
case <-ctx.Done():
|
||||
log.Debugf("context done, closing event stream")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/beme/abide"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -35,36 +37,26 @@ func Test_handler_streamEvents_happy(t *testing.T) {
|
||||
ActorID: "1234",
|
||||
Host: "localhost",
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
})
|
||||
mockedClient.On("FindContainer", "1234").Return(docker.Container{
|
||||
ID: "1234",
|
||||
Name: "test",
|
||||
Image: "test",
|
||||
Stats: utils.NewRingBuffer[docker.ContainerStat](300), // 300 seconds of stats
|
||||
}, nil)
|
||||
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_handler_streamEvents_error_request(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/events/stream", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
|
||||
errChannel := make(chan error)
|
||||
mockedClient.On("Events", mock.Anything, mock.Anything).Return(errChannel)
|
||||
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
go func() {
|
||||
cancel()
|
||||
}()
|
||||
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
|
||||
// This is needed so that the server is initialized for store
|
||||
server := CreateServer(clients, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
|
||||
handler := server.Handler
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
|
||||
log.Trace("Executing healthcheck request")
|
||||
var client DockerClient
|
||||
var client docker.Client
|
||||
for _, v := range h.clients {
|
||||
client = v
|
||||
break
|
||||
|
||||
@@ -85,16 +85,9 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
g := docker.NewEventGenerator(reader, container.Tty)
|
||||
encoder := json.NewEncoder(w)
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-g.Events:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
if err := encoder.Encode(event); err != nil {
|
||||
log.Errorf("json encoding error while streaming %v", err.Error())
|
||||
}
|
||||
for event := range g.Events {
|
||||
if err := encoder.Encode(event); err != nil {
|
||||
log.Errorf("json encoding error while streaming %v", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func (h *handler) releases(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Warnf("error reading releases: %v", err)
|
||||
log.Debugf("error reading releases: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,14 @@ package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -47,30 +45,25 @@ type Authorizer interface {
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
clients map[string]DockerClient
|
||||
clients map[string]docker.Client
|
||||
stores map[string]*docker.ContainerStore
|
||||
content fs.FS
|
||||
config *Config
|
||||
}
|
||||
|
||||
// Client is a proxy around the docker client
|
||||
type DockerClient interface {
|
||||
ListContainers() ([]docker.Container, error)
|
||||
FindContainer(string) (docker.Container, error)
|
||||
ContainerLogs(context.Context, string, string, docker.StdType) (io.ReadCloser, error)
|
||||
Events(context.Context, chan<- docker.ContainerEvent) <-chan error
|
||||
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, docker.StdType) (io.ReadCloser, error)
|
||||
ContainerStats(context.Context, string, chan<- docker.ContainerStat) error
|
||||
Ping(context.Context) (types.Ping, error)
|
||||
Host() *docker.Host
|
||||
ContainerActions(action string, containerID string) error
|
||||
}
|
||||
func CreateServer(clients map[string]docker.Client, content fs.FS, config Config) *http.Server {
|
||||
stores := make(map[string]*docker.ContainerStore)
|
||||
for host, client := range clients {
|
||||
stores[host] = docker.NewContainerStore(context.Background(), client)
|
||||
}
|
||||
|
||||
func CreateServer(clients map[string]DockerClient, content fs.FS, config Config) *http.Server {
|
||||
handler := &handler{
|
||||
clients: clients,
|
||||
content: content,
|
||||
config: &config,
|
||||
stores: stores,
|
||||
}
|
||||
|
||||
return &http.Server{Addr: config.Addr, Handler: createRouter(handler)}
|
||||
}
|
||||
|
||||
@@ -135,7 +128,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *handler) clientFromRequest(r *http.Request) DockerClient {
|
||||
func (h *handler) clientFromRequest(r *http.Request) docker.Client {
|
||||
host := chi.URLParam(r, "host")
|
||||
|
||||
if host == "" {
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
type MockedClient struct {
|
||||
mock.Mock
|
||||
DockerClient
|
||||
docker.Client
|
||||
}
|
||||
|
||||
func (m *MockedClient) FindContainer(id string) (docker.Container, error) {
|
||||
@@ -59,7 +59,7 @@ func (m *MockedClient) Host() *docker.Host {
|
||||
return args.Get(0).(*docker.Host)
|
||||
}
|
||||
|
||||
func createHandler(client DockerClient, content fs.FS, config Config) *chi.Mux {
|
||||
func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux {
|
||||
if client == nil {
|
||||
client = new(MockedClient)
|
||||
client.(*MockedClient).On("ListContainers").Return([]docker.Container{}, nil)
|
||||
@@ -74,7 +74,7 @@ func createHandler(client DockerClient, content fs.FS, config Config) *chi.Mux {
|
||||
content = afero.NewIOFS(fs)
|
||||
}
|
||||
|
||||
clients := map[string]DockerClient{
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": client,
|
||||
}
|
||||
return createRouter(&handler{
|
||||
@@ -84,6 +84,6 @@ func createHandler(client DockerClient, content fs.FS, config Config) *chi.Mux {
|
||||
})
|
||||
}
|
||||
|
||||
func createDefaultHandler(client DockerClient) *chi.Mux {
|
||||
func createDefaultHandler(client docker.Client) *chi.Mux {
|
||||
return createHandler(client, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ toolbar:
|
||||
show-all: Zeige alle Streams
|
||||
label:
|
||||
containers: Container
|
||||
running-containers: Laufende Container
|
||||
all-containers: Alle Container
|
||||
total-containers: Gesamte Container
|
||||
running: Laufende
|
||||
total-cpu-usage: Gesamte CPU Auslastung
|
||||
@@ -62,6 +64,7 @@ placeholder:
|
||||
search-containers: Suche Container (⌘ + k, ⌃k)
|
||||
settings:
|
||||
display: Anzeige
|
||||
locale: Sprache überschreiben
|
||||
small-scrollbars: Verwende kleinere Scrollbars
|
||||
show-timesamps: Zeige Zeitstempel
|
||||
soft-wrap: Zeilenumbruch
|
||||
@@ -79,6 +82,7 @@ settings:
|
||||
Eine neue Version ist verfügbar! Aktualisiere auf <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Zeige stdout und stderr Labels
|
||||
automatic-redirect: Automatische Weiterleitung zu neuen Containern mit demselben Namen
|
||||
compact: Aktiviere den kompakten Modus für Logs
|
||||
releases:
|
||||
features: Ein neues Feature | {count} Features
|
||||
bugFixes: Ein Bug Fix | {count} Fixes
|
||||
|
||||
@@ -9,6 +9,8 @@ toolbar:
|
||||
restart: Restart
|
||||
label:
|
||||
containers: Containers
|
||||
running-containers: Running Containers
|
||||
all-containers: All Containers
|
||||
total-containers: Total Containers
|
||||
running: Running
|
||||
total-cpu-usage: Total CPU Usage
|
||||
@@ -41,7 +43,7 @@ error:
|
||||
events-timeout:
|
||||
title: Something is not right
|
||||
message: >-
|
||||
Dozzle UI timeed out while connecting to API. Please check network
|
||||
Dozzle UI timed out while connecting to API. Please check network
|
||||
connection and try again.
|
||||
alert:
|
||||
redirected:
|
||||
@@ -51,7 +53,7 @@ alert:
|
||||
title: Similar container found
|
||||
message: >-
|
||||
Dozzle found a similar container {containerId} that is running on the same
|
||||
host. Do you want to switch to it?
|
||||
host and will automatically switch to it unless you click 'Cancel'.
|
||||
title:
|
||||
page-not-found: Page not found
|
||||
login: Authentication Required
|
||||
@@ -65,6 +67,7 @@ placeholder:
|
||||
search-containers: Search containers (⌘ + k, ⌃k)
|
||||
settings:
|
||||
display: Display
|
||||
locale: Override language
|
||||
small-scrollbars: Use smaller scrollbars
|
||||
show-timesamps: Show timestamps
|
||||
soft-wrap: Soft wrap lines
|
||||
@@ -81,6 +84,7 @@ settings:
|
||||
rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Show stdout and stderr labels
|
||||
automatic-redirect: Automatically redirect to new containers with the same name
|
||||
compact: Enable compact mode for logs
|
||||
releases:
|
||||
features: one new feature | {count} features
|
||||
bugFixes: one bug fix | {count} fixes
|
||||
|
||||
@@ -4,8 +4,13 @@ toolbar:
|
||||
search: Buscar
|
||||
show: Mostrar sólo {std}
|
||||
show-all: Mostrar todos los flujos
|
||||
stop: Detener
|
||||
start: Iniciar
|
||||
restart: Reiniciar
|
||||
label:
|
||||
containers: Contenedores
|
||||
running-containers: Contenedores en ejecución
|
||||
all-containers: Todos los contenedores
|
||||
total-containers: Contenedores Totales
|
||||
running: En ejecución
|
||||
total-cpu-usage: Uso total del Procesador
|
||||
@@ -20,6 +25,7 @@ label:
|
||||
created: Creado
|
||||
avg-cpu: Promedio de CPU (%)
|
||||
avg-mem: Promedio de MEM (%)
|
||||
pinned: Fijado
|
||||
tooltip:
|
||||
search: Buscar contenedores (⌘ + K, CTRL + K)
|
||||
pin-column: Anclar como columna
|
||||
@@ -58,9 +64,11 @@ placeholder:
|
||||
search-containers: Buscar contenedores (⌘ + K, CTRL + K)
|
||||
settings:
|
||||
display: Vista
|
||||
locale: Sobrescribir idioma
|
||||
small-scrollbars: Utilizar barras de desplazamiento más pequeñas
|
||||
show-timesamps: Mostrar marcas de tiempo
|
||||
soft-wrap: Líneas de texto con ajuste suave
|
||||
datetime-format: Sobrescribir el formato de fecha y hora
|
||||
12-24-format: >-
|
||||
Por defecto, Dozzle utilizará la configuración regional de tu navegador para formatear la hora. Usted puede
|
||||
forzar el estilo de 12 o 24 horas.
|
||||
@@ -76,6 +84,7 @@ settings:
|
||||
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Mostrar etiquetas de salida estándar y salida de error estándar
|
||||
automatic-redirect: Redireccionar automáticamente a nuevos contenedores con el mismo nombre
|
||||
compact: Modo compacto
|
||||
releases:
|
||||
features: Características
|
||||
bugFixes: Correcciones de errores
|
||||
@@ -86,6 +95,7 @@ releases:
|
||||
no_releases: No hay lanzamientos
|
||||
log_actions:
|
||||
copy_log: Copiar registro
|
||||
jump_to_context: Ir al contexto
|
||||
toasts:
|
||||
copied:
|
||||
title: Copiado
|
||||
|
||||
@@ -6,6 +6,8 @@ toolbar:
|
||||
show-all: Mostrar todos os fluxos
|
||||
label:
|
||||
containers: Contentores
|
||||
running-containers: Contentores em execução
|
||||
all-containers: Todos os contentores
|
||||
total-containers: Contentores Totais
|
||||
running: Em execução
|
||||
total-cpu-usage: Utilização total da CPU
|
||||
@@ -58,6 +60,7 @@ placeholder:
|
||||
search-containers: Pesquisar contentores (⌘ + K, CTRL + K)
|
||||
settings:
|
||||
display: Visão
|
||||
locale: Localidade
|
||||
small-scrollbars: Usar barras de rolagem mais pequenas
|
||||
show-timesamps: Mostrar carimbos de tempo
|
||||
soft-wrap: Linhas de texto de embrulho suave
|
||||
@@ -76,3 +79,4 @@ settings:
|
||||
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Mostrar etiquetas de saída padrão e saída de erro padrão
|
||||
automatic-redirect: Redirecionar automaticamente para novos contentores com o mesmo nome
|
||||
compact: Compactar a interface do utilizador
|
||||
|
||||
@@ -6,6 +6,8 @@ toolbar:
|
||||
show-all: Показать все потоки
|
||||
label:
|
||||
containers: Контейнеры
|
||||
running-containers: Запущенные контейнеры
|
||||
all-containers: Все контейнеры
|
||||
total-containers: Всего Контейнеров
|
||||
running: Запущенные
|
||||
total-cpu-usage: Использование процессора
|
||||
@@ -58,6 +60,7 @@ placeholder:
|
||||
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
|
||||
settings:
|
||||
display: Вид
|
||||
locale: Язык
|
||||
small-scrollbars: Уменьшенная полоса прокрутки
|
||||
show-timesamps: Показывать временные метки
|
||||
soft-wrap: Плавный перенос текста
|
||||
@@ -74,3 +77,4 @@ settings:
|
||||
Доступна новая версия! Обновить до <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Показывать метки stdout и stderr
|
||||
automatic-redirect: Автоматическое перенаправление на новые контейнеры с тем же именем.
|
||||
compact: Компактный режим
|
||||
|
||||
@@ -6,6 +6,8 @@ toolbar:
|
||||
show-all: 顯示全部資料流
|
||||
label:
|
||||
containers: 容器
|
||||
running-containers: 運行中的容器
|
||||
all-containers: 所有容器
|
||||
total-containers: 總容器數
|
||||
running: 運行中
|
||||
total-cpu-usage: 總 CPU 使用率
|
||||
@@ -41,6 +43,7 @@ placeholder:
|
||||
search-containers: 查詢容器 (⌘ + k, ⌃k)
|
||||
settings:
|
||||
display: 顯示
|
||||
locale: 覆寫語言
|
||||
small-scrollbars: 使用較小的捲軸
|
||||
show-timesamps: 顯示時間戳記
|
||||
soft-wrap: 自動換行
|
||||
@@ -56,3 +59,4 @@ settings:
|
||||
update-available: >-
|
||||
有新版本可用!請更新至 <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>。
|
||||
show-std: 顯示 stdout 和 stderr 標籤
|
||||
compact: 緊湊模式
|
||||
|
||||
@@ -6,6 +6,8 @@ toolbar:
|
||||
show-all: 显示全部
|
||||
label:
|
||||
containers: 容器
|
||||
running-containers: 运行中的容器
|
||||
all-containers: 所有容器
|
||||
total-containers: 所有容器
|
||||
running: 运行中
|
||||
total-cpu-usage: 所有CPU使用率
|
||||
@@ -58,6 +60,7 @@ placeholder:
|
||||
search-containers: 搜索容器 (⌘ + k, ⌃k)
|
||||
settings:
|
||||
display: 显示
|
||||
locale: 覆盖语言
|
||||
small-scrollbars: 使用较小的滚动条
|
||||
show-timesamps: 显示时间戳
|
||||
soft-wrap: 断行
|
||||
@@ -74,5 +77,6 @@ settings:
|
||||
新版本可用!更新到 <a href="{href}" rel="noreferrer noopener">{nextVersion}</a>。
|
||||
show-std: 显示stdout和stderr标签
|
||||
automatic-redirect: 自动重定向到同名的新容器
|
||||
compact: 紧凑模式
|
||||
log_actions:
|
||||
copy_log: 复制日志
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -120,12 +121,12 @@ func doStartEvent(arg args) {
|
||||
}
|
||||
|
||||
func createClients(args args,
|
||||
localClientFactory func(map[string][]string) (*docker.Client, error),
|
||||
remoteClientFactory func(map[string][]string, docker.Host) (*docker.Client, error),
|
||||
hostname string) map[string]web.DockerClient {
|
||||
clients := make(map[string]web.DockerClient)
|
||||
localClientFactory func(map[string][]string) (docker.Client, error),
|
||||
remoteClientFactory func(map[string][]string, docker.Host) (docker.Client, error),
|
||||
hostname string) map[string]docker.Client {
|
||||
clients := make(map[string]docker.Client)
|
||||
|
||||
if localClient := createLocalClient(args, localClientFactory); localClient != nil {
|
||||
if localClient, err := createLocalClient(args, localClientFactory); err == nil {
|
||||
if hostname != "" {
|
||||
localClient.Host().Name = hostname
|
||||
}
|
||||
@@ -154,7 +155,7 @@ func createClients(args args,
|
||||
return clients
|
||||
}
|
||||
|
||||
func createServer(args args, clients map[string]web.DockerClient) *http.Server {
|
||||
func createServer(args args, clients map[string]docker.Client) *http.Server {
|
||||
_, dev := os.LookupEnv("DEV")
|
||||
|
||||
var provider web.AuthProvider = web.NONE
|
||||
@@ -221,7 +222,7 @@ func createServer(args args, clients map[string]web.DockerClient) *http.Server {
|
||||
return web.CreateServer(clients, assets, config)
|
||||
}
|
||||
|
||||
func createLocalClient(args args, localClientFactory func(map[string][]string) (*docker.Client, error)) *docker.Client {
|
||||
func createLocalClient(args args, localClientFactory func(map[string][]string) (docker.Client, error)) (docker.Client, error) {
|
||||
for i := 1; ; i++ {
|
||||
dockerClient, err := localClientFactory(args.Filter)
|
||||
if err == nil {
|
||||
@@ -230,7 +231,7 @@ func createLocalClient(args args, localClientFactory func(map[string][]string) (
|
||||
log.Debugf("Could not connect to local Docker Engine: %s", err)
|
||||
} else {
|
||||
log.Debugf("Connected to local Docker Engine")
|
||||
return dockerClient
|
||||
return dockerClient, nil
|
||||
}
|
||||
}
|
||||
if args.WaitForDockerSeconds > 0 {
|
||||
@@ -242,7 +243,7 @@ func createLocalClient(args args, localClientFactory func(map[string][]string) (
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return nil, errors.New("could not connect to local Docker Engine")
|
||||
}
|
||||
|
||||
func parseArgs() args {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -17,7 +18,7 @@ type fakeCLI struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (f *fakeCLI) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) {
|
||||
func (f *fakeCLI) ContainerList(context.Context, container.ListOptions) ([]types.Container, error) {
|
||||
args := f.Called()
|
||||
return args.Get(0).([]types.Container), args.Error(1)
|
||||
}
|
||||
@@ -25,7 +26,7 @@ func (f *fakeCLI) ContainerList(context.Context, types.ContainerListOptions) ([]
|
||||
func Test_valid_localhost(t *testing.T) {
|
||||
client := new(fakeCLI)
|
||||
client.On("ContainerList").Return([]types.Container{}, nil)
|
||||
fakeClientFactory := func(filter map[string][]string) (*docker.Client, error) {
|
||||
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
|
||||
ID: "localhost",
|
||||
}), nil
|
||||
@@ -33,7 +34,7 @@ func Test_valid_localhost(t *testing.T) {
|
||||
|
||||
args := args{}
|
||||
|
||||
actualClient := createLocalClient(args, fakeClientFactory)
|
||||
actualClient, _ := createLocalClient(args, fakeClientFactory)
|
||||
|
||||
assert.NotNil(t, actualClient)
|
||||
client.AssertExpectations(t)
|
||||
@@ -42,7 +43,7 @@ func Test_valid_localhost(t *testing.T) {
|
||||
func Test_invalid_localhost(t *testing.T) {
|
||||
client := new(fakeCLI)
|
||||
client.On("ContainerList").Return([]types.Container{}, errors.New("error"))
|
||||
fakeClientFactory := func(filter map[string][]string) (*docker.Client, error) {
|
||||
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
|
||||
ID: "localhost",
|
||||
}), nil
|
||||
@@ -50,7 +51,7 @@ func Test_invalid_localhost(t *testing.T) {
|
||||
|
||||
args := args{}
|
||||
|
||||
actualClient := createLocalClient(args, fakeClientFactory)
|
||||
actualClient, _ := createLocalClient(args, fakeClientFactory)
|
||||
|
||||
assert.Nil(t, actualClient)
|
||||
client.AssertExpectations(t)
|
||||
@@ -59,7 +60,7 @@ func Test_invalid_localhost(t *testing.T) {
|
||||
func Test_valid_remote(t *testing.T) {
|
||||
local := new(fakeCLI)
|
||||
local.On("ContainerList").Return([]types.Container{}, errors.New("error"))
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
|
||||
ID: "localhost",
|
||||
}), nil
|
||||
@@ -67,7 +68,7 @@ func Test_valid_remote(t *testing.T) {
|
||||
|
||||
remote := new(fakeCLI)
|
||||
remote.On("ContainerList").Return([]types.Container{}, nil)
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
|
||||
return docker.NewClient(remote, filters.NewArgs(), &docker.Host{
|
||||
ID: "test",
|
||||
}), nil
|
||||
@@ -89,7 +90,7 @@ func Test_valid_remote(t *testing.T) {
|
||||
func Test_valid_remote_and_local(t *testing.T) {
|
||||
local := new(fakeCLI)
|
||||
local.On("ContainerList").Return([]types.Container{}, nil)
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
|
||||
ID: "localhost",
|
||||
}), nil
|
||||
@@ -97,7 +98,7 @@ func Test_valid_remote_and_local(t *testing.T) {
|
||||
|
||||
remote := new(fakeCLI)
|
||||
remote.On("ContainerList").Return([]types.Container{}, nil)
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
|
||||
return docker.NewClient(remote, filters.NewArgs(), &docker.Host{
|
||||
ID: "test",
|
||||
}), nil
|
||||
@@ -118,13 +119,13 @@ func Test_valid_remote_and_local(t *testing.T) {
|
||||
func Test_no_clients(t *testing.T) {
|
||||
local := new(fakeCLI)
|
||||
local.On("ContainerList").Return([]types.Container{}, errors.New("error"))
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
|
||||
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
|
||||
ID: "localhost",
|
||||
}), nil
|
||||
}
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
|
||||
client := new(fakeCLI)
|
||||
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
|
||||
ID: "test",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "6.0.7",
|
||||
"version": "6.2.0",
|
||||
"description": "Realtime log viewer for docker containers.",
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/amir20/dozzle/issues"
|
||||
},
|
||||
"packageManager": "pnpm@8.14.1",
|
||||
"packageManager": "pnpm@8.15.1",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,7 +28,7 @@
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/carbon": "^1.1.27",
|
||||
"@iconify-json/carbon": "^1.1.28",
|
||||
"@iconify-json/cil": "^1.1.8",
|
||||
"@iconify-json/ic": "^1.1.17",
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
@@ -38,20 +38,20 @@
|
||||
"@intlify/unplugin-vue-i18n": "^2.0.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vueuse/components": "^10.7.1",
|
||||
"@vueuse/core": "^10.7.1",
|
||||
"@vueuse/integrations": "^10.7.1",
|
||||
"@vueuse/math": "^10.7.1",
|
||||
"@vueuse/router": "^10.7.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"@vueuse/components": "^10.7.2",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"@vueuse/integrations": "^10.7.2",
|
||||
"@vueuse/math": "^10.7.2",
|
||||
"@vueuse/router": "^10.7.2",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"daisyui": "^4.5.0",
|
||||
"date-fns": "^3.2.0",
|
||||
"daisyui": "^4.6.1",
|
||||
"date-fns": "^3.3.1",
|
||||
"entities": "^4.5.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
@@ -60,20 +60,20 @@
|
||||
"postcss-mixins": "^9.0.4",
|
||||
"splitpanes": "^3.1.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"unplugin-auto-import": "^0.17.3",
|
||||
"unplugin-icons": "^0.18.2",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-icons": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"unplugin-vue-macros": "^2.7.9",
|
||||
"vite": "5.0.11",
|
||||
"unplugin-vue-macros": "^2.7.10",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-pages": "^0.32.0",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue": "^3.4.8",
|
||||
"vue-i18n": "^9.9.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-i18n": "^9.9.1",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pinia/testing": "^0.1.3",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@types/d3-array": "^3.2.1",
|
||||
"@types/d3-ease": "^3.0.2",
|
||||
"@types/d3-scale": "^4.0.8",
|
||||
@@ -81,25 +81,25 @@
|
||||
"@types/d3-shape": "^3.1.6",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^20.10.8",
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@vitejs/plugin-vue": "5.0.3",
|
||||
"@vue/compiler-sfc": "^3.4.8",
|
||||
"@vue/test-utils": "^2.4.3",
|
||||
"bumpp": "^9.2.1",
|
||||
"c8": "^9.0.0",
|
||||
"@vue/compiler-sfc": "^3.4.15",
|
||||
"@vue/test-utils": "^2.4.4",
|
||||
"bumpp": "^9.3.0",
|
||||
"c8": "^9.1.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"jest-serializer-vue": "^3.1.0",
|
||||
"jsdom": "^23.2.0",
|
||||
"lint-staged": "^15.2.0",
|
||||
"prettier": "^3.1.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"lint-staged": "^15.2.1",
|
||||
"prettier": "^3.2.4",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vitepress": "1.0.0-rc.36",
|
||||
"vitest": "^1.1.3",
|
||||
"vitepress": "1.0.0-rc.41",
|
||||
"vitest": "^1.2.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||