feat: supports multiple hosts in parallel and update hosts menu (#2269)
* feat: updates host menu to be part of side menu * updates routes to be host/id * fixes go tests * fixes js tests * fixes typescheck * fixes int tests * fixes mobile * fixes bug in merging containers * fixed minor bug with menu
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
certs
|
||||
dist
|
||||
node_modules
|
||||
.cache
|
||||
|
||||
2
.reflex
@@ -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 main.go --level debug --remote-host tcp://64.225.88.189:2376
|
||||
|
||||
2
Makefile
@@ -31,4 +31,4 @@ dev:
|
||||
|
||||
.PHONY: int
|
||||
int:
|
||||
docker compose up --force-recreate --exit-code-from playwright
|
||||
docker compose up --build --force-recreate --exit-code-from playwright
|
||||
|
||||
2
assets/components.d.ts
vendored
@@ -19,6 +19,7 @@ declare module 'vue' {
|
||||
ContainerPopup: typeof import('./components/LogViewer/ContainerPopup.vue')['default']
|
||||
ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
|
||||
ContainerTitle: typeof import('./components/LogViewer/ContainerTitle.vue')['default']
|
||||
copy: typeof import('./components/SidePanel copy.vue')['default']
|
||||
DateTime: typeof import('./components/common/DateTime.vue')['default']
|
||||
DistanceTime: typeof import('./components/common/DistanceTime.vue')['default']
|
||||
DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.vue')['default']
|
||||
@@ -52,6 +53,7 @@ declare module 'vue' {
|
||||
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
|
||||
Search: typeof import('./components/Search.vue')['default']
|
||||
SideMenu: typeof import('./components/SideMenu.vue')['default']
|
||||
SidePanel: typeof import('./components/SidePanel.vue')['default']
|
||||
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
|
||||
SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default']
|
||||
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
<octicon:container-24 />
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="media-content">{{ item.host }} / {{ item.name }}</div>
|
||||
<div class="media-right">
|
||||
<span
|
||||
class="icon is-small column-icon"
|
||||
@@ -52,12 +50,13 @@ const store = useContainerStore();
|
||||
const { containers } = storeToRefs(store);
|
||||
|
||||
const list = computed(() => {
|
||||
return containers.value.map(({ id, created, name, state }) => {
|
||||
return containers.value.map(({ id, created, name, state, host }) => {
|
||||
return {
|
||||
id,
|
||||
created,
|
||||
name,
|
||||
state,
|
||||
host,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("<LogEventSource />", () => {
|
||||
LogViewer,
|
||||
},
|
||||
provide: {
|
||||
container: computed(() => ({ id: "abc", image: "test:v123" })),
|
||||
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
|
||||
"stream-config": reactive({ stdout: true, stderr: true }),
|
||||
scrollingPaused: computed(() => false),
|
||||
},
|
||||
@@ -85,7 +85,7 @@ describe("<LogEventSource />", () => {
|
||||
});
|
||||
}
|
||||
|
||||
const sourceUrl = "/api/logs/stream?id=abc&lastEventId=&host=localhost&stdout=1&stderr=1";
|
||||
const sourceUrl = "/api/logs/stream/localhost/abc?lastEventId=&stdout=1&stderr=1";
|
||||
|
||||
test("renders correctly", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
|
||||
@@ -1,123 +1,88 @@
|
||||
<template>
|
||||
<aside>
|
||||
<div class="columns is-marginless">
|
||||
<div class="column is-paddingless">
|
||||
<h1>
|
||||
<router-link :to="{ name: 'index' }">
|
||||
<svg class="logo">
|
||||
<use href="#logo"></use>
|
||||
</svg>
|
||||
</router-link>
|
||||
|
||||
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
|
||||
{{ hostname }}
|
||||
</small>
|
||||
</h1>
|
||||
<div v-if="config.hosts.length > 1" class="mb-3">
|
||||
<o-dropdown v-model="sessionHost" aria-role="list">
|
||||
<template #trigger>
|
||||
<o-button variant="primary" type="button" size="small">
|
||||
<span>{{ sessionHost }}</span>
|
||||
<span class="icon">
|
||||
<carbon:caret-down />
|
||||
</span>
|
||||
</o-button>
|
||||
</template>
|
||||
|
||||
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in config.hosts" :key="value">
|
||||
<span>{{ value }}</span>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-marginless">
|
||||
<div class="column is-narrow py-0 pl-0 pr-1">
|
||||
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
|
||||
<span class="icon">
|
||||
<mdi:light-magnify />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
|
||||
<router-link
|
||||
:to="{ name: 'settings' }"
|
||||
active-class="is-active"
|
||||
class="button is-rounded is-small"
|
||||
:aria-label="$t('title.settings')"
|
||||
>
|
||||
<span class="icon">
|
||||
<mdi:light-cog />
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="column is-narrow py-0 px-0" v-if="secured">
|
||||
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
|
||||
<span class="icon">
|
||||
<mdi:light-logout />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="menu-label is-hidden-mobile">{{ $t("label.containers") }}</p>
|
||||
<ul class="menu-list is-hidden-mobile" v-if="ready">
|
||||
<li v-for="item in sortedContainers" :key="item.id" :class="item.state">
|
||||
<popup>
|
||||
<router-link
|
||||
:to="{ name: 'container-id', params: { id: item.id } }"
|
||||
active-class="is-active"
|
||||
:title="item.name"
|
||||
>
|
||||
<div class="container is-flex is-align-items-center">
|
||||
<div class="is-flex-grow-1 is-ellipsis">
|
||||
<span>{{ item.name }}</span
|
||||
><span class="has-text-weight-light has-light-opacity" v-if="item.isSwarm">{{ item.swarmId }}</span>
|
||||
</div>
|
||||
<div class="is-flex-shrink-1 is-flex icons">
|
||||
<div
|
||||
class="icon is-small pin"
|
||||
@click.stop.prevent="store.appendActiveContainer(item)"
|
||||
v-show="!activeContainersById[item.id]"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
>
|
||||
<cil:columns />
|
||||
<div v-if="ready">
|
||||
<nav class="breadcrumb menu-label" aria-label="breadcrumbs">
|
||||
<ul v-if="sessionHost">
|
||||
<li>
|
||||
<a href="#" @click.prevent="setHost(null)">{{ sessionHost }}</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">{{ $t("label.containers") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else>
|
||||
<li>Hosts</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
|
||||
<ul class="menu-list" v-if="!sessionHost">
|
||||
<li v-for="host in config.hosts">
|
||||
<a @click.prevent="setHost(host)">{{ host }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="menu-list" v-else>
|
||||
<li v-for="item in sortedContainers" :key="item.id" :class="item.state">
|
||||
<popup>
|
||||
<router-link
|
||||
:to="{ name: 'container-id', params: { id: item.id } }"
|
||||
active-class="is-active"
|
||||
:title="item.name"
|
||||
>
|
||||
<div class="container is-flex is-align-items-center">
|
||||
<div class="is-flex-grow-1 is-ellipsis">
|
||||
<span>{{ item.name }}</span
|
||||
><span class="has-text-weight-light has-light-opacity" v-if="item.isSwarm">{{ item.swarmId }}</span>
|
||||
</div>
|
||||
<div class="is-flex-shrink-1 is-flex icons">
|
||||
<div
|
||||
class="icon is-small pin"
|
||||
@click.stop.prevent="store.appendActiveContainer(item)"
|
||||
v-show="!activeContainersById[item.id]"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
>
|
||||
<cil:columns />
|
||||
</div>
|
||||
|
||||
<container-health :health="item.health"></container-health>
|
||||
<container-health :health="item.health"></container-health>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<template #content>
|
||||
<container-popup :container="item"></container-popup>
|
||||
</template>
|
||||
</popup>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="menu-list is-hidden-mobile loading" v-else>
|
||||
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</router-link>
|
||||
<template #content>
|
||||
<container-popup :container="item"></container-popup>
|
||||
</template>
|
||||
</popup>
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</div>
|
||||
<ul class="menu-list is-hidden-mobile loading" v-else>
|
||||
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Container } from "@/models/Container";
|
||||
import { sessionHost } from "@/composables/storage";
|
||||
|
||||
const { base, secured, hostname } = config;
|
||||
const store = useContainerStore();
|
||||
|
||||
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
|
||||
|
||||
function setHost(host: string | null) {
|
||||
sessionHost.value = host;
|
||||
}
|
||||
|
||||
const sortedContainers = computed(() =>
|
||||
visibleContainers.value.sort((a, b) => {
|
||||
if (a.state === "running" && b.state !== "running") {
|
||||
return -1;
|
||||
} else if (a.state !== "running" && b.state === "running") {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
})
|
||||
visibleContainers.value
|
||||
.filter((c) => c.host === sessionHost.value)
|
||||
.sort((a, b) => {
|
||||
if (a.state === "running" && b.state !== "running") {
|
||||
return -1;
|
||||
} else if (a.state !== "running" && b.state === "running") {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const activeContainersById = computed(() =>
|
||||
@@ -128,32 +93,15 @@ const activeContainersById = computed(() =>
|
||||
);
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
aside {
|
||||
padding: 1em;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
width: inherit;
|
||||
|
||||
.is-hidden-mobile.is-active {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.has-light-opacity {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.logo {
|
||||
width: 122px;
|
||||
height: 54px;
|
||||
fill: var(--logo-color);
|
||||
}
|
||||
|
||||
.loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
li.exited a {
|
||||
li.exited a, li.dead a {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
@@ -176,4 +124,31 @@ a {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active,
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active {
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
.slide-left-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-right-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.slide-left-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.slide-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
71
assets/components/SidePanel.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<aside>
|
||||
<div class="columns is-marginless">
|
||||
<div class="column is-paddingless">
|
||||
<h1>
|
||||
<router-link :to="{ name: 'index' }">
|
||||
<svg class="logo">
|
||||
<use href="#logo"></use>
|
||||
</svg>
|
||||
</router-link>
|
||||
|
||||
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
|
||||
{{ hostname }}
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-marginless">
|
||||
<div class="column is-narrow py-0 pl-0 pr-1">
|
||||
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
|
||||
<span class="icon">
|
||||
<mdi:light-magnify />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
|
||||
<router-link
|
||||
:to="{ name: 'settings' }"
|
||||
active-class="is-active"
|
||||
class="button is-rounded is-small"
|
||||
:aria-label="$t('title.settings')"
|
||||
>
|
||||
<span class="icon">
|
||||
<mdi:light-cog />
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="column is-narrow py-0 px-0" v-if="secured">
|
||||
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
|
||||
<span class="icon">
|
||||
<mdi:light-logout />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<side-menu class="mt-4"></side-menu>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { base, secured, hostname } = config;
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
aside {
|
||||
padding: 1em;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
width: inherit;
|
||||
|
||||
.is-hidden-mobile.is-active {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 122px;
|
||||
height: 54px;
|
||||
fill: var(--logo-color);
|
||||
}
|
||||
</style>
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
|
||||
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
|
||||
<li v-for="item in visibleContainers" :key="item.id">
|
||||
<li v-for="item in sortedContainers" :key="item.id">
|
||||
<router-link
|
||||
:to="{ name: 'container-id', params: { id: item.id } }"
|
||||
active-class="is-active"
|
||||
@@ -97,6 +97,20 @@ let showNav = $ref(false);
|
||||
watch(route, () => {
|
||||
showNav = false;
|
||||
});
|
||||
|
||||
const sortedContainers = computed(() =>
|
||||
visibleContainers.value
|
||||
.filter((c) => c.host === sessionHost.value)
|
||||
.sort((a, b) => {
|
||||
if (a.state === "running" && b.state !== "running") {
|
||||
return -1;
|
||||
} else if (a.state !== "running" && b.state === "running") {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
})
|
||||
);
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
aside {
|
||||
|
||||
@@ -70,10 +70,8 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
|
||||
}
|
||||
|
||||
const params = {
|
||||
id: container.value.id,
|
||||
lastEventId,
|
||||
host: sessionHost.value,
|
||||
} as { id: string; lastEventId: string; host: string; stdout?: string; stderr?: string };
|
||||
} as { lastEventId: string; stdout?: string; stderr?: string };
|
||||
|
||||
if (streamConfig.stdout) {
|
||||
params.stdout = "1";
|
||||
@@ -82,7 +80,11 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
|
||||
params.stderr = "1";
|
||||
}
|
||||
|
||||
es = new EventSource(`${config.base}/api/logs/stream?${new URLSearchParams(params).toString()}`);
|
||||
es = new EventSource(
|
||||
`${config.base}/api/logs/stream/${container.value.host}/${container.value.id}?${new URLSearchParams(
|
||||
params
|
||||
).toString()}`
|
||||
);
|
||||
es.addEventListener("container-stopped", () => {
|
||||
es?.close();
|
||||
es = null;
|
||||
@@ -111,11 +113,9 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
|
||||
const from = new Date(to.getTime() + delta);
|
||||
|
||||
const params = {
|
||||
id: container.value.id,
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
host: sessionHost.value,
|
||||
} as { id: string; from: string; to: string; host: string; stdout?: string; stderr?: string };
|
||||
} as { from: string; to: string; stdout?: string; stderr?: string };
|
||||
|
||||
if (streamConfig.stdout) {
|
||||
params.stdout = "1";
|
||||
@@ -124,7 +124,13 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
|
||||
params.stderr = "1";
|
||||
}
|
||||
|
||||
const logs = await (await fetch(`${config.base}/api/logs?${new URLSearchParams(params).toString()}`)).text();
|
||||
const logs = await (
|
||||
await fetch(
|
||||
`${config.base}/api/logs/${container.value.host}/${container.value.id}?${new URLSearchParams(
|
||||
params
|
||||
).toString()}`
|
||||
)
|
||||
).text();
|
||||
if (logs) {
|
||||
const newMessages = logs
|
||||
.trim()
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
const sessionHost = useSessionStorage("host", config.hosts[0]);
|
||||
const sessionHost = useSessionStorage<string | null>("host", null);
|
||||
|
||||
if (config.hosts.length === 1 && !sessionHost.value) {
|
||||
sessionHost.value = config.hosts[0];
|
||||
}
|
||||
|
||||
function persistentVisibleKeys(container: ComputedRef<Container>) {
|
||||
return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
|
||||
<splitpanes @resized="onResized($event)">
|
||||
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
|
||||
<side-menu @search="showFuzzySearch"></side-menu>
|
||||
<side-panel @search="showFuzzySearch"></side-panel>
|
||||
</pane>
|
||||
<pane min-size="10">
|
||||
<splitpanes>
|
||||
|
||||
@@ -12,7 +12,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", "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);
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ export class Container {
|
||||
public readonly image: string,
|
||||
public readonly name: string,
|
||||
public readonly command: string,
|
||||
public readonly host: string,
|
||||
public status: string,
|
||||
public state: ContainerState,
|
||||
public health?: ContainerHealth
|
||||
|
||||
@@ -23,7 +23,7 @@ if (config.version == "{{ .Version }}") {
|
||||
config.authorizationNeeded = false;
|
||||
config.secured = false;
|
||||
config.hostname = "localhost";
|
||||
config.hosts = ["localhost"];
|
||||
config.hosts = ["localhost", "64.225.88.189"];
|
||||
} else {
|
||||
config.version = config.version.replace(/^v/, "");
|
||||
config.authorizationNeeded = config.authorizationNeeded === "true";
|
||||
|
||||
@@ -23,21 +23,13 @@ export const useContainerStore = defineStore("container", () => {
|
||||
|
||||
const activeContainers = computed(() => activeContainerIds.value.map((id) => allContainersById.value[id]));
|
||||
|
||||
watch(
|
||||
sessionHost,
|
||||
() => {
|
||||
connect();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function connect() {
|
||||
es?.close();
|
||||
ready.value = false;
|
||||
es = new EventSource(`${config.base}/api/events/stream?host=${sessionHost.value}`);
|
||||
es = new EventSource(`${config.base}/api/events/stream`);
|
||||
|
||||
es.addEventListener("containers-changed", (e: Event) =>
|
||||
setContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
|
||||
updateContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
|
||||
);
|
||||
es.addEventListener("container-stat", (e) => {
|
||||
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
|
||||
@@ -66,17 +58,35 @@ export const useContainerStore = defineStore("container", () => {
|
||||
watchOnce(containers, () => (ready.value = true));
|
||||
}
|
||||
|
||||
const setContainers = (newContainers: ContainerJson[]) => {
|
||||
containers.value = newContainers.map((c) => {
|
||||
connect();
|
||||
|
||||
const updateContainers = (containersPayload: ContainerJson[]) => {
|
||||
const existingContainers = containersPayload.filter((c) => allContainersById.value[c.id]);
|
||||
const newContainers = containersPayload.filter((c) => !allContainersById.value[c.id]);
|
||||
|
||||
existingContainers.forEach((c) => {
|
||||
const existing = allContainersById.value[c.id];
|
||||
if (existing) {
|
||||
existing.status = c.status;
|
||||
existing.state = c.state;
|
||||
existing.health = c.health;
|
||||
return existing;
|
||||
}
|
||||
return new Container(c.id, new Date(c.created * 1000), c.image, c.name, c.command, c.status, c.state, c.health);
|
||||
existing.status = c.status;
|
||||
existing.state = c.state;
|
||||
existing.health = c.health;
|
||||
});
|
||||
|
||||
containers.value = [
|
||||
...containers.value,
|
||||
...newContainers.map((c) => {
|
||||
return new Container(
|
||||
c.id,
|
||||
new Date(c.created * 1000),
|
||||
c.image,
|
||||
c.name,
|
||||
c.command,
|
||||
c.host,
|
||||
c.status,
|
||||
c.state,
|
||||
c.health
|
||||
);
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
|
||||
|
||||
1
assets/types/Container.d.ts
vendored
@@ -13,6 +13,7 @@ export type ContainerJson = {
|
||||
readonly command: string;
|
||||
readonly status: string;
|
||||
readonly state: ContainerState;
|
||||
readonly host: string;
|
||||
readonly health?: ContainerHealth;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
type dockerClient struct {
|
||||
cli dockerProxy
|
||||
filters filters.Args
|
||||
host string
|
||||
}
|
||||
|
||||
type StdType int
|
||||
@@ -64,10 +65,11 @@ type Client interface {
|
||||
FindContainer(string) (Container, error)
|
||||
ContainerLogs(context.Context, string, string, StdType) (io.ReadCloser, error)
|
||||
ContainerLogReader(context.Context, string) (io.ReadCloser, error)
|
||||
Events(context.Context) (<-chan ContainerEvent, <-chan 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() string
|
||||
}
|
||||
|
||||
// NewClientWithFilters creates a new instance of Client with docker filters
|
||||
@@ -87,7 +89,7 @@ func NewClientWithFilters(f map[string][]string) (Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dockerClient{cli, filterArgs}, nil
|
||||
return &dockerClient{cli, filterArgs, "localhost"}, nil
|
||||
}
|
||||
|
||||
func NewClientWithTlsAndFilter(f map[string][]string, connection string) (Client, error) {
|
||||
@@ -110,7 +112,10 @@ func NewClientWithTlsAndFilter(f map[string][]string, connection string) (Client
|
||||
}
|
||||
|
||||
host := remoteUrl.Hostname()
|
||||
basePath := "/certs"
|
||||
basePath, err := filepath.Abs("./certs")
|
||||
if err != nil {
|
||||
log.Fatalf("error converting certs path to absolute: %s", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(basePath, host)); !os.IsNotExist(err) {
|
||||
basePath = filepath.Join(basePath, host)
|
||||
@@ -139,7 +144,7 @@ func NewClientWithTlsAndFilter(f map[string][]string, connection string) (Client
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dockerClient{cli, filterArgs}, nil
|
||||
return &dockerClient{cli, filterArgs, host}, nil
|
||||
}
|
||||
|
||||
func (d *dockerClient) FindContainer(id string) (Container, error) {
|
||||
@@ -186,6 +191,7 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
Created: c.Created,
|
||||
State: c.State,
|
||||
Status: c.Status,
|
||||
Host: d.host,
|
||||
Health: findBetweenParentheses(c.Status),
|
||||
}
|
||||
containers = append(containers, container)
|
||||
@@ -284,12 +290,10 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since strin
|
||||
return newLogReader(reader, containerJSON.Config.Tty, true), nil
|
||||
}
|
||||
|
||||
func (d *dockerClient) Events(ctx context.Context) (<-chan ContainerEvent, <-chan error) {
|
||||
func (d *dockerClient) Events(ctx context.Context, messages chan<- ContainerEvent) <-chan error {
|
||||
dockerMessages, errors := d.cli.Events(ctx, types.EventsOptions{})
|
||||
messages := make(chan ContainerEvent)
|
||||
|
||||
go func() {
|
||||
defer close(messages)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -304,13 +308,14 @@ func (d *dockerClient) Events(ctx context.Context) (<-chan ContainerEvent, <-cha
|
||||
messages <- ContainerEvent{
|
||||
ActorID: message.Actor.ID[:12],
|
||||
Name: message.Action,
|
||||
Host: d.host,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return messages, errors
|
||||
return errors
|
||||
}
|
||||
|
||||
func (d *dockerClient) ContainerLogReader(ctx context.Context, id string) (io.ReadCloser, error) {
|
||||
@@ -366,6 +371,10 @@ func (d *dockerClient) Ping(ctx context.Context) (types.Ping, error) {
|
||||
return d.cli.Ping(ctx)
|
||||
}
|
||||
|
||||
func (d *dockerClient) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
var PARENTHESIS_RE = regexp.MustCompile(`\(([a-zA-Z]+)\)`)
|
||||
|
||||
func findBetweenParentheses(s string) string {
|
||||
|
||||
@@ -44,12 +44,7 @@ func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options type
|
||||
|
||||
func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
args := m.Called(ctx, containerID)
|
||||
json, ok := args.Get(0).(types.ContainerJSON)
|
||||
if !ok && args.Get(0) != nil {
|
||||
panic("proxies return value is not of type types.ContainerJSON")
|
||||
}
|
||||
|
||||
return json, args.Error(1)
|
||||
return args.Get(0).(types.ContainerJSON), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error) {
|
||||
@@ -59,7 +54,7 @@ func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, st
|
||||
func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
client := &dockerClient{proxy, filters.NewArgs(), "localhost"}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
assert.Empty(t, list, "list should be empty")
|
||||
@@ -71,7 +66,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 := &dockerClient{proxy, filters.NewArgs()}
|
||||
client := &dockerClient{proxy, filters.NewArgs(), "localhost"}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
assert.Nil(t, list, "list should be nil")
|
||||
@@ -94,7 +89,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
client := &dockerClient{proxy, filters.NewArgs(), "localhost"}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
@@ -104,11 +99,13 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
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",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -132,7 +129,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
||||
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
|
||||
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
client := &dockerClient{proxy, filters.NewArgs(), "localhost"}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
|
||||
|
||||
actual, _ := io.ReadAll(logReader)
|
||||
@@ -153,7 +150,7 @@ func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
|
||||
json := types.ContainerJSON{Config: &container.Config{Tty: true}}
|
||||
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
client := &dockerClient{proxy, filters.NewArgs(), "localhost"}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, "", STDALL)
|
||||
|
||||
actual, _ := io.ReadAll(logReader)
|
||||
@@ -168,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 := &dockerClient{proxy, filters.NewArgs()}
|
||||
client := &dockerClient{proxy, filters.NewArgs(), "localhost"}
|
||||
|
||||
reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
|
||||
|
||||
@@ -191,7 +188,7 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
client := &dockerClient{proxy, filters.NewArgs(), "localhost"}
|
||||
|
||||
container, err := client.FindContainer("abcdefghijkl")
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
@@ -200,6 +197,7 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
ID: "abcdefghijkl",
|
||||
Name: "z_test_container",
|
||||
Names: []string{"/z_test_container"},
|
||||
Host: "localhost",
|
||||
})
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
@@ -218,7 +216,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
client := &dockerClient{proxy, filters.NewArgs(), "localhost"}
|
||||
|
||||
_, err := client.FindContainer("not_valid")
|
||||
require.Error(t, err, "error should be thrown")
|
||||
|
||||
@@ -16,6 +16,7 @@ type Container struct {
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Health string `json:"health,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerStat represent stats instant for a container
|
||||
@@ -30,6 +31,7 @@ type ContainerStat struct {
|
||||
type ContainerEvent struct {
|
||||
ActorID string `json:"actorId"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type LogPosition string
|
||||
|
||||
@@ -5,5 +5,5 @@ test("authentication", async ({ page }) => {
|
||||
await page.locator('input[name="username"]').fill("foo");
|
||||
await page.locator('input[name="password"]').fill("bar");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("p.menu-label")).toHaveText("Containers");
|
||||
await expect(page.locator(".menu-label [aria-current]")).toHaveText("Containers");
|
||||
});
|
||||
|
||||
@@ -27,6 +27,6 @@ test.describe("es locale", () => {
|
||||
test.use({ locale: "es" });
|
||||
|
||||
test("translated text", async ({ page }) => {
|
||||
await expect(page.locator("p.menu-label").getByText("Contenedores")).toBeVisible();
|
||||
await expect(page.locator(".menu-label [aria-current]").getByText("Contenedores")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
14
main.go
@@ -141,14 +141,18 @@ func createClients(args args, localClientFactory func(map[string][]string) (dock
|
||||
clients := make(map[string]docker.Client)
|
||||
|
||||
if localClient := createLocalClient(args, localClientFactory); localClient != nil {
|
||||
clients["localhost"] = localClient
|
||||
clients[localClient.Host()] = localClient
|
||||
}
|
||||
|
||||
for _, host := range args.RemoteHost {
|
||||
log.Infof("Creating client for %s", host)
|
||||
client, err := remoteClientFactory(args.Filter, host)
|
||||
if err == nil {
|
||||
clients[host] = client
|
||||
if client, err := remoteClientFactory(args.Filter, host); err == nil {
|
||||
if _, err := client.ListContainers(); err == nil {
|
||||
log.Debugf("Connected to local Docker Engine")
|
||||
clients[client.Host()] = client
|
||||
} else {
|
||||
log.Warnf("Could not connect to remote host %s: %s", host, err)
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Could not create client for %s: %s", host, err)
|
||||
}
|
||||
@@ -184,14 +188,12 @@ func createServer(args args, clients map[string]docker.Client) *http.Server {
|
||||
func createLocalClient(args args, localClientFactory func(map[string][]string) (docker.Client, error)) docker.Client {
|
||||
for i := 1; ; i++ {
|
||||
dockerClient, err := localClientFactory(args.Filter)
|
||||
|
||||
if err == nil {
|
||||
_, err := dockerClient.ListContainers()
|
||||
|
||||
if err == nil {
|
||||
log.Debugf("Connected to local Docker Engine")
|
||||
return dockerClient
|
||||
|
||||
}
|
||||
}
|
||||
if args.WaitForDockerSeconds > 0 {
|
||||
|
||||
24
main_test.go
@@ -19,10 +19,16 @@ func (f *fakeClient) ListContainers() ([]docker.Container, error) {
|
||||
return args.Get(0).([]docker.Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (f *fakeClient) Host() string {
|
||||
args := f.Called()
|
||||
return args.String(0)
|
||||
}
|
||||
|
||||
func Test_valid_localhost(t *testing.T) {
|
||||
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
client := new(fakeClient)
|
||||
client.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
client.On("Host").Return("localhost")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -37,6 +43,7 @@ func Test_invalid_localhost(t *testing.T) {
|
||||
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
client := new(fakeClient)
|
||||
client.On("ListContainers").Return([]docker.Container{}, errors.New("error"))
|
||||
client.On("Host").Return("localhost")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -51,22 +58,26 @@ func Test_valid_remote(t *testing.T) {
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
client := new(fakeClient)
|
||||
client.On("ListContainers").Return([]docker.Container{}, errors.New("error"))
|
||||
client.On("Host").Return("localhost")
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host string) (docker.Client, error) {
|
||||
client := new(fakeClient)
|
||||
client.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
client.On("Host").Return("test")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
args := args{
|
||||
RemoteHost: []string{"tcp://localhost:2375"},
|
||||
RemoteHost: []string{"tcp://test:2375"},
|
||||
}
|
||||
|
||||
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory)
|
||||
|
||||
assert.Equal(t, 1, len(clients))
|
||||
assert.Contains(t, clients, "tcp://localhost:2375")
|
||||
assert.Contains(t, clients, "test")
|
||||
assert.NotContains(t, clients, "localhost")
|
||||
}
|
||||
|
||||
@@ -74,22 +85,25 @@ func Test_valid_remote_and_local(t *testing.T) {
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
client := new(fakeClient)
|
||||
client.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
client.On("Host").Return("localhost")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host string) (docker.Client, error) {
|
||||
client := new(fakeClient)
|
||||
client.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
client.On("Host").Return("test")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
args := args{
|
||||
RemoteHost: []string{"tcp://localhost:2375"},
|
||||
RemoteHost: []string{"tcp://test:2375"},
|
||||
}
|
||||
|
||||
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory)
|
||||
|
||||
assert.Equal(t, 2, len(clients))
|
||||
assert.Contains(t, clients, "tcp://localhost:2375")
|
||||
assert.Contains(t, clients, "test")
|
||||
assert.Contains(t, clients, "localhost")
|
||||
}
|
||||
|
||||
@@ -97,11 +111,13 @@ func Test_no_clients(t *testing.T) {
|
||||
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
|
||||
client := new(fakeClient)
|
||||
client.On("ListContainers").Return([]docker.Container{}, errors.New("error"))
|
||||
client.On("Host").Return("localhost")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
fakeRemoteClientFactory := func(filter map[string][]string, host string) (docker.Client, error) {
|
||||
client := new(fakeClient)
|
||||
client.On("Host").Return("test")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ Content-Type: text/html
|
||||
/* snapshot: Test_handler_between_dates */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: application/ld+json; charset=UTF-8
|
||||
|
||||
{"m":"INFO Testing logs...","ts":1589396137772,"id":1122614848,"l":"info","s":"stdout"}
|
||||
@@ -97,6 +98,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -109,6 +111,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -121,11 +124,12 @@ data: []
|
||||
|
||||
|
||||
event: container-start
|
||||
data: {"actorId":"1234","name":"start"}
|
||||
data: {"actorId":"1234","name":"start","host":"localhost"}
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_error_finding_container */
|
||||
HTTP/1.1 404 Not Found
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
@@ -137,6 +141,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Accel-Buffering: no
|
||||
X-Content-Type-Options: nosniff
|
||||
@@ -146,6 +151,7 @@ test error
|
||||
/* snapshot: Test_handler_streamLogs_error_std */
|
||||
HTTP/1.1 400 Bad Request
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
@@ -157,6 +163,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -171,6 +178,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
@@ -183,6 +191,7 @@ Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; manifest-src 'self'; connect-src 'self' api.github.com;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
|
||||
@@ -27,27 +27,29 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
client := h.clientFromRequest(r)
|
||||
events, err := client.Events(ctx)
|
||||
events := make(chan docker.ContainerEvent)
|
||||
stats := make(chan docker.ContainerStat)
|
||||
|
||||
if err := sendContainersJSON(client, w); err != nil {
|
||||
log.Errorf("error while encoding containers to stream: %v", err)
|
||||
}
|
||||
f.Flush()
|
||||
|
||||
if containers, err := client.ListContainers(); err == nil {
|
||||
go func() {
|
||||
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)
|
||||
for _, client := range h.clients {
|
||||
client.Events(ctx, events)
|
||||
if err := sendContainersJSON(client, w); err != nil {
|
||||
log.Errorf("error while encoding containers to stream: %v", err)
|
||||
}
|
||||
if containers, err := client.ListContainers(); err == nil {
|
||||
go func(client docker.Client) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
f.Flush()
|
||||
|
||||
for {
|
||||
select {
|
||||
case stat := <-stats:
|
||||
@@ -66,10 +68,11 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debugf("triggering docker event: %v", event.Name)
|
||||
if event.Name == "start" {
|
||||
log.Debugf("found new container with id: %v", event.ActorID)
|
||||
if err := client.ContainerStats(ctx, event.ActorID, stats); err != nil && !errors.Is(err, context.Canceled) {
|
||||
|
||||
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)
|
||||
}
|
||||
if err := sendContainersJSON(client, w); err != nil {
|
||||
if err := sendContainersJSON(h.clients[event.Host], w); err != nil {
|
||||
log.Errorf("error encoding containers to stream: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -105,8 +108,6 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-err:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -50,7 +51,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
|
||||
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
|
||||
id := r.URL.Query().Get("id")
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var stdTypes docker.StdType
|
||||
if r.URL.Query().Has("stdout") {
|
||||
@@ -89,11 +90,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var stdTypes docker.StdType
|
||||
if r.URL.Query().Has("stdout") {
|
||||
|
||||
@@ -58,10 +58,10 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Route(base, func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(authorizationRequired)
|
||||
r.Get("/api/logs/stream", h.streamLogs)
|
||||
r.Get("/api/logs/stream/{host}/{id}", h.streamLogs)
|
||||
r.Get("/api/logs/download/{host}/{id}", h.downloadLogs) //TODO
|
||||
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
|
||||
r.Get("/api/events/stream", h.streamEvents)
|
||||
r.Get("/api/logs/download", h.downloadLogs)
|
||||
r.Get("/api/logs", h.fetchLogsBetweenDates)
|
||||
r.Get("/logout", h.clearSession)
|
||||
r.Get("/version", h.version)
|
||||
})
|
||||
@@ -195,11 +195,12 @@ func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *handler) clientFromRequest(r *http.Request) docker.Client {
|
||||
if !r.URL.Query().Has("host") {
|
||||
log.Fatalf("No host parameter found in request %v", r.URL)
|
||||
host := chi.URLParam(r, "host")
|
||||
|
||||
if host == "" {
|
||||
log.Fatalf("No host found for url %v", r.URL)
|
||||
}
|
||||
|
||||
host := r.URL.Query().Get("host")
|
||||
if client, ok := h.clients[host]; ok {
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func Test_createRoutes_username_password(t *testing.T) {
|
||||
|
||||
func Test_createRoutes_username_password_invalid(t *testing.T) {
|
||||
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream?id=123&stdout=1&stderr=1", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
@@ -182,7 +182,7 @@ func Test_createRoutes_username_password_valid_session(t *testing.T) {
|
||||
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
|
||||
|
||||
// Get cookie first
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream?id=123&stdout=1&stderr=1&host=localhost", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
session, _ := store.Get(req, sessionName)
|
||||
session.Values[authorityKey] = time.Now().Unix()
|
||||
@@ -191,7 +191,7 @@ func Test_createRoutes_username_password_valid_session(t *testing.T) {
|
||||
cookies := recorder.Result().Cookies()
|
||||
|
||||
// Test with cookie
|
||||
req, err = http.NewRequest("GET", "/api/logs/stream?id=123&stdout=1&stderr=1&host=localhost", nil)
|
||||
req, err = http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
req.AddCookie(cookies[0])
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -204,7 +204,7 @@ func Test_createRoutes_username_password_invalid_session(t *testing.T) {
|
||||
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, "since", docker.STDALL).Return(io.NopCloser(strings.NewReader("test data")), io.EOF)
|
||||
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream?id=123&stdout=1&stderr=1&host=localhost", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/123?stdout=1&stderr=1", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"})
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
71
web/routes_events_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/beme/abide"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_handler_streamEvents_happy(t *testing.T) {
|
||||
context, cancel := context.WithCancel(context.Background())
|
||||
req, err := http.NewRequestWithContext(context, "GET", "/api/events/stream", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
errChannel := make(chan error)
|
||||
|
||||
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
mockedClient.On("Events", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(errChannel).Run(func(args mock.Arguments) {
|
||||
messages := args.Get(1).(chan<- docker.ContainerEvent)
|
||||
go func() {
|
||||
messages <- docker.ContainerEvent{
|
||||
Name: "start",
|
||||
ActorID: "1234",
|
||||
Host: "localhost",
|
||||
}
|
||||
messages <- docker.ContainerEvent{
|
||||
Name: "something-random",
|
||||
ActorID: "1234",
|
||||
Host: "localhost",
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
})
|
||||
|
||||
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)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
@@ -19,12 +18,11 @@ import (
|
||||
|
||||
func Test_handler_streamLogs_happy(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/"+id, nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
q.Add("stdout", "true")
|
||||
q.Add("stderr", "true")
|
||||
q.Add("host", "localhost")
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
@@ -33,11 +31,7 @@ func Test_handler_streamLogs_happy(t *testing.T) {
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(reader, nil)
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
@@ -46,12 +40,11 @@ func Test_handler_streamLogs_happy(t *testing.T) {
|
||||
|
||||
func Test_handler_streamLogs_happy_with_id(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/"+id, nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
q.Add("stdout", "true")
|
||||
q.Add("stderr", "true")
|
||||
q.Add("host", "localhost")
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
@@ -60,11 +53,7 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) {
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(reader, nil)
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
@@ -73,12 +62,11 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) {
|
||||
|
||||
func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/"+id, nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
q.Add("stdout", "true")
|
||||
q.Add("stderr", "true")
|
||||
q.Add("host", "localhost")
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
@@ -86,11 +74,7 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), io.EOF)
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
@@ -99,23 +83,18 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
|
||||
|
||||
func Test_handler_streamLogs_error_finding_container(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/"+id, nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
q.Add("stdout", "true")
|
||||
q.Add("stderr", "true")
|
||||
q.Add("host", "localhost")
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container"))
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
@@ -124,12 +103,11 @@ func Test_handler_streamLogs_error_finding_container(t *testing.T) {
|
||||
|
||||
func Test_handler_streamLogs_error_reading(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/"+id, nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
q.Add("stdout", "true")
|
||||
q.Add("stderr", "true")
|
||||
q.Add("host", "localhost")
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
@@ -137,11 +115,7 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), errors.New("test error"))
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
@@ -150,115 +124,13 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
|
||||
|
||||
func Test_handler_streamLogs_error_std(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
q.Add("host", "localhost")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream/localhost/"+id, nil)
|
||||
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_handler_streamEvents_happy(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/events/stream", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
q := req.URL.Query()
|
||||
q.Add("host", "localhost")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
mockedClient := new(MockedClient)
|
||||
messages := make(chan docker.ContainerEvent)
|
||||
errChannel := make(chan error)
|
||||
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
|
||||
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
|
||||
go func() {
|
||||
messages <- docker.ContainerEvent{
|
||||
Name: "start",
|
||||
ActorID: "1234",
|
||||
}
|
||||
messages <- docker.ContainerEvent{
|
||||
Name: "something-random",
|
||||
ActorID: "1234",
|
||||
}
|
||||
close(messages)
|
||||
}()
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_handler_streamEvents_error(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/events/stream", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
q := req.URL.Query()
|
||||
q.Add("host", "localhost")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
mockedClient := new(MockedClient)
|
||||
messages := make(chan docker.ContainerEvent)
|
||||
errChannel := make(chan error)
|
||||
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
|
||||
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
|
||||
go func() {
|
||||
errChannel <- errors.New("fake error")
|
||||
close(messages)
|
||||
}()
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
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.")
|
||||
q := req.URL.Query()
|
||||
q.Add("host", "localhost")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
|
||||
messages := make(chan docker.ContainerEvent)
|
||||
errChannel := make(chan error)
|
||||
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
|
||||
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
go func() {
|
||||
cancel()
|
||||
}()
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
@@ -267,7 +139,7 @@ func Test_handler_streamEvents_error_request(t *testing.T) {
|
||||
|
||||
// for /api/logs
|
||||
func Test_handler_between_dates(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/logs", nil)
|
||||
req, err := http.NewRequest("GET", "/api/logs/localhost/123456", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
from, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00Z")
|
||||
@@ -276,21 +148,16 @@ func Test_handler_between_dates(t *testing.T) {
|
||||
q := req.URL.Query()
|
||||
q.Add("from", from.Format(time.RFC3339))
|
||||
q.Add("to", to.Format(time.RFC3339))
|
||||
q.Add("id", "123456")
|
||||
q.Add("stdout", "true")
|
||||
q.Add("stderr", "true")
|
||||
q.Add("host", "localhost")
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
reader := io.NopCloser(strings.NewReader("OUT2020-05-13T18:55:37.772853839Z INFO Testing logs...\nERR2020-05-13T18:55:37.772853839Z INFO Testing logs...\n"))
|
||||
mockedClient.On("ContainerLogsBetweenDates", mock.Anything, "123456", from, to, docker.STDALL).Return(reader, nil)
|
||||
|
||||
clients := map[string]docker.Client{
|
||||
"localhost": mockedClient,
|
||||
}
|
||||
h := handler{clients: clients, config: &Config{}}
|
||||
handler := http.HandlerFunc(h.fetchLogsBetweenDates)
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
|
||||
@@ -35,18 +35,9 @@ func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since strin
|
||||
return args.Get(0).(io.ReadCloser), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) Events(ctx context.Context) (<-chan docker.ContainerEvent, <-chan error) {
|
||||
args := m.Called(ctx)
|
||||
channel, ok := args.Get(0).(chan docker.ContainerEvent)
|
||||
if !ok {
|
||||
panic("channel is not of type chan events.Message")
|
||||
}
|
||||
|
||||
err, ok := args.Get(1).(chan error)
|
||||
if !ok {
|
||||
panic("error is not of type chan error")
|
||||
}
|
||||
return channel, err
|
||||
func (m *MockedClient) Events(ctx context.Context, events chan<- docker.ContainerEvent) <-chan error {
|
||||
args := m.Called(ctx, events)
|
||||
return args.Get(0).(chan error)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerStats(context.Context, string, chan<- docker.ContainerStat) error {
|
||||
@@ -58,6 +49,11 @@ func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string,
|
||||
return args.Get(0).(io.ReadCloser), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) Host() string {
|
||||
args := m.Called()
|
||||
return args.String(0)
|
||||
}
|
||||
|
||||
func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux {
|
||||
if client == nil {
|
||||
client = new(MockedClient)
|
||||
@@ -79,3 +75,7 @@ func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux
|
||||
config: &config,
|
||||
})
|
||||
}
|
||||
|
||||
func createDefaultHandler(client docker.Client) *chi.Mux {
|
||||
return createHandler(client, nil, Config{Base: "/"})
|
||||
}
|
||||
|
||||