mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-24 06:28:42 +01:00
feat: collects all stats like cpu and mem in background for up to 5 minutes (#2740)
This commit is contained in:
3
assets/auto-imports.d.ts
vendored
3
assets/auto-imports.d.ts
vendored
@@ -290,6 +290,7 @@ declare global {
|
|||||||
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
|
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
|
||||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||||
const useShare: typeof import('@vueuse/core')['useShare']
|
const useShare: typeof import('@vueuse/core')['useShare']
|
||||||
|
const useSimpleRefHistory: typeof import('./utils/index')['useSimpleRefHistory']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||||
@@ -646,6 +647,7 @@ declare module 'vue' {
|
|||||||
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
|
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
|
||||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||||
|
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
|
||||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||||
@@ -995,6 +997,7 @@ declare module '@vue/runtime-core' {
|
|||||||
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
|
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
|
||||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||||
|
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
|
||||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ function createFuzzySearchModal() {
|
|||||||
initialState: {
|
initialState: {
|
||||||
container: {
|
container: {
|
||||||
containers: [
|
containers: [
|
||||||
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running"),
|
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("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running", []),
|
||||||
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "exited"),
|
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "exited", []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ const { container } = useContainerContext();
|
|||||||
const cpuData = computedWithControl(
|
const cpuData = computedWithControl(
|
||||||
() => container.value.stat,
|
() => container.value.stat,
|
||||||
() => {
|
() => {
|
||||||
const history = container.value.statHistory;
|
const history = container.value.statsHistory;
|
||||||
const points: Point<unknown>[] = history.map((stat, i) => ({
|
const points: Point<unknown>[] = history.map((stat, i) => ({
|
||||||
x: i,
|
x: i,
|
||||||
y: Math.max(0, stat.snapshot.cpu),
|
y: Math.max(0, stat.cpu),
|
||||||
value: Math.max(0, stat.snapshot.cpu).toFixed(2) + "%",
|
value: Math.max(0, stat.cpu).toFixed(2) + "%",
|
||||||
}));
|
}));
|
||||||
return points;
|
return points;
|
||||||
},
|
},
|
||||||
@@ -24,11 +24,11 @@ const cpuData = computedWithControl(
|
|||||||
const memoryData = computedWithControl(
|
const memoryData = computedWithControl(
|
||||||
() => container.value.stat,
|
() => container.value.stat,
|
||||||
() => {
|
() => {
|
||||||
const history = container.value.statHistory;
|
const history = container.value.statsHistory;
|
||||||
const points: Point<string>[] = history.map((stat, i) => ({
|
const points: Point<string>[] = history.map((stat, i) => ({
|
||||||
x: i,
|
x: i,
|
||||||
y: stat.snapshot.memory,
|
y: stat.memory,
|
||||||
value: formatBytes(stat.snapshot.memoryUsage),
|
value: formatBytes(stat.memoryUsage),
|
||||||
}));
|
}));
|
||||||
return points;
|
return points;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { area, curveStep } from "d3-shape";
|
|||||||
|
|
||||||
const d3 = { extent, scaleLinear, area, curveStep };
|
const d3 = { extent, scaleLinear, area, curveStep };
|
||||||
const { data, width = 150, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
|
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 y = d3.scaleLinear().range([height, 0]);
|
||||||
|
|
||||||
const selectedPoint = defineEmit<[value: Point<unknown>]>();
|
const selectedPoint = defineEmit<[value: Point<unknown>]>();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe("Container", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
test.each(names)("name %s should be %s and %s", (name, expectedName, expectedSwarmId) => {
|
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.name).toBe(expectedName);
|
||||||
expect(c.swarmId).toBe(expectedSwarmId);
|
expect(c.swarmId).toBe(expectedSwarmId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
|
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
|
||||||
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
|
import { useExponentialMovingAverage, useSimpleRefHistory } from "@/utils";
|
||||||
import { useExponentialMovingAverage } from "@/utils";
|
|
||||||
import { Ref } from "vue";
|
import { Ref } from "vue";
|
||||||
|
|
||||||
type Stat = Omit<ContainerStat, "id">;
|
type Stat = Omit<ContainerStat, "id">;
|
||||||
@@ -19,7 +18,7 @@ const hosts = computed(() =>
|
|||||||
|
|
||||||
export class Container {
|
export class Container {
|
||||||
private _stat: Ref<Stat>;
|
private _stat: Ref<Stat>;
|
||||||
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
|
private readonly _statsHistory: Ref<Stat[]>;
|
||||||
public readonly swarmId: string | null = null;
|
public readonly swarmId: string | null = null;
|
||||||
public readonly isSwarm: boolean = false;
|
public readonly isSwarm: boolean = false;
|
||||||
private readonly movingAverageStat: Ref<Stat>;
|
private readonly movingAverageStat: Ref<Stat>;
|
||||||
@@ -34,10 +33,11 @@ export class Container {
|
|||||||
public readonly labels = {} as Record<string, string>,
|
public readonly labels = {} as Record<string, string>,
|
||||||
public status: string,
|
public status: string,
|
||||||
public state: ContainerState,
|
public state: ContainerState,
|
||||||
|
stats: Stat[],
|
||||||
public health?: ContainerHealth,
|
public health?: ContainerHealth,
|
||||||
) {
|
) {
|
||||||
this._stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
|
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);
|
this.movingAverageStat = useExponentialMovingAverage(this._stat, 0.2);
|
||||||
|
|
||||||
const match = name.match(SWARM_ID_REGEX);
|
const match = name.match(SWARM_ID_REGEX);
|
||||||
@@ -48,8 +48,8 @@ export class Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get statHistory() {
|
get statsHistory() {
|
||||||
return unref(this.throttledStatHistory.history);
|
return unref(this._statsHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
get movingAverage() {
|
get movingAverage() {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const useContainerStore = defineStore("container", () => {
|
|||||||
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
|
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
|
||||||
const container = allContainersById.value[event.actorId];
|
const container = allContainersById.value[event.actorId];
|
||||||
if (container) {
|
if (container) {
|
||||||
container.state = "dead";
|
container.state = "exited";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,6 +127,7 @@ export const useContainerStore = defineStore("container", () => {
|
|||||||
c.labels,
|
c.labels,
|
||||||
c.status,
|
c.status,
|
||||||
c.state,
|
c.state,
|
||||||
|
c.stats,
|
||||||
c.health,
|
c.health,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
1
assets/types/Container.d.ts
vendored
1
assets/types/Container.d.ts
vendored
@@ -15,6 +15,7 @@ export type ContainerJson = {
|
|||||||
readonly state: ContainerState;
|
readonly state: ContainerState;
|
||||||
readonly host: string;
|
readonly host: string;
|
||||||
readonly labels: Record<string, string>;
|
readonly labels: Record<string, string>;
|
||||||
|
readonly stats: ContainerStat[];
|
||||||
readonly health?: ContainerHealth;
|
readonly health?: ContainerHealth;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,27 @@ export function useExponentialMovingAverage<T extends Record<string, number>>(so
|
|||||||
|
|
||||||
return ema;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/amir20/dozzle/internal/utils"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/events"
|
"github.com/docker/docker/api/types/events"
|
||||||
@@ -55,18 +56,30 @@ type DockerCLI interface {
|
|||||||
ContainerRestart(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
|
cli DockerCLI
|
||||||
filters filters.Args
|
filters filters.Args
|
||||||
host *Host
|
host *Host
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(cli DockerCLI, filters filters.Args, host *Host) *Client {
|
func NewClient(cli DockerCLI, filters filters.Args, host *Host) Client {
|
||||||
return &Client{cli, filters, host}
|
return &_client{cli, filters, host}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientWithFilters creates a new instance of Client with docker filters
|
// 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()
|
filterArgs := filters.NewArgs()
|
||||||
for key, values := range f {
|
for key, values := range f {
|
||||||
for _, value := range values {
|
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
|
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()
|
filterArgs := filters.NewArgs()
|
||||||
for key, values := range f {
|
for key, values := range f {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
@@ -121,7 +134,7 @@ func NewClientWithTlsAndFilter(f map[string][]string, host Host) (*Client, error
|
|||||||
return NewClient(cli, filterArgs, &host), nil
|
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
|
var container Container
|
||||||
containers, err := d.ListContainers()
|
containers, err := d.ListContainers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -149,7 +162,7 @@ func (d *Client) FindContainer(id string) (Container, error) {
|
|||||||
return container, nil
|
return container, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Client) ContainerActions(action string, containerID string) error {
|
func (d *_client) ContainerActions(action string, containerID string) error {
|
||||||
switch action {
|
switch action {
|
||||||
case "start":
|
case "start":
|
||||||
return d.cli.ContainerStart(context.Background(), containerID, container.StartOptions{})
|
return d.cli.ContainerStart(context.Background(), containerID, container.StartOptions{})
|
||||||
@@ -162,7 +175,7 @@ func (d *Client) ContainerActions(action string, containerID string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Client) ListContainers() ([]Container, error) {
|
func (d *_client) ListContainers() ([]Container, error) {
|
||||||
containerListOptions := container.ListOptions{
|
containerListOptions := container.ListOptions{
|
||||||
Filters: d.filters,
|
Filters: d.filters,
|
||||||
All: true,
|
All: true,
|
||||||
@@ -191,6 +204,7 @@ func (d *Client) ListContainers() ([]Container, error) {
|
|||||||
Host: d.host.ID,
|
Host: d.host.ID,
|
||||||
Health: findBetweenParentheses(c.Status),
|
Health: findBetweenParentheses(c.Status),
|
||||||
Labels: c.Labels,
|
Labels: c.Labels,
|
||||||
|
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
|
||||||
}
|
}
|
||||||
containers = append(containers, container)
|
containers = append(containers, container)
|
||||||
}
|
}
|
||||||
@@ -202,25 +216,20 @@ func (d *Client) ListContainers() ([]Container, error) {
|
|||||||
return containers, nil
|
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)
|
response, err := d.cli.ContainerStats(ctx, id, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Debugf("starting to stream stats for: %s", id)
|
log.Debugf("starting to stream stats for: %s", id)
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
decoder := json.NewDecoder(response.Body)
|
decoder := json.NewDecoder(response.Body)
|
||||||
var v *types.StatsJSON
|
var v *types.StatsJSON
|
||||||
for {
|
for {
|
||||||
if err := decoder.Decode(&v); err != nil {
|
if err := decoder.Decode(&v); err != nil {
|
||||||
if err == context.Canceled || err == io.EOF {
|
return err
|
||||||
log.Debugf("stopping stats streaming for container %s", id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Errorf("decoder for stats api returned an unknown error %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -248,7 +257,7 @@ func (d *Client) ContainerStats(ctx context.Context, id string, stats chan<- Con
|
|||||||
if cpuPercent > 0 || mem > 0 {
|
if cpuPercent > 0 || mem > 0 {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return nil
|
||||||
case stats <- ContainerStat{
|
case stats <- ContainerStat{
|
||||||
ID: id,
|
ID: id,
|
||||||
CPUPercent: cpuPercent,
|
CPUPercent: cpuPercent,
|
||||||
@@ -258,12 +267,9 @@ func (d *Client) ContainerStats(ctx context.Context, id string, stats chan<- Con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
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")
|
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
|
||||||
|
|
||||||
if since != "" {
|
if since != "" {
|
||||||
@@ -291,7 +297,7 @@ func (d *Client) ContainerLogs(ctx context.Context, id string, since string, std
|
|||||||
return reader, nil
|
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{})
|
dockerMessages, errors := d.cli.Events(ctx, types.EventsOptions{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -319,7 +325,7 @@ func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-c
|
|||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
|
func (d *_client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
|
||||||
options := container.LogsOptions{
|
options := container.LogsOptions{
|
||||||
ShowStdout: stdType&STDOUT != 0,
|
ShowStdout: stdType&STDOUT != 0,
|
||||||
ShowStderr: stdType&STDERR != 0,
|
ShowStderr: stdType&STDERR != 0,
|
||||||
@@ -338,11 +344,11 @@ func (d *Client) ContainerLogsBetweenDates(ctx context.Context, id string, from
|
|||||||
return reader, nil
|
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)
|
return d.cli.Ping(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Client) Host() *Host {
|
func (d *_client) Host() *Host {
|
||||||
return d.host
|
return d.host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string,
|
|||||||
func Test_dockerClient_ListContainers_null(t *testing.T) {
|
func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||||
proxy := new(mockedProxy)
|
proxy := new(mockedProxy)
|
||||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
|
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()
|
list, err := client.ListContainers()
|
||||||
assert.Empty(t, list, "list should be empty")
|
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) {
|
func Test_dockerClient_ListContainers_error(t *testing.T) {
|
||||||
proxy := new(mockedProxy)
|
proxy := new(mockedProxy)
|
||||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
|
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()
|
list, err := client.ListContainers()
|
||||||
assert.Nil(t, list, "list should be nil")
|
assert.Nil(t, list, "list should be nil")
|
||||||
@@ -124,25 +124,15 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
|||||||
|
|
||||||
proxy := new(mockedProxy)
|
proxy := new(mockedProxy)
|
||||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
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()
|
list, err := client.ListContainers()
|
||||||
require.NoError(t, err, "error should not return an error.")
|
require.NoError(t, err, "error should not return an error.")
|
||||||
|
|
||||||
assert.Equal(t, list, []Container{
|
Ids := []string{"1234567890_a", "abcdefghijkl"}
|
||||||
{
|
for i, container := range list {
|
||||||
ID: "1234567890_a",
|
assert.Equal(t, container.ID, Ids[i])
|
||||||
Name: "a_test_container",
|
}
|
||||||
Names: []string{"/a_test_container"},
|
|
||||||
Host: "localhost",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "abcdefghijkl",
|
|
||||||
Name: "z_test_container",
|
|
||||||
Names: []string{"/z_test_container"},
|
|
||||||
Host: "localhost",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
proxy.AssertExpectations(t)
|
proxy.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
@@ -161,7 +151,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
|||||||
options := container.LogsOptions{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)
|
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)
|
logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
|
||||||
|
|
||||||
actual, _ := io.ReadAll(logReader)
|
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"))
|
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)
|
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}}
|
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
|
||||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
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")
|
container, err := client.FindContainer("abcdefghijkl")
|
||||||
require.NoError(t, err, "error should not be thrown")
|
require.NoError(t, err, "error should not be thrown")
|
||||||
|
|
||||||
assert.Equal(t, container, Container{
|
assert.Equal(t, container.ID, "abcdefghijkl")
|
||||||
ID: "abcdefghijkl",
|
|
||||||
Name: "z_test_container",
|
|
||||||
Names: []string{"/z_test_container"},
|
|
||||||
Host: "localhost",
|
|
||||||
Tty: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
proxy.AssertExpectations(t)
|
proxy.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
@@ -231,7 +215,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
|
|||||||
|
|
||||||
proxy := new(mockedProxy)
|
proxy := new(mockedProxy)
|
||||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
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")
|
_, err := client.FindContainer("not_valid")
|
||||||
require.Error(t, err, "error should be thrown")
|
require.Error(t, err, "error should be thrown")
|
||||||
@@ -252,7 +236,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxy := new(mockedProxy)
|
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}}
|
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
|
||||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, 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")
|
container, err := client.FindContainer("abcdefghijkl")
|
||||||
require.NoError(t, err, "error should not be thrown")
|
require.NoError(t, err, "error should not be thrown")
|
||||||
|
|
||||||
assert.Equal(t, container, Container{
|
assert.Equal(t, container.ID, "abcdefghijkl")
|
||||||
ID: "abcdefghijkl",
|
|
||||||
Name: "z_test_container",
|
|
||||||
Names: []string{"/z_test_container"},
|
|
||||||
Host: "localhost",
|
|
||||||
Tty: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
actions := []string{"start", "stop", "restart"}
|
actions := []string{"start", "stop", "restart"}
|
||||||
for _, action := range actions {
|
for _, action := range actions {
|
||||||
@@ -294,7 +272,7 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxy := new(mockedProxy)
|
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("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||||
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||||
|
|||||||
121
internal/docker/container_store.go
Normal file
121
internal/docker/container_store.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerStore struct {
|
||||||
|
containers map[string]*Container
|
||||||
|
client Client
|
||||||
|
statsCollector *StatsCollector
|
||||||
|
subscribers []chan ContainerEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContainerStore(client Client) *ContainerStore {
|
||||||
|
s := &ContainerStore{
|
||||||
|
containers: make(map[string]*Container),
|
||||||
|
client: client,
|
||||||
|
statsCollector: NewStatsCollector(client),
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.init(context.Background())
|
||||||
|
go s.statsCollector.StartCollecting(context.Background())
|
||||||
|
|
||||||
|
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(events chan ContainerEvent) {
|
||||||
|
s.subscribers = append(s.subscribers, events)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContainerStore) Unsubscribe(toRemove chan ContainerEvent) {
|
||||||
|
for i, sub := range s.subscribers {
|
||||||
|
if sub == toRemove {
|
||||||
|
s.subscribers = append(s.subscribers[:i], s.subscribers[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContainerStore) SubscribeStats(stats chan ContainerStat) {
|
||||||
|
s.statsCollector.Subscribe(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContainerStore) UnsubscribeStats(toRemove chan ContainerStat) {
|
||||||
|
s.statsCollector.Unsubscribe(toRemove)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContainerStore) init(ctx context.Context) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
events := make(chan ContainerEvent)
|
||||||
|
s.client.Events(ctx, events)
|
||||||
|
|
||||||
|
stats := make(chan ContainerStat)
|
||||||
|
s.statsCollector.Subscribe(stats)
|
||||||
|
defer s.statsCollector.Unsubscribe(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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range s.subscribers {
|
||||||
|
sub <- event
|
||||||
|
}
|
||||||
|
case stat := <-stats:
|
||||||
|
if container, ok := s.containers[stat.ID]; ok {
|
||||||
|
container.Stats.Push(stat)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
internal/docker/stats_collector.go
Normal file
92
internal/docker/stats_collector.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatsCollector struct {
|
||||||
|
stream chan ContainerStat
|
||||||
|
subscribers []chan ContainerStat
|
||||||
|
client Client
|
||||||
|
cancelers map[string]context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStatsCollector(client Client) *StatsCollector {
|
||||||
|
return &StatsCollector{
|
||||||
|
stream: make(chan ContainerStat),
|
||||||
|
subscribers: []chan ContainerStat{},
|
||||||
|
client: client,
|
||||||
|
cancelers: make(map[string]context.CancelFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatsCollector) Subscribe(stats chan ContainerStat) {
|
||||||
|
c.subscribers = append(c.subscribers, stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatsCollector) Unsubscribe(subscriber chan ContainerStat) {
|
||||||
|
for i, s := range c.subscribers {
|
||||||
|
if s == subscriber {
|
||||||
|
c.subscribers = append(c.subscribers[:i], c.subscribers[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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[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[event.ActorID]; ok {
|
||||||
|
cancel()
|
||||||
|
delete(sc.cancelers, event.ActorID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case stat := <-sc.stream:
|
||||||
|
for _, subscriber := range sc.subscribers {
|
||||||
|
subscriber <- stat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package docker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
|
"github.com/amir20/dozzle/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Container represents an internal representation of docker containers
|
// Container represents an internal representation of docker containers
|
||||||
@@ -19,6 +21,7 @@ type Container struct {
|
|||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
Tty bool `json:"-"`
|
Tty bool `json:"-"`
|
||||||
Labels map[string]string `json:"labels,omitempty"`
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
|
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerStat represent stats instant for a container
|
// ContainerStat represent stats instant for a container
|
||||||
|
|||||||
45
internal/utils/ring_buffer.go
Normal file
45
internal/utils/ring_buffer.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type RingBuffer[T any] struct {
|
||||||
|
Size int
|
||||||
|
data []T
|
||||||
|
start int
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 {
|
||||||
|
if len(r.data) == r.Size {
|
||||||
|
return append(r.data[r.start:], r.data[:r.start]...)
|
||||||
|
} else {
|
||||||
|
return r.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RingBuffer[T]) Len() int {
|
||||||
|
return len(r.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RingBuffer[T]) Full() bool {
|
||||||
|
return len(r.data) == r.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RingBuffer[T]) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(r.Data())
|
||||||
|
}
|
||||||
39
internal/utils/ring_buffer_test.go
Normal file
39
internal/utils/ring_buffer_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRingBuffer(t *testing.T) {
|
||||||
|
rb := NewRingBuffer[int](3)
|
||||||
|
|
||||||
|
if rb.Len() != 0 {
|
||||||
|
t.Errorf("Expected length to be 0, got %d", rb.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
rb.Push(1)
|
||||||
|
rb.Push(2)
|
||||||
|
rb.Push(3)
|
||||||
|
|
||||||
|
if rb.Len() != 3 {
|
||||||
|
t.Errorf("Expected length to be 3, got %d", rb.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rb.Full() {
|
||||||
|
t.Errorf("Expected buffer to be full")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
event: containers-changed
|
||||||
data: []
|
data: [{"id":"1234","names":null,"name":"test","image":"test","imageId":"","command":"","created":0,"state":"","status":"","stats":[]}]
|
||||||
|
|
||||||
|
|
||||||
event: container-start
|
event: container-start
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/amir20/dozzle/internal/analytics"
|
"github.com/amir20/dozzle/internal/analytics"
|
||||||
"github.com/amir20/dozzle/internal/docker"
|
"github.com/amir20/dozzle/internal/docker"
|
||||||
@@ -29,9 +26,6 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
events := make(chan docker.ContainerEvent)
|
|
||||||
stats := make(chan docker.ContainerStat)
|
|
||||||
|
|
||||||
b := analytics.BeaconEvent{
|
b := analytics.BeaconEvent{
|
||||||
Name: "events",
|
Name: "events",
|
||||||
Version: h.config.Version,
|
Version: h.config.Version,
|
||||||
@@ -44,47 +38,28 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
HasActions: h.config.EnableActions,
|
HasActions: h.config.EnableActions,
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
allContainers := make([]docker.Container, 0)
|
||||||
wg := sync.WaitGroup{}
|
events := make(chan docker.ContainerEvent)
|
||||||
wg.Add(len(h.clients))
|
stats := make(chan docker.ContainerStat)
|
||||||
results := make(chan []docker.Container, len(h.clients))
|
|
||||||
|
|
||||||
for _, client := range h.clients {
|
for _, store := range h.stores {
|
||||||
client.Events(ctx, events)
|
allContainers = append(allContainers, store.List()...)
|
||||||
|
store.SubscribeStats(stats)
|
||||||
|
store.Subscribe(events)
|
||||||
|
}
|
||||||
|
|
||||||
go func(client DockerClient) {
|
defer func() {
|
||||||
defer wg.Done()
|
for _, store := range h.stores {
|
||||||
if containers, err := client.ListContainers(); err == nil {
|
store.UnsubscribeStats(stats)
|
||||||
results <- containers
|
store.Unsubscribe(events)
|
||||||
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 {
|
if err := sendContainersJSON(allContainers, w); err != nil {
|
||||||
log.Errorf("error writing containers to event stream: %v", err)
|
log.Errorf("error writing containers to event stream: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.RunningContainers = len(allContainers)
|
b.RunningContainers = len(allContainers)
|
||||||
f.Flush()
|
f.Flush()
|
||||||
}
|
|
||||||
|
|
||||||
if !h.config.NoAnalytics {
|
if !h.config.NoAnalytics {
|
||||||
go func() {
|
go func() {
|
||||||
@@ -109,17 +84,9 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
switch event.Name {
|
switch event.Name {
|
||||||
case "start", "die":
|
case "start", "die":
|
||||||
log.Debugf("triggering docker event: %v", event.Name)
|
|
||||||
if event.Name == "start" {
|
if event.Name == "start" {
|
||||||
log.Debugf("found new container with id: %v", event.ActorID)
|
log.Debugf("found new container with id: %v", event.ActorID)
|
||||||
|
containers := h.stores[event.Host].List()
|
||||||
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)
|
|
||||||
}
|
|
||||||
if err := sendContainersJSON(containers, w); err != nil {
|
if err := sendContainersJSON(containers, w); err != nil {
|
||||||
log.Errorf("error encoding containers to stream: %v", err)
|
log.Errorf("error encoding containers to stream: %v", err)
|
||||||
return
|
return
|
||||||
@@ -150,11 +117,9 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.Flush()
|
f.Flush()
|
||||||
default:
|
|
||||||
log.Tracef("ignoring docker event: %v", event.Name)
|
|
||||||
// do nothing
|
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
log.Debugf("context done, closing event stream")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/amir20/dozzle/internal/docker"
|
"github.com/amir20/dozzle/internal/docker"
|
||||||
|
"github.com/amir20/dozzle/internal/utils"
|
||||||
"github.com/beme/abide"
|
"github.com/beme/abide"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -35,36 +37,26 @@ func Test_handler_streamEvents_happy(t *testing.T) {
|
|||||||
ActorID: "1234",
|
ActorID: "1234",
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
}
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
cancel()
|
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)
|
clients := map[string]docker.Client{
|
||||||
rr := httptest.NewRecorder()
|
"localhost": mockedClient,
|
||||||
handler.ServeHTTP(rr, req)
|
}
|
||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
|
||||||
mockedClient.AssertExpectations(t)
|
// This is needed so that the server is initialized for store
|
||||||
}
|
server := CreateServer(clients, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
|
||||||
|
handler := server.Handler
|
||||||
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()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||||
mockedClient.AssertExpectations(t)
|
mockedClient.AssertExpectations(t)
|
||||||
@@ -4,12 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/amir20/dozzle/internal/docker"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Trace("Executing healthcheck request")
|
log.Trace("Executing healthcheck request")
|
||||||
var client DockerClient
|
var client docker.Client
|
||||||
for _, v := range h.clients {
|
for _, v := range h.clients {
|
||||||
client = v
|
client = v
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"time"
|
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/amir20/dozzle/internal/auth"
|
"github.com/amir20/dozzle/internal/auth"
|
||||||
"github.com/amir20/dozzle/internal/docker"
|
"github.com/amir20/dozzle/internal/docker"
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -47,30 +44,25 @@ type Authorizer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
clients map[string]DockerClient
|
clients map[string]docker.Client
|
||||||
|
stores map[string]*docker.ContainerStore
|
||||||
content fs.FS
|
content fs.FS
|
||||||
config *Config
|
config *Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client is a proxy around the docker client
|
func CreateServer(clients map[string]docker.Client, content fs.FS, config Config) *http.Server {
|
||||||
type DockerClient interface {
|
stores := make(map[string]*docker.ContainerStore)
|
||||||
ListContainers() ([]docker.Container, error)
|
for host, client := range clients {
|
||||||
FindContainer(string) (docker.Container, error)
|
stores[host] = docker.NewContainerStore(client)
|
||||||
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]DockerClient, content fs.FS, config Config) *http.Server {
|
|
||||||
handler := &handler{
|
handler := &handler{
|
||||||
clients: clients,
|
clients: clients,
|
||||||
content: content,
|
content: content,
|
||||||
config: &config,
|
config: &config,
|
||||||
|
stores: stores,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Server{Addr: config.Addr, Handler: createRouter(handler)}
|
return &http.Server{Addr: config.Addr, Handler: createRouter(handler)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +127,7 @@ func createRouter(h *handler) *chi.Mux {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) clientFromRequest(r *http.Request) DockerClient {
|
func (h *handler) clientFromRequest(r *http.Request) docker.Client {
|
||||||
host := chi.URLParam(r, "host")
|
host := chi.URLParam(r, "host")
|
||||||
|
|
||||||
if host == "" {
|
if host == "" {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
type MockedClient struct {
|
type MockedClient struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
DockerClient
|
docker.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockedClient) FindContainer(id string) (docker.Container, error) {
|
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)
|
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 {
|
if client == nil {
|
||||||
client = new(MockedClient)
|
client = new(MockedClient)
|
||||||
client.(*MockedClient).On("ListContainers").Return([]docker.Container{}, nil)
|
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)
|
content = afero.NewIOFS(fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
clients := map[string]DockerClient{
|
clients := map[string]docker.Client{
|
||||||
"localhost": client,
|
"localhost": client,
|
||||||
}
|
}
|
||||||
return createRouter(&handler{
|
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}})
|
return createHandler(client, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
|
||||||
}
|
}
|
||||||
|
|||||||
19
main.go
19
main.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
|
"errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -120,12 +121,12 @@ func doStartEvent(arg args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createClients(args args,
|
func createClients(args args,
|
||||||
localClientFactory func(map[string][]string) (*docker.Client, error),
|
localClientFactory func(map[string][]string) (docker.Client, error),
|
||||||
remoteClientFactory func(map[string][]string, docker.Host) (*docker.Client, error),
|
remoteClientFactory func(map[string][]string, docker.Host) (docker.Client, error),
|
||||||
hostname string) map[string]web.DockerClient {
|
hostname string) map[string]docker.Client {
|
||||||
clients := make(map[string]web.DockerClient)
|
clients := make(map[string]docker.Client)
|
||||||
|
|
||||||
if localClient := createLocalClient(args, localClientFactory); localClient != nil {
|
if localClient, err := createLocalClient(args, localClientFactory); err == nil {
|
||||||
if hostname != "" {
|
if hostname != "" {
|
||||||
localClient.Host().Name = hostname
|
localClient.Host().Name = hostname
|
||||||
}
|
}
|
||||||
@@ -154,7 +155,7 @@ func createClients(args args,
|
|||||||
return clients
|
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")
|
_, dev := os.LookupEnv("DEV")
|
||||||
|
|
||||||
var provider web.AuthProvider = web.NONE
|
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)
|
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++ {
|
for i := 1; ; i++ {
|
||||||
dockerClient, err := localClientFactory(args.Filter)
|
dockerClient, err := localClientFactory(args.Filter)
|
||||||
if err == nil {
|
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)
|
log.Debugf("Could not connect to local Docker Engine: %s", err)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Connected to local Docker Engine")
|
log.Debugf("Connected to local Docker Engine")
|
||||||
return dockerClient
|
return dockerClient, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if args.WaitForDockerSeconds > 0 {
|
if args.WaitForDockerSeconds > 0 {
|
||||||
@@ -242,7 +243,7 @@ func createLocalClient(args args, localClientFactory func(map[string][]string) (
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil, errors.New("could not connect to local Docker Engine")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseArgs() args {
|
func parseArgs() args {
|
||||||
|
|||||||
20
main_test.go
20
main_test.go
@@ -26,7 +26,7 @@ func (f *fakeCLI) ContainerList(context.Context, container.ListOptions) ([]types
|
|||||||
func Test_valid_localhost(t *testing.T) {
|
func Test_valid_localhost(t *testing.T) {
|
||||||
client := new(fakeCLI)
|
client := new(fakeCLI)
|
||||||
client.On("ContainerList").Return([]types.Container{}, nil)
|
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{
|
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
|
||||||
ID: "localhost",
|
ID: "localhost",
|
||||||
}), nil
|
}), nil
|
||||||
@@ -34,7 +34,7 @@ func Test_valid_localhost(t *testing.T) {
|
|||||||
|
|
||||||
args := args{}
|
args := args{}
|
||||||
|
|
||||||
actualClient := createLocalClient(args, fakeClientFactory)
|
actualClient, _ := createLocalClient(args, fakeClientFactory)
|
||||||
|
|
||||||
assert.NotNil(t, actualClient)
|
assert.NotNil(t, actualClient)
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
@@ -43,7 +43,7 @@ func Test_valid_localhost(t *testing.T) {
|
|||||||
func Test_invalid_localhost(t *testing.T) {
|
func Test_invalid_localhost(t *testing.T) {
|
||||||
client := new(fakeCLI)
|
client := new(fakeCLI)
|
||||||
client.On("ContainerList").Return([]types.Container{}, errors.New("error"))
|
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{
|
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
|
||||||
ID: "localhost",
|
ID: "localhost",
|
||||||
}), nil
|
}), nil
|
||||||
@@ -51,7 +51,7 @@ func Test_invalid_localhost(t *testing.T) {
|
|||||||
|
|
||||||
args := args{}
|
args := args{}
|
||||||
|
|
||||||
actualClient := createLocalClient(args, fakeClientFactory)
|
actualClient, _ := createLocalClient(args, fakeClientFactory)
|
||||||
|
|
||||||
assert.Nil(t, actualClient)
|
assert.Nil(t, actualClient)
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
@@ -60,7 +60,7 @@ func Test_invalid_localhost(t *testing.T) {
|
|||||||
func Test_valid_remote(t *testing.T) {
|
func Test_valid_remote(t *testing.T) {
|
||||||
local := new(fakeCLI)
|
local := new(fakeCLI)
|
||||||
local.On("ContainerList").Return([]types.Container{}, errors.New("error"))
|
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{
|
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
|
||||||
ID: "localhost",
|
ID: "localhost",
|
||||||
}), nil
|
}), nil
|
||||||
@@ -68,7 +68,7 @@ func Test_valid_remote(t *testing.T) {
|
|||||||
|
|
||||||
remote := new(fakeCLI)
|
remote := new(fakeCLI)
|
||||||
remote.On("ContainerList").Return([]types.Container{}, nil)
|
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{
|
return docker.NewClient(remote, filters.NewArgs(), &docker.Host{
|
||||||
ID: "test",
|
ID: "test",
|
||||||
}), nil
|
}), nil
|
||||||
@@ -90,7 +90,7 @@ func Test_valid_remote(t *testing.T) {
|
|||||||
func Test_valid_remote_and_local(t *testing.T) {
|
func Test_valid_remote_and_local(t *testing.T) {
|
||||||
local := new(fakeCLI)
|
local := new(fakeCLI)
|
||||||
local.On("ContainerList").Return([]types.Container{}, nil)
|
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{
|
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
|
||||||
ID: "localhost",
|
ID: "localhost",
|
||||||
}), nil
|
}), nil
|
||||||
@@ -98,7 +98,7 @@ func Test_valid_remote_and_local(t *testing.T) {
|
|||||||
|
|
||||||
remote := new(fakeCLI)
|
remote := new(fakeCLI)
|
||||||
remote.On("ContainerList").Return([]types.Container{}, nil)
|
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{
|
return docker.NewClient(remote, filters.NewArgs(), &docker.Host{
|
||||||
ID: "test",
|
ID: "test",
|
||||||
}), nil
|
}), nil
|
||||||
@@ -119,13 +119,13 @@ func Test_valid_remote_and_local(t *testing.T) {
|
|||||||
func Test_no_clients(t *testing.T) {
|
func Test_no_clients(t *testing.T) {
|
||||||
local := new(fakeCLI)
|
local := new(fakeCLI)
|
||||||
local.On("ContainerList").Return([]types.Container{}, errors.New("error"))
|
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{
|
return docker.NewClient(local, filters.NewArgs(), &docker.Host{
|
||||||
ID: "localhost",
|
ID: "localhost",
|
||||||
}), nil
|
}), 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)
|
client := new(fakeCLI)
|
||||||
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
|
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
|
||||||
ID: "test",
|
ID: "test",
|
||||||
|
|||||||
Reference in New Issue
Block a user