Compare commits

...

28 Commits

Author SHA1 Message Date
Amir Raminfar
09a8ace624 Removes timeout 2018-12-03 21:05:26 -08:00
Amir Raminfar
8c2ad64439 1.5.0 2018-12-03 17:44:47 -08:00
Amir Raminfar
e0c6244d54 Adds graceful shutdown 2018-12-03 17:37:23 -08:00
Amir Raminfar
2a837762ed Adds more tests 2018-12-03 17:27:50 -08:00
Amir Raminfar
19ce377ee9 Adds reflex config 2018-12-03 13:32:00 -08:00
Amir Raminfar
b4532bf9f7 Fixes npm watch 2018-12-03 13:26:17 -08:00
Amir Raminfar
033636dd46 1.4.6 2018-12-03 13:10:54 -08:00
Amir Raminfar
769de61657 1.4.5 2018-12-03 13:09:13 -08:00
Amir Raminfar
9d59278035 Fixes version 2018-12-03 13:09:02 -08:00
Amir Raminfar
1f510cdd4e 1.4.4 2018-12-03 13:06:49 -08:00
Amir Raminfar
52bfda2ab2 1.4.3 2018-12-03 13:03:56 -08:00
Amir Raminfar
b92de8a508 Adds more tests 2018-12-03 13:01:04 -08:00
Amir Raminfar
957a5104b8 Uses a proxy interface instead 2018-12-03 12:42:38 -08:00
Amir Raminfar
a4981f1b2c Does a bunch of little cleanup (#11)
* Cleans up the context by reusing r.Context() and passing messages instead of readers

* Fixes tests

* Removes parameters

* Fixes error buffer
2018-12-03 12:34:08 -08:00
Amir Raminfar
beaeecd457 Adds reportcard 2018-11-30 10:23:00 -08:00
Amir Raminfar
dcc9088e31 Cleans up assignment 2018-11-30 10:21:43 -08:00
Amir Raminfar
27e8129caa Adds comments 2018-11-30 10:10:51 -08:00
Amir Raminfar
7d801379db Uses 4 spaces 2018-11-30 10:04:55 -08:00
Amir Raminfar
420da8c363 Runs go fmt 2018-11-30 10:03:32 -08:00
Amir Raminfar
917384a2d9 Adds more tests 2018-11-30 08:38:17 -08:00
Amir Raminfar
e97e69a0e1 Adds more tests 2018-11-30 08:27:44 -08:00
Amir Raminfar
87d51409ff 1.4.2 2018-11-29 19:38:35 -08:00
Amir Raminfar
551b1f04c7 Removes unused code 2018-11-29 19:38:22 -08:00
Amir Raminfar
a35f4ef32e Adds more tests 2018-11-29 19:36:49 -08:00
Amir Raminfar
441f234398 Uses a buffer instead 2018-11-29 12:59:43 -08:00
Amir Raminfar
dc452e2847 Adds snapshot as binary 2018-11-29 11:06:00 -08:00
Amir Raminfar
b9ee28ca8d Adds tests and snapshots 2018-11-29 10:55:25 -08:00
Amir Raminfar
474ce714db 1.4.1 2018-11-28 12:05:43 -08:00
12 changed files with 718 additions and 252 deletions

View File

@@ -10,10 +10,8 @@ trim_trailing_whitespace = true
max_line_length = 120
[*.go]
indent_size = 4
[Makefile]
indent_style = tab
indent_size = 4
[package.json]
indent_size = 1

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.snapshot binary

1
.reflex Normal file
View File

@@ -0,0 +1 @@
-r '\.go$' -R '^node_modules/' -R '^static/' -R '^.cache/' -G '*_test.go' -s -- go run main.go --level debug

View File

@@ -1,3 +1,5 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/amir20/dozzle)](https://goreportcard.com/report/github.com/amir20/dozzle)
# dozzle
Dozzle is a log viewer for Docker. It's free. It's small. And it's right in your browser. Oh, did I mention it is also real-time?

View File

@@ -0,0 +1,46 @@
/* snapshot: Test_handler_listContainers_happy */
HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain; charset=utf-8
[{"id":"1234567890","names":null,"name":"test","image":"image","imageId":"image_id","command":"command","created":0,"state":"state","status":"status"}]
/* snapshot: Test_handler_streamEvents_error */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
/* snapshot: Test_handler_streamEvents_error_request */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
/* snapshot: Test_handler_streamEvents_happy */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
event: containers-changed
data: start
/* snapshot: Test_handler_streamLogs_error_reading */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
/* snapshot: Test_handler_streamLogs_happy */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
data: INFO Testing logs...

View File

@@ -1,69 +1,122 @@
package docker
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/client"
"io"
"log"
"sort"
"strings"
"bytes"
"context"
"encoding/binary"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/client"
"io"
"log"
"sort"
"strings"
)
type dockerClient struct {
cli *client.Client
cli dockerProxy
}
type dockerProxy interface {
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
Events(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
}
// Client is a proxy around the docker client
type Client interface {
ListContainers() ([]Container, error)
ContainerLogs(ctx context.Context, id string) (io.ReadCloser, error)
Events(ctx context.Context) (<-chan events.Message, <-chan error)
ListContainers() ([]Container, error)
ContainerLogs(ctx context.Context, id string) (<-chan string, <-chan error)
Events(ctx context.Context) (<-chan events.Message, <-chan error)
}
// NewClient creates a new instance of Client
func NewClient() Client {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
log.Fatal(err)
}
return &dockerClient{cli}
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
log.Fatal(err)
}
return &dockerClient{cli}
}
func (d *dockerClient) ListContainers() ([]Container, error) {
list, err := d.cli.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
return nil, err
}
list, err := d.cli.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
return nil, err
}
var containers []Container
for _, c := range list {
var containers []Container
for _, c := range list {
container := Container{
ID: c.ID[:12],
Names: c.Names,
Name: strings.TrimPrefix(c.Names[0], "/"),
Image: c.Image,
ImageID: c.ImageID,
Command: c.Command,
Created: c.Created,
State: c.State,
Status: c.Status,
}
containers = append(containers, container)
}
container := Container{
ID: c.ID[:12],
Names: c.Names,
Name: strings.TrimPrefix(c.Names[0], "/"),
Image: c.Image,
ImageID: c.ImageID,
Command: c.Command,
Created: c.Created,
State: c.State,
Status: c.Status,
}
containers = append(containers, container)
}
sort.Slice(containers, func(i, j int) bool {
return containers[i].Name < containers[j].Name
})
sort.Slice(containers, func(i, j int) bool {
return containers[i].Name < containers[j].Name
})
return containers, nil
if containers == nil {
containers = []Container{}
}
return containers, nil
}
func (d *dockerClient) ContainerLogs(ctx context.Context, id string) (io.ReadCloser, error) {
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true}
return d.cli.ContainerLogs(ctx, id, options)
func (d *dockerClient) ContainerLogs(ctx context.Context, id string) (<-chan string, <-chan error) {
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true}
reader, err := d.cli.ContainerLogs(ctx, id, options)
errChannel := make(chan error, 1)
if err != nil {
errChannel <- err
close(errChannel)
return nil, errChannel
}
go func() {
<-ctx.Done()
reader.Close()
}()
messages := make(chan string)
go func() {
defer close(messages)
defer close(errChannel)
defer reader.Close()
hdr := make([]byte, 8)
var buffer bytes.Buffer
for {
_, err := reader.Read(hdr)
if err != nil {
errChannel <- err
break
}
count := binary.BigEndian.Uint32(hdr[4:])
_, err = io.CopyN(&buffer, reader, int64(count))
if err != nil {
errChannel <- err
break
}
messages <- buffer.String()
buffer.Reset()
}
}()
return messages, errChannel
}
func (d *dockerClient) Events(ctx context.Context) (<-chan events.Message, <-chan error) {
return d.cli.Events(ctx, types.EventsOptions{})
return d.cli.Events(ctx, types.EventsOptions{})
}

140
docker/client_test.go Normal file
View File

@@ -0,0 +1,140 @@
package docker
import (
"bytes"
"context"
"encoding/binary"
"errors"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"io"
"io/ioutil"
"testing"
)
type mockedProxy struct {
mock.Mock
dockerProxy
}
func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) {
args := m.Called()
containers, ok := args.Get(0).([]types.Container)
if !ok && args.Get(0) != nil {
panic("containers is not of type []types.Container")
}
return containers, args.Error(1)
}
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
args := m.Called(ctx, id)
reader, ok := args.Get(0).(io.ReadCloser)
if !ok && args.Get(0) != nil {
panic("reader is not of type io.ReadCloser")
}
return reader, args.Error(1)
}
func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &dockerClient{proxy}
list, err := client.ListContainers()
assert.Empty(t, list, "list should be empty")
require.NoError(t, err, "error should not return an error.")
proxy.AssertExpectations(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}
list, err := client.ListContainers()
assert.Nil(t, list, "list should be nil")
require.Error(t, err, "test.")
proxy.AssertExpectations(t)
}
func Test_dockerClient_ListContainers_happy(t *testing.T) {
containers := []types.Container{
{
ID: "abcdefghijklmnopqrst",
Names: []string{"/z_test_container"},
},
{
ID: "1234567890_abcxyzdef",
Names: []string{"/a_test_container"},
},
}
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &dockerClient{proxy}
list, err := client.ListContainers()
require.NoError(t, err, "error should not return an error.")
assert.Equal(t, list, []Container{
{
ID: "1234567890_a",
Name: "a_test_container",
Names: []string{"/a_test_container"},
},
{
ID: "abcdefghijkl",
Name: "z_test_container",
Names: []string{"/z_test_container"},
},
})
proxy.AssertExpectations(t)
}
func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
id := "123456"
proxy := new(mockedProxy)
expected := "INFO Testing logs..."
b := make([]byte, 8)
binary.BigEndian.PutUint32(b[4:], uint32(len(expected)))
b = append(b, []byte(expected)...)
var reader io.ReadCloser
reader = ioutil.NopCloser(bytes.NewReader(b))
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(reader, nil)
client := &dockerClient{proxy}
messages, _ := client.ContainerLogs(context.Background(), id)
actual, _ := <-messages
assert.Equal(t, expected, actual, "message doesn't match expected")
_, ok := <-messages
assert.False(t, ok, "channel should have been closed")
proxy.AssertExpectations(t)
}
func Test_dockerClient_ContainerLogs_error(t *testing.T) {
id := "123456"
proxy := new(mockedProxy)
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
client := &dockerClient{proxy}
messages, err := client.ContainerLogs(context.Background(), id)
assert.Nil(t, messages, "messages should be nil")
e, _ := <-err
assert.Error(t, e, "error should have been returned")
_, ok := <-err
assert.False(t, ok, "error channel should have been closed")
proxy.AssertExpectations(t)
}

View File

@@ -1,13 +1,14 @@
package docker
// Container represents an internal representation of docker containers
type Container struct {
ID string `json:"id"`
Names []string `json:"names"`
Name string `json:"name"`
Image string `json:"image"`
ImageID string `json:"imageId"`
Command string `json:"command"`
Created int64 `json:"created"`
State string `json:"state"`
Status string `json:"status"`
ID string `json:"id"`
Names []string `json:"names"`
Name string `json:"name"`
Image string `json:"image"`
ImageID string `json:"imageId"`
Command string `json:"command"`
Created int64 `json:"created"`
State string `json:"state"`
Status string `json:"status"`
}

378
main.go
View File

@@ -1,218 +1,224 @@
package main
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/amir20/dozzle/docker"
"github.com/gobuffalo/packr"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"html/template"
"net/http"
"strings"
"context"
"encoding/json"
"fmt"
"github.com/amir20/dozzle/docker"
"github.com/gobuffalo/packr"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"html/template"
"net/http"
"os"
"os/signal"
"strings"
"time"
)
var (
dockerClient docker.Client
addr = ""
base = ""
level = ""
version = "dev"
commit = "none"
date = "unknown"
addr = ""
base = ""
level = ""
version = "dev"
commit = "none"
date = "unknown"
)
type handler struct {
client docker.Client
box packr.Box
}
func init() {
flag.StringVar(&addr, "addr", ":8080", "http service address")
flag.StringVar(&base, "base", "/", "base address of the application to mount")
flag.StringVar(&level, "level", "info", "logging level")
flag.Parse()
flag.StringVar(&addr, "addr", ":8080", "http service address")
flag.StringVar(&base, "base", "/", "base address of the application to mount")
flag.StringVar(&level, "level", "info", "logging level")
flag.Parse()
l, _ := log.ParseLevel(level)
log.SetLevel(l)
l, _ := log.ParseLevel(level)
log.SetLevel(l)
log.SetFormatter(&log.TextFormatter{
DisableTimestamp: true,
DisableLevelTruncation: true,
})
dockerClient = docker.NewClient()
_, err := dockerClient.ListContainers()
if err != nil {
log.Fatalf("Could not connect to Docker Engine: %v", err)
}
log.SetFormatter(&log.TextFormatter{
DisableTimestamp: true,
DisableLevelTruncation: true,
})
}
func main() {
r := mux.NewRouter()
dockerClient := docker.NewClient()
_, err := dockerClient.ListContainers()
if base != "/" {
r.HandleFunc(base, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
}))
}
if err != nil {
log.Fatalf("Could not connect to Docker Engine: %v", err)
}
s := r.PathPrefix(base).Subrouter()
box := packr.NewBox("./static")
box := packr.NewBox("./static")
h := &handler{dockerClient, box}
s.HandleFunc("/api/containers.json", listContainers)
s.HandleFunc("/api/logs/stream", streamLogs)
s.HandleFunc("/api/events/stream", streamEvents)
s.HandleFunc("/version", versionHandler)
s.PathPrefix("/").Handler(http.StripPrefix(base, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
fileServer := http.FileServer(box)
if box.Has(req.URL.Path) && req.URL.Path != "" && req.URL.Path != "/" {
fileServer.ServeHTTP(w, req)
} else {
handleIndex(box, w)
}
})))
r := mux.NewRouter()
log.Infof("Accepting connections on %s", addr)
log.Fatal(http.ListenAndServe(addr, r))
if base != "/" {
r.HandleFunc(base, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
}))
}
s := r.PathPrefix(base).Subrouter()
s.HandleFunc("/api/containers.json", h.listContainers)
s.HandleFunc("/api/logs/stream", h.streamLogs)
s.HandleFunc("/api/events/stream", h.streamEvents)
s.HandleFunc("/version", h.version)
s.PathPrefix("/").Handler(http.StripPrefix(base, http.HandlerFunc(h.index)))
srv := &http.Server{
Addr: addr,
Handler: r,
}
go func() {
log.Infof("Accepting connections on %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
signal.Notify(c, os.Kill)
<-c
log.Infof("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
srv.Shutdown(ctx)
os.Exit(0)
}
func versionHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, version)
fmt.Fprintln(w, commit)
fmt.Fprintln(w, date)
func (h *handler) index(w http.ResponseWriter, req *http.Request) {
fileServer := http.FileServer(h.box)
if h.box.Has(req.URL.Path) && req.URL.Path != "" && req.URL.Path != "/" {
fileServer.ServeHTTP(w, req)
} else {
text, _ := h.box.FindString("index.html")
text = strings.Replace(text, "__BASE__", "{{ .Base }}", -1)
tmpl, err := template.New("index.html").Parse(text)
if err != nil {
panic(err)
}
path := ""
if base != "/" {
path = base
}
data := struct{ Base string }{path}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
func handleIndex(box packr.Box, w http.ResponseWriter) {
text, _ := box.FindString("index.html")
text = strings.Replace(text, "__BASE__", "{{ .Base }}", -1)
tmpl, err := template.New("index.html").Parse(text)
if err != nil {
panic(err)
}
path := ""
if base != "/" {
path = base
}
data := struct{ Base string }{path}
err = tmpl.Execute(w, data)
if err != nil {
panic(err)
}
func (h *handler) listContainers(w http.ResponseWriter, r *http.Request) {
containers, err := h.client.ListContainers()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(containers)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func listContainers(w http.ResponseWriter, r *http.Request) {
containers, err := dockerClient.ListContainers()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(containers)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id is required", http.StatusBadRequest)
return
}
f, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
messages, err := h.client.ContainerLogs(r.Context(), id)
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Transfer-Encoding", "chunked")
log.Debugf("Starting to stream logs for %s", id)
Loop:
for {
select {
case message, ok := <-messages:
if !ok {
break Loop
}
_, e := fmt.Fprintf(w, "data: %s\n\n", message)
if e != nil {
log.Debugf("Error while writing to log stream: %v", e)
break Loop
}
f.Flush()
case e := <-err:
log.Debugf("Error while reading from log stream: %v", e)
break Loop
}
}
}
func streamLogs(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id is required", http.StatusBadRequest)
return
}
func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
f, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
f, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Transfer-Encoding", "chunked")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reader, err := dockerClient.ContainerLogs(ctx, id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer reader.Close()
go func() {
<-r.Context().Done()
reader.Close()
}()
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Transfer-Encoding", "chunked")
hdr := make([]byte, 8)
content := make([]byte, 1024, 1024*1024)
for {
_, err := reader.Read(hdr)
if err != nil {
log.Debugf("Error while reading from log stream: %v", err)
break
}
count := binary.BigEndian.Uint32(hdr[4:])
n, err := reader.Read(content[:count])
if err != nil {
log.Debugf("Error while reading from log stream: %v", err)
break
}
_, err = fmt.Fprintf(w, "data: %s\n\n", content[:n])
if err != nil {
log.Debugf("Error while writing to log stream: %v", err)
break
}
f.Flush()
}
}
func streamEvents(w http.ResponseWriter, r *http.Request) {
f, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Transfer-Encoding", "chunked")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
messages, err := dockerClient.Events(ctx)
ctx := r.Context()
messages, err := h.client.Events(ctx)
Loop:
for {
select {
case message, ok := <-messages:
if !ok {
break Loop
}
switch message.Action {
case "connect", "disconnect", "create", "destroy", "start", "stop":
log.Debugf("Triggering docker event: %v", message.Action)
_, err := fmt.Fprintf(w, "event: containers-changed\n")
_, err = fmt.Fprintf(w, "data: %s\n\n", message.Action)
for {
select {
case message, ok := <-messages:
if !ok {
break Loop
}
switch message.Action {
case "connect", "disconnect", "create", "destroy", "start", "stop":
log.Debugf("Triggering docker event: %v", message.Action)
_, err := fmt.Fprintf(w, "event: containers-changed\ndata: %s\n\n", message.Action)
if err != nil {
log.Debugf("Error while writing to event stream: %v", err)
break
}
f.Flush()
default:
log.Debugf("Ignoring docker event: %v", message.Action)
}
case <-r.Context().Done():
cancel()
break Loop
case <-err:
cancel()
break Loop
}
}
if err != nil {
log.Debugf("Error while writing to event stream: %v", err)
break
}
f.Flush()
default:
log.Debugf("Ignoring docker event: %v", message.Action)
}
case <-ctx.Done():
break Loop
case <-err:
break Loop
}
}
}
func (h *handler) version(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, version)
fmt.Fprintln(w, commit)
fmt.Fprintln(w, date)
}

217
main_test.go Normal file
View File

@@ -0,0 +1,217 @@
package main
import (
"context"
"errors"
"github.com/docker/docker/api/types/events"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/amir20/dozzle/docker"
"github.com/beme/abide"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type MockedClient struct {
mock.Mock
docker.Client
}
func (m *MockedClient) ListContainers() ([]docker.Container, error) {
args := m.Called()
containers, ok := args.Get(0).([]docker.Container)
if !ok {
panic("containers is not of type []docker.Container")
}
return containers, args.Error(1)
}
func (m *MockedClient) ContainerLogs(ctx context.Context, id string) (<-chan string, <-chan error) {
args := m.Called(ctx, id)
channel, ok := args.Get(0).(chan string)
if !ok {
panic("channel is not of type chan string")
}
err, ok := args.Get(1).(chan error)
if !ok {
panic("error is not of type chan error")
}
return channel, err
}
func (m *MockedClient) Events(ctx context.Context) (<-chan events.Message, <-chan error) {
args := m.Called(ctx)
channel, ok := args.Get(0).(chan events.Message)
if !ok {
panic("channel is not of type chan events.Message")
}
err, ok := args.Get(1).(chan error)
if !ok {
panic("error is not of type chan error")
}
return channel, err
}
func Test_handler_listContainers_happy(t *testing.T) {
req, err := http.NewRequest("GET", "/api/containers.json", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
mockedClient := new(MockedClient)
containers := []docker.Container{
{
ID: "1234567890",
Status: "status",
State: "state",
Name: "test",
Created: 0,
Command: "command",
ImageID: "image_id",
Image: "image",
},
}
mockedClient.On("ListContainers", mock.Anything).Return(containers, nil)
h := handler{client: mockedClient}
handler := http.HandlerFunc(h.listContainers)
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_happy(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", "123456")
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
mockedClient := new(MockedClient)
messages := make(chan string)
errChannel := make(chan error)
mockedClient.On("ContainerLogs", mock.Anything, id).Return(messages, errChannel)
go func() {
messages <- "INFO Testing logs..."
close(messages)
}()
h := handler{client: mockedClient}
handler := http.HandlerFunc(h.streamLogs)
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_error_reading(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", "123456")
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
mockedClient := new(MockedClient)
messages := make(chan string)
errChannel := make(chan error)
mockedClient.On("ContainerLogs", mock.Anything, id).Return(messages, errChannel)
go func() {
errChannel <- errors.New("test error")
}()
h := handler{client: mockedClient}
handler := http.HandlerFunc(h.streamLogs)
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_happy(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
mockedClient := new(MockedClient)
messages := make(chan events.Message)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
go func() {
messages <- events.Message{
Action: "start",
}
messages <- events.Message{
Action: "something-random",
}
close(messages)
}()
h := handler{client: mockedClient}
handler := http.HandlerFunc(h.streamEvents)
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_error(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
mockedClient := new(MockedClient)
messages := make(chan events.Message)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
go func() {
errChannel <- errors.New("fake error")
close(messages)
}()
h := handler{client: mockedClient}
handler := http.HandlerFunc(h.streamEvents)
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_error_request(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
mockedClient := new(MockedClient)
messages := make(chan events.Message)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
ctx, cancel := context.WithCancel(context.Background())
req = req.WithContext(ctx)
go func() {
cancel()
}()
h := handler{client: mockedClient}
handler := http.HandlerFunc(h.streamEvents)
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func TestMain(m *testing.M) {
exit := m.Run()
abide.Cleanup()
os.Exit(exit)
}

16
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "dozzle",
"version": "1.4.0",
"version": "1.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -9263,9 +9263,9 @@
"dev": true
},
"yargs": {
"version": "12.0.4",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.4.tgz",
"integrity": "sha512-f5esswlPO351AnejaO2A1ZZr0zesz19RehQKwiRDqWtrraWrJy16tsUIKgDXFMVytvNOHPVmTiaTh3wO67I0fQ==",
"version": "12.0.5",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
"integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
"dev": true,
"requires": {
"cliui": "^4.0.0",
@@ -9279,13 +9279,13 @@
"string-width": "^2.0.0",
"which-module": "^2.0.0",
"y18n": "^3.2.1 || ^4.0.0",
"yargs-parser": "^11.1.0"
"yargs-parser": "^11.1.1"
}
},
"yargs-parser": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.0.tgz",
"integrity": "sha512-lGA5HsbjkpCfekDBHAhgE5OE8xEoqiUDylowr+BvhRCwG1xVYTsd8hx2CYC0NY4k9RIgJeybFTG2EZW4P2aN1w==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
"integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",

View File

@@ -1,12 +1,13 @@
{
"name": "dozzle",
"version": "1.4.0",
"version": "1.5.0",
"description": "",
"main": "index.js",
"scripts": {
"prestart": "npm run clean",
"start": "DOCKER_API_VERSION=1.38 concurrently 'reflex -g '*.go' -R 'node_modules' -s -- go run main.go --level debug' 'npm run watch-assets'",
"start": "DOCKER_API_VERSION=1.38 concurrently 'npm run watch-server' 'npm run watch-assets'",
"watch-assets": "parcel watch --public-url '__BASE__' assets/index.html -d static",
"watch-server": "reflex -c .reflex",
"prebuild": "npm run clean",
"build": "parcel build --no-source-maps --public-url '__BASE__' assets/index.html -d static",
"clean": "rm -rf static/ a_main-packr.go",