diff --git a/README.md b/README.md index bbf1015f..fd7e2591 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ dozzle doesn't support authentication out of the box. You can control the device will bind to `localhost` on port `1224`. You can then use a reverse proxy to control who can see dozzle. +If you wish to restrict the containers shown you can pass the `--filterName` parameter. For example, + + $ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --filterName "xyz" + +this would then only allow you to view containers with a name starting with "xyz" + #### Changing base URL dozzle by default mounts to "/". If you want to control the base path you can use the `--base` option. For example, if you want to mount at "/foobar", @@ -66,7 +72,20 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can | `--level` | `DOZZLE_LEVEL` | `info` | | n/a | `DOCKER_API_VERSION` | `1.38` | | `--tailSize` | `DOZZLE_TAILSIZE` | `300` | +| `--filterName` | `DOZZLE_FILTERNAME` | `""` | ## License [MIT](LICENSE) + +### Building +To Build and test locally: + +1. Install NodeJs. +2. Install Go. +3. Globally install [packr utility](https://github.com/gobuffalo/packr) with `go get -u github.com/gobuffalo/packr/packr` outside of dozzle directory. +4. Install [reflex](https://github.com/cespare/reflex) with `get -u github.com/cespare/reflex` outside of dozzle. +5. Install node modules with `npm install`. +6. Do `npm start` + +Instructions for Github actions can be found at [here](.github/goreleaser/Dockerfile). diff --git a/docker/client.go b/docker/client.go index daa8ac3e..8934f43c 100644 --- a/docker/client.go +++ b/docker/client.go @@ -1,21 +1,24 @@ package docker import ( - "strconv" "bytes" "context" "encoding/binary" + "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "io" "log" "sort" + "strconv" "strings" ) type dockerClient struct { - cli dockerProxy + cli dockerProxy + filter string } type dockerProxy interface { @@ -27,21 +30,47 @@ type dockerProxy interface { // Client is a proxy around the docker client type Client interface { ListContainers() ([]Container, error) + FindContainer(string) (Container, error) ContainerLogs(context.Context, string, int) (<-chan string, <-chan error) Events(context.Context) (<-chan events.Message, <-chan error) } // NewClient creates a new instance of Client -func NewClient() Client { +func NewClient(filter string) Client { cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { log.Fatal(err) } - return &dockerClient{cli} + return &dockerClient{cli, filter} +} + +func (d *dockerClient) FindContainer(id string) (Container, error) { + var container Container + containers, err := d.ListContainers() + if err != nil { + return container, err + } + + found := false + for _, c := range containers { + if c.ID == id { + container = c + found = true + break + } + } + if found == false { + return container, fmt.Errorf("Unable to find container with id: %s", id) + } + + return container, nil } func (d *dockerClient) ListContainers() ([]Container, error) { - list, err := d.cli.ContainerList(context.Background(), types.ContainerListOptions{}) + containerListOptions := types.ContainerListOptions{ + Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: d.filter}), + } + list, err := d.cli.ContainerList(context.Background(), containerListOptions) if err != nil { return nil, err } diff --git a/docker/client_test.go b/docker/client_test.go index af1aab0a..f7a0ffd4 100644 --- a/docker/client_test.go +++ b/docker/client_test.go @@ -41,7 +41,7 @@ func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options type func Test_dockerClient_ListContainers_null(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil) - client := &dockerClient{proxy} + client := &dockerClient{proxy, ""} list, err := client.ListContainers() assert.Empty(t, list, "list should be empty") @@ -53,7 +53,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) { func Test_dockerClient_ListContainers_error(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test")) - client := &dockerClient{proxy} + client := &dockerClient{proxy, ""} list, err := client.ListContainers() assert.Nil(t, list, "list should be nil") @@ -76,7 +76,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) - client := &dockerClient{proxy} + client := &dockerClient{proxy, ""} list, err := client.ListContainers() require.NoError(t, err, "error should not return an error.") @@ -112,7 +112,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) { options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true} proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil) - client := &dockerClient{proxy} + client := &dockerClient{proxy, ""} messages, _ := client.ContainerLogs(context.Background(), id, 300) actual, _ := <-messages @@ -129,7 +129,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) { proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test")) - client := &dockerClient{proxy} + client := &dockerClient{proxy, ""} messages, err := client.ContainerLogs(context.Background(), id, 300) diff --git a/main.go b/main.go index 3a6d8152..922ea275 100644 --- a/main.go +++ b/main.go @@ -20,13 +20,14 @@ import ( ) var ( - addr = "" - base = "" - level = "" - tailSize = 300 - version = "dev" - commit = "none" - date = "unknown" + addr = "" + base = "" + level = "" + tailSize = 300 + filterName = "" + version = "dev" + commit = "none" + date = "unknown" ) type handler struct { @@ -39,6 +40,7 @@ func init() { pflag.String("base", "/", "base address of the application to mount") pflag.String("level", "info", "logging level") pflag.Int("tailSize", 300, "Tail size to use for initial container logs") + pflag.String("filterName", "", "Filters containers by name") pflag.Parse() viper.AutomaticEnv() @@ -49,6 +51,7 @@ func init() { base = viper.GetString("base") level = viper.GetString("level") tailSize = viper.GetInt("tailSize") + filterName = viper.GetString("filterName") l, _ := log.ParseLevel(level) log.SetLevel(l) @@ -77,7 +80,8 @@ func createRoutes(base string, h *handler) *mux.Router { func main() { log.Infof("Dozzle version %s", version) - dockerClient := docker.NewClient() + log.Infof("Restricting to containers with names matching '%s'", filterName) + dockerClient := docker.NewClient(filterName) _, err := dockerClient.ListContainers() if err != nil { @@ -156,7 +160,14 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) { http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) return } - messages, err := h.client.ContainerLogs(r.Context(), id, tailSize) + + container, e := h.client.FindContainer(id) + if e != nil { + http.Error(w, e.Error(), http.StatusInternalServerError) + return + } + + messages, err := h.client.ContainerLogs(r.Context(), container.ID, tailSize) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") diff --git a/main_test.go b/main_test.go index 543b1da2..85df02ad 100644 --- a/main_test.go +++ b/main_test.go @@ -3,12 +3,13 @@ package main import ( "context" "errors" - "github.com/magiconair/properties/assert" "net/http" "net/http/httptest" "os" "testing" + "github.com/magiconair/properties/assert" + "github.com/amir20/dozzle/docker" "github.com/beme/abide" "github.com/docker/docker/api/types/events" @@ -22,6 +23,15 @@ type MockedClient struct { docker.Client } +func (m *MockedClient) FindContainer(id string) (docker.Container, error) { + args := m.Called(id) + container, ok := args.Get(0).(docker.Container) + if !ok { + panic("containers is not of type docker.Container") + } + return container, args.Error(1) +} + func (m *MockedClient) ListContainers() ([]docker.Container, error) { args := m.Called() containers, ok := args.Get(0).([]docker.Container) @@ -101,7 +111,8 @@ func Test_handler_streamLogs_happy(t *testing.T) { messages := make(chan string) errChannel := make(chan error) - mockedClient.On("ContainerLogs", mock.Anything, id, 300).Return(messages, errChannel) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) + mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, 300).Return(messages, errChannel) go func() { messages <- "INFO Testing logs..." close(messages) @@ -126,6 +137,7 @@ func Test_handler_streamLogs_error_reading(t *testing.T) { mockedClient := new(MockedClient) messages := make(chan string) errChannel := make(chan error) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) mockedClient.On("ContainerLogs", mock.Anything, id, 300).Return(messages, errChannel) go func() {