1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 13:23:07 +01:00

feat: adds ability to support labels with | delimeter (#2276)

* feat: adds ability to support labels with | delimeter

* fixes tests

* updates docs
This commit is contained in:
Amir Raminfar
2023-06-28 12:14:17 -07:00
committed by GitHub
parent f6807db592
commit 70acaa64d8
15 changed files with 203 additions and 95 deletions

View File

@@ -1 +1 @@
-r '\.(go)$' -R 'node_modules' -G '*_test.go' -s -- go run main.go --level debug --remote-host tcp://64.225.88.189:2376 -r '\.(go)$' -R 'node_modules' -G '*_test.go' -s -- go run main.go --level debug --remote-host tcp://64.225.88.189:2376|clashleaders.com

View File

@@ -3,7 +3,7 @@
<nav class="breadcrumb menu-label" aria-label="breadcrumbs"> <nav class="breadcrumb menu-label" aria-label="breadcrumbs">
<ul v-if="sessionHost"> <ul v-if="sessionHost">
<li> <li>
<a href="#" @click.prevent="setHost(null)">{{ sessionHost }}</a> <a href="#" @click.prevent="setHost(null)">{{ hosts[sessionHost].name }}</a>
</li> </li>
<li class="is-active"> <li class="is-active">
<a href="#" aria-current="page">{{ $t("label.containers") }}</a> <a href="#" aria-current="page">{{ $t("label.containers") }}</a>
@@ -16,7 +16,7 @@
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in"> <transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
<ul class="menu-list" v-if="!sessionHost"> <ul class="menu-list" v-if="!sessionHost">
<li v-for="host in config.hosts"> <li v-for="host in config.hosts">
<a @click.prevent="setHost(host)">{{ host }}</a> <a @click.prevent="setHost(host.host)">{{ host.name }}</a>
</li> </li>
</ul> </ul>
<ul class="menu-list" v-else> <ul class="menu-list" v-else>
@@ -85,6 +85,13 @@ const sortedContainers = computed(() =>
}) })
); );
const hosts = computed(() =>
config.hosts.reduce((acc, item) => {
acc[item.host] = item;
return acc;
}, {} as Record<string, { name: string; host: string }>)
);
const activeContainersById = computed(() => const activeContainersById = computed(() =>
activeContainers.value.reduce((acc, item) => { activeContainers.value.reduce((acc, item) => {
acc[item.id] = item; acc[item.id] = item;
@@ -101,7 +108,8 @@ const activeContainersById = computed(() =>
opacity: 0.5; opacity: 0.5;
} }
li.exited a, li.dead a { li.exited a,
li.dead a {
color: #777; color: #777;
} }

View File

@@ -26,15 +26,15 @@
<o-dropdown v-model="sessionHost" aria-role="list"> <o-dropdown v-model="sessionHost" aria-role="list">
<template #trigger> <template #trigger>
<o-button variant="primary" type="button" size="small"> <o-button variant="primary" type="button" size="small">
<span>{{ sessionHost }}</span> <span>{{ sessionHost ? hosts[sessionHost].name : "" }}</span>
<span class="icon"> <span class="icon">
<carbon:caret-down /> <carbon:caret-down />
</span> </span>
</o-button> </o-button>
</template> </template>
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in config.hosts" :key="value"> <o-dropdown-item :value="value.host" aria-role="listitem" v-for="value in config.hosts" :key="value">
<span>{{ value }}</span> <span>{{ value.name }}</span>
</o-dropdown-item> </o-dropdown-item>
</o-dropdown> </o-dropdown>
</div> </div>
@@ -111,6 +111,13 @@ const sortedContainers = computed(() =>
} }
}) })
); );
const hosts = computed(() =>
config.hosts.reduce((acc, item) => {
acc[item.host] = item;
return acc;
}, {} as Record<string, { name: string; host: string }>)
);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
aside { aside {

View File

@@ -3,7 +3,7 @@ import { Container } from "@/models/Container";
const sessionHost = useSessionStorage<string | null>("host", null); const sessionHost = useSessionStorage<string | null>("host", null);
if (config.hosts.length === 1 && !sessionHost.value) { if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0]; sessionHost.value = config.hosts[0].host;
} }
function persistentVisibleKeys(container: ComputedRef<Container>) { function persistentVisibleKeys(container: ComputedRef<Container>) {

View File

@@ -7,7 +7,7 @@ interface Config {
secured: boolean; secured: boolean;
maxLogs: number; maxLogs: number;
hostname: string; hostname: string;
hosts: string[]; hosts: { name: string; host: string }[];
} }
const pageConfig = JSON.parse(text); const pageConfig = JSON.parse(text);

View File

@@ -5,8 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/url"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
@@ -25,7 +23,7 @@ import (
type dockerClient struct { type dockerClient struct {
cli dockerProxy cli dockerProxy
filters filters.Args filters filters.Args
host string host *Host
} }
type StdType int type StdType int
@@ -69,7 +67,7 @@ type Client interface {
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error) ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error ContainerStats(context.Context, string, chan<- ContainerStat) error
Ping(context.Context) (types.Ping, error) Ping(context.Context) (types.Ping, error)
Host() string Host() *Host
} }
// NewClientWithFilters creates a new instance of Client with docker filters // NewClientWithFilters creates a new instance of Client with docker filters
@@ -89,10 +87,10 @@ func NewClientWithFilters(f map[string][]string) (Client, error) {
return nil, err return nil, err
} }
return &dockerClient{cli, filterArgs, "localhost"}, nil return &dockerClient{cli, filterArgs, &Host{Name: "localhost", Host: "localhost"}}, nil
} }
func NewClientWithTlsAndFilter(f map[string][]string, connection string) (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 {
@@ -102,38 +100,19 @@ func NewClientWithTlsAndFilter(f map[string][]string, connection string) (Client
log.Debugf("filterArgs = %v", filterArgs) log.Debugf("filterArgs = %v", filterArgs)
remoteUrl, err := url.Parse(connection) if host.URL.Scheme != "tcp" {
if err != nil {
return nil, err
}
if remoteUrl.Scheme != "tcp" {
log.Fatal("Only tcp scheme is supported") log.Fatal("Only tcp scheme is supported")
} }
host := remoteUrl.Hostname()
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)
}
cacertPath := filepath.Join(basePath, "ca.pem")
certPath := filepath.Join(basePath, "cert.pem")
keyPath := filepath.Join(basePath, "key.pem")
opts := []client.Opt{ opts := []client.Opt{
client.WithHost(connection), client.WithHost(host.URL.String()),
} }
if _, err := os.Stat(cacertPath); os.IsNotExist(err) { if host.ValidCerts {
log.Debugf("%s does not exist, using plain HTTP", cacertPath) log.Debugf("Using TLS client config with certs at: %s", filepath.Dir(host.CertPath))
opts = append(opts, client.WithTLSClientConfig(host.CACertPath, host.CertPath, host.KeyPath))
} else { } else {
log.Debugf("Using TLS client config with certs at: %s", basePath) log.Debugf("No valid certs found, using plain TCP")
opts = append(opts, client.WithTLSClientConfig(cacertPath, certPath, keyPath))
} }
opts = append(opts, client.WithAPIVersionNegotiation()) opts = append(opts, client.WithAPIVersionNegotiation())
@@ -144,7 +123,7 @@ func NewClientWithTlsAndFilter(f map[string][]string, connection string) (Client
return nil, err return nil, err
} }
return &dockerClient{cli, filterArgs, host}, nil return &dockerClient{cli, filterArgs, &host}, nil
} }
func (d *dockerClient) FindContainer(id string) (Container, error) { func (d *dockerClient) FindContainer(id string) (Container, error) {
@@ -191,7 +170,7 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
Created: c.Created, Created: c.Created,
State: c.State, State: c.State,
Status: c.Status, Status: c.Status,
Host: d.host, Host: d.host.Host,
Health: findBetweenParentheses(c.Status), Health: findBetweenParentheses(c.Status),
} }
containers = append(containers, container) containers = append(containers, container)
@@ -308,7 +287,7 @@ func (d *dockerClient) Events(ctx context.Context, messages chan<- ContainerEven
messages <- ContainerEvent{ messages <- ContainerEvent{
ActorID: message.Actor.ID[:12], ActorID: message.Actor.ID[:12],
Name: message.Action, Name: message.Action,
Host: d.host, Host: d.host.Host,
} }
} }
} }
@@ -371,7 +350,7 @@ func (d *dockerClient) Ping(ctx context.Context) (types.Ping, error) {
return d.cli.Ping(ctx) return d.cli.Ping(ctx)
} }
func (d *dockerClient) Host() string { func (d *dockerClient) Host() *Host {
return d.host return d.host
} }

View File

@@ -54,7 +54,7 @@ func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, st
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 := &dockerClient{proxy, filters.NewArgs(), "localhost"} client := &dockerClient{proxy, filters.NewArgs(), &Host{Host: "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")
@@ -66,7 +66,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 := &dockerClient{proxy, filters.NewArgs(), "localhost"} client := &dockerClient{proxy, filters.NewArgs(), &Host{Host: "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")
@@ -89,7 +89,7 @@ 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 := &dockerClient{proxy, filters.NewArgs(), "localhost"} client := &dockerClient{proxy, filters.NewArgs(), &Host{Host: "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.")
@@ -129,7 +129,7 @@ func Test_dockerClient_ContainerLogs_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, id).Return(json, nil) proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs(), "localhost"} client := &dockerClient{proxy, filters.NewArgs(), &Host{Host: "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)
@@ -150,7 +150,7 @@ func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
json := types.ContainerJSON{Config: &container.Config{Tty: true}} json := types.ContainerJSON{Config: &container.Config{Tty: true}}
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil) proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs(), "localhost"} client := &dockerClient{proxy, filters.NewArgs(), &Host{Host: "localhost"}}
logReader, _ := client.ContainerLogs(context.Background(), id, "", STDALL) logReader, _ := client.ContainerLogs(context.Background(), id, "", STDALL)
actual, _ := io.ReadAll(logReader) actual, _ := io.ReadAll(logReader)
@@ -165,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 := &dockerClient{proxy, filters.NewArgs(), "localhost"} client := &dockerClient{proxy, filters.NewArgs(), &Host{Host: "localhost"}}
reader, err := client.ContainerLogs(context.Background(), id, "", STDALL) reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
@@ -188,7 +188,7 @@ func Test_dockerClient_FindContainer_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 := &dockerClient{proxy, filters.NewArgs(), "localhost"} client := &dockerClient{proxy, filters.NewArgs(), &Host{Host: "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")
@@ -216,7 +216,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 := &dockerClient{proxy, filters.NewArgs(), "localhost"} client := &dockerClient{proxy, filters.NewArgs(), &Host{Host: "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")

68
docker/host.go Normal file
View File

@@ -0,0 +1,68 @@
package docker
import (
"fmt"
"log"
"net/url"
"os"
"path/filepath"
"strings"
)
type Host struct {
Name string `json:"name"`
Host string `json:"host"`
URL *url.URL `json:"-"`
CertPath string `json:"-"`
CACertPath string `json:"-"`
KeyPath string `json:"-"`
ValidCerts bool `json:"-"`
}
func ParseConnection(connection string) (Host, error) {
parts := strings.Split(connection, "|")
if len(parts) > 2 {
return Host{}, fmt.Errorf("invalid connection string: %s", connection)
}
remoteUrl, err := url.Parse(parts[0])
if err != nil {
return Host{}, err
}
name := remoteUrl.Hostname()
if len(parts) == 2 {
name = parts[1]
}
basePath, err := filepath.Abs("./certs")
if err != nil {
log.Fatalf("error converting certs path to absolute: %s", err)
}
host := remoteUrl.Hostname()
if _, err := os.Stat(filepath.Join(basePath, host)); !os.IsNotExist(err) {
basePath = filepath.Join(basePath, host)
}
cacertPath := filepath.Join(basePath, "ca.pem")
certPath := filepath.Join(basePath, "cert.pem")
keyPath := filepath.Join(basePath, "key.pem")
hasCerts := true
if _, err := os.Stat(cacertPath); os.IsNotExist(err) {
cacertPath = ""
hasCerts = false
}
return Host{
Name: name,
Host: host,
URL: remoteUrl,
CertPath: certPath,
CACertPath: cacertPath,
KeyPath: keyPath,
ValidCerts: hasCerts,
}, nil
}

View File

@@ -3,11 +3,9 @@
// @ts-nocheck // @ts-nocheck
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {} export {}
declare module '@vue/runtime-core' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
BrowserWindow: typeof import('./.vitepress/theme/components/BrowserWindow.vue')['default'] BrowserWindow: typeof import('./.vitepress/theme/components/BrowserWindow.vue')['default']
HeroVideo: typeof import('./.vitepress/theme/components/HeroVideo.vue')['default'] HeroVideo: typeof import('./.vitepress/theme/components/HeroVideo.vue')['default']

View File

@@ -53,3 +53,29 @@ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir2
::: warning ::: warning
Exposing `docker.sock` publicly is not safe. Only use a proxy for an internal network where all clients are trusted. Exposing `docker.sock` publicly is not safe. Only use a proxy for an internal network where all clients are trusted.
::: :::
## Adding labels to hosts
`--remote-host` supports host labels by appending them to the connection string with `|`. For example, `--remote-host tcp://123.1.1.1:2375|foobar.com` will use foobar.com as the label in the UI. A full example of this using the CLI or compose are:
::: code-group
```sh
docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --remote-host tcp://123.1.1.1:2375|foobar.com
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /path/to/certs:/certs
ports:
- 8080:8080
environment:
DOZZLE_REMOTE_HOST: tcp://167.99.1.1:2376|foo.com,tcp://167.99.1.2:2376|bar.com
```
:::

19
main.go
View File

@@ -137,24 +137,29 @@ func doStartEvent(arg args) {
} }
} }
func createClients(args args, localClientFactory func(map[string][]string) (docker.Client, error), remoteClientFactory func(map[string][]string, string) (docker.Client, error)) map[string]docker.Client { func createClients(args args, localClientFactory func(map[string][]string) (docker.Client, error), remoteClientFactory func(map[string][]string, docker.Host) (docker.Client, error)) map[string]docker.Client {
clients := make(map[string]docker.Client) clients := make(map[string]docker.Client)
if localClient := createLocalClient(args, localClientFactory); localClient != nil { if localClient := createLocalClient(args, localClientFactory); localClient != nil {
clients[localClient.Host()] = localClient clients[localClient.Host().Host] = localClient
} }
for _, host := range args.RemoteHost { for _, remoteHost := range args.RemoteHost {
log.Infof("Creating client for %s", host) host, err := docker.ParseConnection(remoteHost)
if err != nil {
log.Fatalf("Could not parse remote host %s: %s", remoteHost, err)
}
log.Debugf("Creating remote client for %s with %+v", host.Name, host)
log.Infof("Creating client for %s with %s", host.Name, host.URL.String())
if client, err := remoteClientFactory(args.Filter, host); err == nil { if client, err := remoteClientFactory(args.Filter, host); err == nil {
if _, err := client.ListContainers(); err == nil { if _, err := client.ListContainers(); err == nil {
log.Debugf("Connected to local Docker Engine") log.Debugf("Connected to local Docker Engine")
clients[client.Host()] = client clients[client.Host().Host] = client
} else { } else {
log.Warnf("Could not connect to remote host %s: %s", host, err) log.Warnf("Could not connect to remote host %s: %s", host.Host, err)
} }
} else { } else {
log.Warnf("Could not create client for %s: %s", host, err) log.Warnf("Could not create client for %s: %s", host.Host, err)
} }
} }

View File

@@ -19,16 +19,18 @@ func (f *fakeClient) ListContainers() ([]docker.Container, error) {
return args.Get(0).([]docker.Container), args.Error(1) return args.Get(0).([]docker.Container), args.Error(1)
} }
func (f *fakeClient) Host() string { func (f *fakeClient) Host() *docker.Host {
args := f.Called() args := f.Called()
return args.String(0) return args.Get(0).(*docker.Host)
} }
func Test_valid_localhost(t *testing.T) { func Test_valid_localhost(t *testing.T) {
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) { fakeClientFactory := func(filter map[string][]string) (docker.Client, error) {
client := new(fakeClient) client := new(fakeClient)
client.On("ListContainers").Return([]docker.Container{}, nil) client.On("ListContainers").Return([]docker.Container{}, nil)
client.On("Host").Return("localhost") client.On("Host").Return(&docker.Host{
Host: "localhost",
})
return client, nil return client, nil
} }
@@ -43,7 +45,9 @@ func Test_invalid_localhost(t *testing.T) {
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) { fakeClientFactory := func(filter map[string][]string) (docker.Client, error) {
client := new(fakeClient) client := new(fakeClient)
client.On("ListContainers").Return([]docker.Container{}, errors.New("error")) client.On("ListContainers").Return([]docker.Container{}, errors.New("error"))
client.On("Host").Return("localhost") client.On("Host").Return(&docker.Host{
Host: "localhost",
})
return client, nil return client, nil
} }
@@ -58,15 +62,19 @@ func Test_valid_remote(t *testing.T) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
client := new(fakeClient) client := new(fakeClient)
client.On("ListContainers").Return([]docker.Container{}, errors.New("error")) client.On("ListContainers").Return([]docker.Container{}, errors.New("error"))
client.On("Host").Return("localhost") client.On("Host").Return(&docker.Host{
Host: "localhost",
})
return client, nil return client, nil
} }
fakeRemoteClientFactory := func(filter map[string][]string, host string) (docker.Client, error) { fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
client := new(fakeClient) client := new(fakeClient)
client.On("ListContainers").Return([]docker.Container{}, nil) client.On("ListContainers").Return([]docker.Container{}, nil)
client.On("Host").Return("test") client.On("Host").Return(&docker.Host{
Host: "test",
})
return client, nil return client, nil
} }
@@ -85,14 +93,18 @@ func Test_valid_remote_and_local(t *testing.T) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
client := new(fakeClient) client := new(fakeClient)
client.On("ListContainers").Return([]docker.Container{}, nil) client.On("ListContainers").Return([]docker.Container{}, nil)
client.On("Host").Return("localhost") client.On("Host").Return(&docker.Host{
Host: "localhost",
})
return client, nil return client, nil
} }
fakeRemoteClientFactory := func(filter map[string][]string, host string) (docker.Client, error) { fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
client := new(fakeClient) client := new(fakeClient)
client.On("ListContainers").Return([]docker.Container{}, nil) client.On("ListContainers").Return([]docker.Container{}, nil)
client.On("Host").Return("test") client.On("Host").Return(&docker.Host{
Host: "test",
})
return client, nil return client, nil
} }
@@ -111,13 +123,17 @@ func Test_no_clients(t *testing.T) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) {
client := new(fakeClient) client := new(fakeClient)
client.On("ListContainers").Return([]docker.Container{}, errors.New("error")) client.On("ListContainers").Return([]docker.Container{}, errors.New("error"))
client.On("Host").Return("localhost") client.On("Host").Return(&docker.Host{
Host: "localhost",
})
return client, nil return client, nil
} }
fakeRemoteClientFactory := func(filter map[string][]string, host string) (docker.Client, error) { fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) {
client := new(fakeClient) client := new(fakeClient)
client.On("Host").Return("test") client.On("Host").Return(&docker.Host{
Host: "test",
})
return client, nil return client, nil
} }

View File

@@ -6,6 +6,7 @@ import (
"html/template" "html/template"
"io" "io"
"io/fs" "io/fs"
"sort"
"net/http" "net/http"
"os" "os"
@@ -154,26 +155,21 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
path = h.config.Base path = h.config.Base
} }
// Get all keys from hosts map hosts := make([]*docker.Host, 0, len(h.clients))
hosts := make([]string, 0, len(h.clients)) for _, v := range h.clients {
for k := range h.clients { hosts = append(hosts, v.Host())
hosts = append(hosts, k)
} }
sort.Slice(hosts, func(i, j int) bool {
return hosts[i].Name < hosts[j].Name
})
config := struct { config := map[string]interface{}{
Base string `json:"base"` "base": path,
Version string `json:"version"` "version": h.config.Version,
AuthorizationNeeded bool `json:"authorizationNeeded"` "authorizationNeeded": h.isAuthorizationNeeded(req),
Secured bool `json:"secured"` "secured": secured,
Hostname string `json:"hostname"` "hostname": h.config.Hostname,
Hosts []string `json:"hosts"` "hosts": hosts,
}{
path,
h.config.Version,
h.isAuthorizationNeeded(req),
secured,
h.config.Hostname,
hosts,
} }
data := map[string]interface{}{ data := map[string]interface{}{

View File

@@ -26,6 +26,7 @@ import (
func Test_createRoutes_index(t *testing.T) { func Test_createRoutes_index(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.") require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/"}) handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/"})
req, err := http.NewRequest("GET", "/", nil) req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
@@ -64,6 +65,7 @@ func Test_createRoutes_redirect_with_auth(t *testing.T) {
func Test_createRoutes_foobar(t *testing.T) { func Test_createRoutes_foobar(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("foo page"), 0644), "WriteFile should have no error.") require.NoError(t, afero.WriteFile(fs, "index.html", []byte("foo page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"}) handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"})
req, err := http.NewRequest("GET", "/foobar/", nil) req, err := http.NewRequest("GET", "/foobar/", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")

View File

@@ -49,15 +49,18 @@ func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string,
return args.Get(0).(io.ReadCloser), args.Error(1) return args.Get(0).(io.ReadCloser), args.Error(1)
} }
func (m *MockedClient) Host() string { func (m *MockedClient) Host() *docker.Host {
args := m.Called() args := m.Called()
return args.String(0) return args.Get(0).(*docker.Host)
} }
func createHandler(client docker.Client, 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)
client.(*MockedClient).On("Host").Return(&docker.Host{
Host: "localhost",
})
} }
if content == nil { if content == nil {