Compare commits
312 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71f214e20d | ||
|
|
63f132c820 | ||
|
|
ffd964fe82 | ||
|
|
63dd413296 | ||
|
|
88bb3f296a | ||
|
|
f6e0e4ed08 | ||
|
|
707ab974a1 | ||
|
|
fa0f743cb4 | ||
|
|
bde434851c | ||
|
|
dc8f6f722e | ||
|
|
649e577483 | ||
|
|
4480587ae8 | ||
|
|
a4db64300c | ||
|
|
8925a6ed14 | ||
|
|
c5bd1fc735 | ||
|
|
61a35663dc | ||
|
|
2a52d7b6a1 | ||
|
|
6adec499e1 | ||
|
|
5990f126f4 | ||
|
|
e1d66b9c78 | ||
|
|
6014e10f62 | ||
|
|
457b760da5 | ||
|
|
e8ab871efb | ||
|
|
1651025969 | ||
|
|
b81d718b7e | ||
|
|
2600703625 | ||
|
|
9a613e0b85 | ||
|
|
8e63a5742a | ||
|
|
c9475e10af | ||
|
|
b10c99ae3e | ||
|
|
b93346b673 | ||
|
|
4753dbd847 | ||
|
|
44471e9d3a | ||
|
|
0f5a89aae4 | ||
|
|
d5bdaa172b | ||
|
|
714975ca95 | ||
|
|
68f364c7ff | ||
|
|
f34144e7fc | ||
|
|
e485f0fe9a | ||
|
|
115e7de7bf | ||
|
|
080b0b29a7 | ||
|
|
ebc9158a45 | ||
|
|
38a3167cbd | ||
|
|
b48a75f30d | ||
|
|
5b10a39ad5 | ||
|
|
1d7048115e | ||
|
|
1eb373da5e | ||
|
|
9cf522f7df | ||
|
|
153a620676 | ||
|
|
b1feba8314 | ||
|
|
277701bc56 | ||
|
|
39c250993e | ||
|
|
d8b82c8200 | ||
|
|
93faeaaedc | ||
|
|
35d08a0a97 | ||
|
|
921082e32f | ||
|
|
0dd119efae | ||
|
|
2019d29161 | ||
|
|
c22afb19f3 | ||
|
|
50de4b7f11 | ||
|
|
a9c119297f | ||
|
|
3c954a0840 | ||
|
|
494d95e9cd | ||
|
|
489460042d | ||
|
|
2315965eb2 | ||
|
|
0a5e6da3ae | ||
|
|
d9aedd78ba | ||
|
|
37fdeb6d18 | ||
|
|
3bd3dd2d04 | ||
|
|
af42d980d0 | ||
|
|
2070b9eee4 | ||
|
|
8651b37723 | ||
|
|
22c350aed5 | ||
|
|
80686bc279 | ||
|
|
2b3a60b950 | ||
|
|
08b52106fb | ||
|
|
1ed7c4e513 | ||
|
|
3b2b46f4ac | ||
|
|
b356ffcd68 | ||
|
|
197c2cc87b | ||
|
|
ee7e919c12 | ||
|
|
e4275003b3 | ||
|
|
0da82500f3 | ||
|
|
c14e8bfd1a | ||
|
|
d8eda3384b | ||
|
|
fe14cd184a | ||
|
|
5a367fa20a | ||
|
|
2db858a227 | ||
|
|
3cc17c0802 | ||
|
|
72d20457b1 | ||
|
|
1efa49644e | ||
|
|
e733049876 | ||
|
|
0562c9a625 | ||
|
|
40c3c2d23e | ||
|
|
366329f497 | ||
|
|
7942ae66b7 | ||
|
|
a0d65b7fc5 | ||
|
|
b4623a6d8b | ||
|
|
77a70faf04 | ||
|
|
6a44ebaa5c | ||
|
|
1d411b490b | ||
|
|
1b42514642 | ||
|
|
d371aa5111 | ||
|
|
4743c8be8f | ||
|
|
ff0a774549 | ||
|
|
94ec9e49f0 | ||
|
|
a2fe47b364 | ||
|
|
c04535d9b0 | ||
|
|
1da26a49c1 | ||
|
|
459f3f534d | ||
|
|
2b73c79c5d | ||
|
|
321100935c | ||
|
|
1af781f7a4 | ||
|
|
af4215f0fb | ||
|
|
6474ebf9c2 | ||
|
|
bb4a4407c7 | ||
|
|
e534397dc5 | ||
|
|
a0769a3bdc | ||
|
|
7586b350f3 | ||
|
|
7af1a65503 | ||
|
|
3e29ea82d0 | ||
|
|
082ad4d222 | ||
|
|
a64e6f804c | ||
|
|
82e3eb3671 | ||
|
|
baa2c5583f | ||
|
|
ef1d872c68 | ||
|
|
729e894553 | ||
|
|
1356f0bfc6 | ||
|
|
1c576b71a7 | ||
|
|
adf5055397 | ||
|
|
4011cc5f65 | ||
|
|
df38a3d138 | ||
|
|
c08cadcb89 | ||
|
|
9a6f9afa18 | ||
|
|
08201fc6d3 | ||
|
|
603b7a7298 | ||
|
|
39f5c86159 | ||
|
|
f45cafb10c | ||
|
|
45fc80342e | ||
|
|
c73c6e3bfc | ||
|
|
35caea0830 | ||
|
|
8eccef7e19 | ||
|
|
6fdfd901a0 | ||
|
|
e274786126 | ||
|
|
a03690044e | ||
|
|
dce72466bc | ||
|
|
1ffb85ea13 | ||
|
|
4d5fbe424b | ||
|
|
bd2af1568c | ||
|
|
77e2426605 | ||
|
|
fde6892783 | ||
|
|
9e2135d923 | ||
|
|
596390f813 | ||
|
|
181812d321 | ||
|
|
0d4bfc5e25 | ||
|
|
33b33aff11 | ||
|
|
506e03bbf7 | ||
|
|
1f7385da36 | ||
|
|
d0036f177f | ||
|
|
07e3571a78 | ||
|
|
3662cfb03c | ||
|
|
9ba92162a4 | ||
|
|
8ff7881971 | ||
|
|
43be83330e | ||
|
|
275f72de12 | ||
|
|
e94cfea3c2 | ||
|
|
c8d7e165a7 | ||
|
|
bc608c302e | ||
|
|
b5a4ee3b12 | ||
|
|
5b8c1249e6 | ||
|
|
e4f7f2410e | ||
|
|
d8b95a2921 | ||
|
|
513e6f4e07 | ||
|
|
8146169e18 | ||
|
|
a620ef1c32 | ||
|
|
c81c9a4e7f | ||
|
|
e7f9faf70c | ||
|
|
6294db4ba7 | ||
|
|
7523bae996 | ||
|
|
68f03b7790 | ||
|
|
03f88676a9 | ||
|
|
e6661d5f20 | ||
|
|
72b5cf8199 | ||
|
|
de987026b0 | ||
|
|
b7bfe1cb7a | ||
|
|
309c0240c0 | ||
|
|
bc89a91795 | ||
|
|
7e4c82a9ab | ||
|
|
64e2f44bf5 | ||
|
|
c1dea09711 | ||
|
|
e1ef638c5f | ||
|
|
867a453500 | ||
|
|
8032b1dc83 | ||
|
|
19053a8d15 | ||
|
|
17e1d82d84 | ||
|
|
fd866288f0 | ||
|
|
e7f8e14e8f | ||
|
|
6adce272b7 | ||
|
|
dee0655c8f | ||
|
|
628c5aead2 | ||
|
|
2f7846aeb9 | ||
|
|
ae0972963c | ||
|
|
e2ad58a090 | ||
|
|
6779db113a | ||
|
|
ec50054d52 | ||
|
|
7e65313765 | ||
|
|
e6de703443 | ||
|
|
2b7cfc6ef4 | ||
|
|
46736c6137 | ||
|
|
7a33417e45 | ||
|
|
0cd617d1ea | ||
|
|
70fae98a51 | ||
|
|
87dd7c3d3b | ||
|
|
512b03c098 | ||
|
|
bfda0ceb84 | ||
|
|
29f8fe0b51 | ||
|
|
40f4d51562 | ||
|
|
8d59ecee88 | ||
|
|
dbc9359c3d | ||
|
|
11ed955b37 | ||
|
|
88ebe6655b | ||
|
|
817a5c5c8e | ||
|
|
fbc7b6bb1b | ||
|
|
890d62864c | ||
|
|
2d1fb4c198 | ||
|
|
036ce7d98a | ||
|
|
35769b0d5e | ||
|
|
03eab4c5f8 | ||
|
|
9e92f9621b | ||
|
|
4debc53baa | ||
|
|
76f6c8fb4f | ||
|
|
455ec91735 | ||
|
|
43db357466 | ||
|
|
97213ac0fc | ||
|
|
5087ff02f0 | ||
|
|
f9a47face0 | ||
|
|
5797c16407 | ||
|
|
8c129b2695 | ||
|
|
a97662f3f9 | ||
|
|
36c0d327c7 | ||
|
|
117a58e6b8 | ||
|
|
b29fd24f92 | ||
|
|
6c5323fb67 | ||
|
|
4dc3e32e76 | ||
|
|
b7004b2068 | ||
|
|
3772d36e41 | ||
|
|
86fd8cb82e | ||
|
|
2f472ecd36 | ||
|
|
d430e7f7fd | ||
|
|
5602edb322 | ||
|
|
a1066dee5d | ||
|
|
30f23ccaa9 | ||
|
|
72cc1a08f8 | ||
|
|
37bd1d1163 | ||
|
|
9d76c260e8 | ||
|
|
dba2349f9e | ||
|
|
e09818969c | ||
|
|
5dd1bf971d | ||
|
|
66262869b7 | ||
|
|
a7e71687ba | ||
|
|
137c71e50d | ||
|
|
7c27dde94c | ||
|
|
6b432803ff | ||
|
|
6394fcea39 | ||
|
|
ace93d5e73 | ||
|
|
6d3fafb8d3 | ||
|
|
875aede316 | ||
|
|
667c096ef5 | ||
|
|
e06d6a111a | ||
|
|
537e1a1b5b | ||
|
|
b49d391e73 | ||
|
|
0ab4f7ab51 | ||
|
|
b41f20ecf2 | ||
|
|
9a0e1571c5 | ||
|
|
d50282a9ed | ||
|
|
169d5e88f9 | ||
|
|
488d1aa532 | ||
|
|
26a1367cae | ||
|
|
0cb83dc4c8 | ||
|
|
c9428ae749 | ||
|
|
804c0e6971 | ||
|
|
fd06872223 | ||
|
|
7a3f0cade2 | ||
|
|
eb622b5408 | ||
|
|
8812f80bb8 | ||
|
|
c479121bc6 | ||
|
|
bcdba14302 | ||
|
|
21674ef7d8 | ||
|
|
efb03b2840 | ||
|
|
716e8c6db5 | ||
|
|
4f7598ada6 | ||
|
|
5dcde91936 | ||
|
|
64ba618188 | ||
|
|
b2ac37c322 | ||
|
|
72839fea10 | ||
|
|
262ba5e9f4 | ||
|
|
d0e1614e08 | ||
|
|
da62f2543f | ||
|
|
9462adaae2 | ||
|
|
952c38e7b3 | ||
|
|
00615f5ffa | ||
|
|
38f90f396f | ||
|
|
4a2f757754 | ||
|
|
2016a4c59c | ||
|
|
09a8ace624 | ||
|
|
8c2ad64439 | ||
|
|
e0c6244d54 | ||
|
|
2a837762ed | ||
|
|
19ce377ee9 | ||
|
|
b4532bf9f7 | ||
|
|
033636dd46 | ||
|
|
769de61657 |
23
.babelrc
23
.babelrc
@@ -1,18 +1,9 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"regenerator": true
|
||||
}
|
||||
]
|
||||
]
|
||||
"presets": [["@babel/preset-env", { "modules": false }]],
|
||||
"plugins": [["@babel/plugin-transform-runtime", { "regenerator": true }]],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
.github/golang/Dockerfile
vendored
Normal file
6
.github/golang/Dockerfile
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM golang:1.13
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD [""]
|
||||
4
.github/golang/entrypoint.sh
vendored
Executable file
4
.github/golang/entrypoint.sh
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
go test -cover ./...
|
||||
13
.github/goreleaser/Dockerfile
vendored
Normal file
13
.github/goreleaser/Dockerfile
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM golang:1.13-alpine
|
||||
|
||||
RUN apk --no-cache add git nodejs-current nodejs-npm make g++ bash bzr curl docker rpm && \
|
||||
npm i -g npm && \
|
||||
wget https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz && \
|
||||
tar -xvzf *.tar.gz -C /usr/local/bin && \
|
||||
rm *.gz && \
|
||||
go get -u github.com/gobuffalo/packr/packr
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD [""]
|
||||
18
.github/goreleaser/entrypoint.sh
vendored
Executable file
18
.github/goreleaser/entrypoint.sh
vendored
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ]; then
|
||||
echo "Login to the docker..."
|
||||
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY
|
||||
fi
|
||||
|
||||
# Workaround for github actions when access to different repositories is needed.
|
||||
# Github actions provides a GITHUB_TOKEN secret that can only access the current
|
||||
# repository and you cannot configure it's value.
|
||||
# Access to different repositories is needed by brew for example.
|
||||
|
||||
if [ -n "$GORELEASER_GITHUB_TOKEN" ] ; then
|
||||
export GITHUB_TOKEN=$GORELEASER_GITHUB_TOKEN
|
||||
fi
|
||||
|
||||
npm ci
|
||||
goreleaser $@
|
||||
23
.github/workflows/push.yml
vendored
Normal file
23
.github/workflows/push.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
on: push
|
||||
name: Build, Test and Release
|
||||
jobs:
|
||||
build:
|
||||
name: npm test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: npm test
|
||||
uses: actions/setup-node@v1
|
||||
- run: npm it
|
||||
- name: go test
|
||||
uses: ./.github/golang/
|
||||
- name: Release
|
||||
uses: ./.github/goreleaser/
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REF: ${{ github. ref }}
|
||||
with:
|
||||
args: release
|
||||
if: contains(github.ref, 'tags')
|
||||
@@ -13,14 +13,14 @@ builds:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
archive:
|
||||
replacements:
|
||||
amd64: 64-bit
|
||||
386: 32-bit
|
||||
arm64: ARM_64-bit
|
||||
arm: ARM_32-bit
|
||||
linux: Linux
|
||||
darwin: Darwin
|
||||
archives:
|
||||
- replacements:
|
||||
amd64: 64-bit
|
||||
386: 32-bit
|
||||
arm64: ARM_64-bit
|
||||
arm: ARM_32-bit
|
||||
linux: Linux
|
||||
darwin: Darwin
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
@@ -36,3 +36,12 @@ dockers:
|
||||
- "amir20/dozzle:{{ .Tag }}"
|
||||
- "amir20/dozzle:v{{ .Major }}.{{ .Minor }}"
|
||||
- amir20/dozzle:latest
|
||||
build_flag_templates:
|
||||
- "--label=org.label-schema.schema-version=1.0"
|
||||
- "--label=org.label-schema.build-date={{.Date}}"
|
||||
- "--label=org.label-schema.vcs-ref={{.ShortCommit}}"
|
||||
- "--label=org.label-schema.version={{.Version}}"
|
||||
- "--label=org.label-schema.name=Dozzle"
|
||||
- "--label=org.label-schema.url=https://dozzle.dev/"
|
||||
- "--label=org.label-schema.vcs-url=https://github.com/amir20/dozzle"
|
||||
- "--label=org.label-schema.description=Dozzle is a real-time log viewer for docker containers."
|
||||
|
||||
1
.reflex
Normal file
1
.reflex
Normal file
@@ -0,0 +1 @@
|
||||
-r '\.go$' -R '^node_modules/' -R '^static/' -R '^.cache/' -G '*_test.go' -s -- go run main.go --level debug
|
||||
26
.travis.yml
26
.travis.yml
@@ -1,26 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.11"
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- nvm install --lts
|
||||
- npm i -g npm
|
||||
- npm ci
|
||||
- go get -u github.com/gobuffalo/packr/packr
|
||||
|
||||
after_success:
|
||||
# docker login is required if you want to push docker images.
|
||||
# DOCKER_PASSWORD should be a secret in your .travis.yml configuration.
|
||||
# - test -n "$TRAVIS_TAG" && docker login -u=myuser -p="$DOCKER_PASSWORD"
|
||||
|
||||
deploy:
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: curl -sL https://git.io/goreleaser | bash
|
||||
on:
|
||||
tags: true
|
||||
condition: $TRAVIS_OS_NAME = linux
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at findamir@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -7,4 +7,3 @@ ENV DOCKER_API_VERSION 1.38
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY dozzle /
|
||||
ENTRYPOINT ["/dozzle"]
|
||||
EXPOSE 8080
|
||||
76
README.md
76
README.md
@@ -1,12 +1,15 @@
|
||||
[](https://goreportcard.com/report/github.com/amir20/dozzle)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||
|
||||
# dozzle
|
||||
# Dozzle - [dozzle.dev](https://dozzle.dev/)
|
||||
|
||||
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?
|
||||
Dozzle is a real-time log viewer for Docker. It's free. It's small. And it's right in your browser.
|
||||
|
||||
While dozzle should work for most, it is not meant to be a full logging solution. For enterprise use, I recommend you look at [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana).
|
||||
|
||||
But if you don't want to pay for those services, then you are in luck! Dozzle will be able to capture all logs from your containers and send them in real-time to your browser. Installation is also very easy.
|
||||
But if you don't want to pay for those services, then you are in luck! Dozzle will be able to capture all logs from your containers and send them in real-time to your browser. Installation is also very easy. Dozzle is not a database. It does not store or save any logs. You can only see live logs while using Dozzle.
|
||||
|
||||

|
||||
|
||||
@@ -18,12 +21,35 @@ Dozzle is a very small Docker container (4 MB compressed). Pull the latest relea
|
||||
|
||||
## Using dozzle
|
||||
|
||||
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `-volume` to `/var/run/docker.sock`:
|
||||
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
|
||||
|
||||
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
|
||||
|
||||
dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`.
|
||||
|
||||
### With Docker swarm
|
||||
|
||||
docker service create \
|
||||
--name=dozzle \
|
||||
--publish=8888:8080 \
|
||||
--constraint=node.role==manager \
|
||||
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
amir20/dozzle:latest
|
||||
|
||||
### With Docker compose
|
||||
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
container_name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
environment:
|
||||
- DOZZLE_TAILSIZE=100
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 9999:8080
|
||||
|
||||
#### Security
|
||||
|
||||
dozzle doesn't support authentication out of the box. You can control the device dozzle binds to by passing `--addr` parameter. For example,
|
||||
@@ -32,28 +58,48 @@ 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 `--filter` parameter. For example,
|
||||
|
||||
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --filter name=foo
|
||||
|
||||
this would then only allow you to view containers with a name starting with "foo"
|
||||
|
||||
#### 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",
|
||||
then you can override by using `--base /foobar`.
|
||||
then you can override by using `--base /foobar`. See env variables below for using `DOZZLE_BASE` to change this.
|
||||
|
||||
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest --base /foobar
|
||||
|
||||
dozzle will be available at [http://localhost:8080/foobar/](http://localhost:8080/foobar/).
|
||||
|
||||
#### Environment variables and configuration
|
||||
|
||||
#### Environment variable, DOCKER_API_VERSION
|
||||
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or enviroment variables. The table below outlines all supported options and their respective env vars.
|
||||
|
||||
If you see
|
||||
|
||||
2018/10/31 08:53:17 Error response from daemon: client version 1.40 is too new. Maximum supported API version is 1.38
|
||||
|
||||
Then you need to modify `DOCKER_API_VERSION` to let dozzle know which version of the API is supported. By default, `DOCKER_API_VERSION=1.38` and you can change it by passing `-e` flag. For example, this would change the `DOCKER_API_VERSION` to `1.20`
|
||||
|
||||
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -e DOCKER_API_VERSION=1.20 -p 8888:8080 amir20/dozzle:latest
|
||||
|
||||
If you are not sure what to set `DOCKER_API_VERSION` then run `docker version` which will show supported API version.
|
||||
| Flag | Env Variable | Default |
|
||||
| ------------ | -------------------- | ------- |
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| `--showAll` | `DOZZLE_SHOWALL` | `false` |
|
||||
| n/a | `DOCKER_API_VERSION` | `1.38` |
|
||||
| `--tailSize` | `DOZZLE_TAILSIZE` | `300` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
|
||||
## 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 [here](.github/goreleaser/Dockerfile) which build and tests Dozzle.
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
/* snapshot: Test_createRoutes_foobar */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
foo page
|
||||
|
||||
/* snapshot: Test_createRoutes_index */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
index page
|
||||
|
||||
/* snapshot: Test_createRoutes_redirect */
|
||||
HTTP/1.1 301 Moved Permanently
|
||||
Connection: close
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Location: /foobar/
|
||||
|
||||
<a href="/foobar/">Moved Permanently</a>.
|
||||
|
||||
/* snapshot: Test_createRoutes_version */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
dev
|
||||
none
|
||||
unknown
|
||||
|
||||
/* snapshot: Test_handler_listContainers_happy */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
@@ -29,6 +60,14 @@ Content-Type: text/event-stream
|
||||
event: containers-changed
|
||||
data: start
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_error_finding_container */
|
||||
HTTP/1.1 500 Internal Server Error
|
||||
Connection: close
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
error finding container
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_error_reading */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
|
||||
50
assets/App.spec.js
Normal file
50
assets/App.spec.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import EventSource from "eventsourcemock";
|
||||
import { shallowMount, RouterLinkStub, createLocalVue } from "@vue/test-utils";
|
||||
import Vuex from "vuex";
|
||||
import App from "./App";
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe("<App />", () => {
|
||||
const stubs = { RouterLink: RouterLinkStub, "router-view": true };
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
global.BASE_PATH = "";
|
||||
global.EventSource = EventSource;
|
||||
const state = {
|
||||
containers: [
|
||||
{ id: "abc", name: "Test 1" },
|
||||
{ id: "xyz", name: "Test 2" }
|
||||
]
|
||||
};
|
||||
|
||||
const actions = {
|
||||
FETCH_CONTAINERS: () => Promise.resolve()
|
||||
};
|
||||
|
||||
store = new Vuex.Store({
|
||||
state,
|
||||
actions
|
||||
});
|
||||
});
|
||||
|
||||
test("is a Vue instance", async () => {
|
||||
const wrapper = shallowMount(App, { stubs, store, localVue });
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("has right title", async () => {
|
||||
const wrapper = shallowMount(App, { stubs, store, localVue });
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.title).toContain("2 containers");
|
||||
});
|
||||
|
||||
test("renders correctly", async () => {
|
||||
const wrapper = shallowMount(App, { stubs, store, localVue });
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
135
assets/App.vue
135
assets/App.vue
@@ -1,102 +1,89 @@
|
||||
<template lang="html">
|
||||
<div class="columns is-marginless">
|
||||
<aside class="column menu is-2-desktop is-3-tablet">
|
||||
<a
|
||||
role="button"
|
||||
class="navbar-burger burger is-white is-hidden-tablet is-pulled-right"
|
||||
@click="showNav = !showNav;"
|
||||
:class="{ 'is-active': showNav }"
|
||||
>
|
||||
<span></span> <span></span> <span></span>
|
||||
</a>
|
||||
<h1 class="title has-text-warning is-marginless">Dozzle</h1>
|
||||
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
|
||||
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
|
||||
<li v-for="item in containers">
|
||||
<router-link :to="{ name: 'container', params: { id: item.id, name: item.name } }" active-class="is-active">
|
||||
<div class="hide-overflow">{{ item.name }}</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="column is-offset-2-desktop is-offset-3-tablet"><router-view></router-view></div>
|
||||
<vue-headful :title="title" />
|
||||
<side-menu></side-menu>
|
||||
<div class="column is-offset-3-tablet is-offset-2-widescreen is-9-tablet is-10-widescreen is-paddingless">
|
||||
<splitpanes>
|
||||
<pane>
|
||||
<router-view></router-view>
|
||||
</pane>
|
||||
<pane v-for="other in activeContainers" :key="other.id">
|
||||
<scrollable-view>
|
||||
<template v-slot:header>
|
||||
<div class="name columns is-marginless">
|
||||
<span class="column">{{ other.name }}</span>
|
||||
<span class="column is-narrow">
|
||||
<button class="delete is-medium" @click="removeActiveContainer(other)"></button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<log-viewer-with-source :id="other.id"></log-viewer-with-source>
|
||||
</scrollable-view>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let es;
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
|
||||
import LogViewerWithSource from "./components/LogViewerWithSource";
|
||||
import ScrollableView from "./components/ScrollableView";
|
||||
import SideMenu from "./components/SideMenu";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
LogViewerWithSource,
|
||||
SideMenu,
|
||||
ScrollableView,
|
||||
Splitpanes,
|
||||
Pane
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: "Dozzle",
|
||||
containers: [],
|
||||
title: "",
|
||||
showNav: false
|
||||
};
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleTemplate: "%s - Dozzle"
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
await this.fetchContainerList();
|
||||
this.title = `${this.containers.length} containers - Dozzle`;
|
||||
es = new EventSource(`${BASE_PATH}/api/events/stream`);
|
||||
es.addEventListener("containers-changed", e => setTimeout(this.fetchContainerList, 1000), false);
|
||||
this.title = `${this.containers.length} containers`;
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
}
|
||||
computed: {
|
||||
...mapState(["containers", "activeContainers"])
|
||||
},
|
||||
methods: {
|
||||
async fetchContainerList() {
|
||||
this.containers = await (await fetch(`${BASE_PATH}/api/containers.json`)).json();
|
||||
...mapActions({
|
||||
fetchContainerList: "FETCH_CONTAINERS",
|
||||
removeActiveContainer: "REMOVE_ACTIVE_CONTAINER"
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.showNav = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.is-hidden-mobile.is-active {
|
||||
display: block !important;
|
||||
.name {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.navbar-burger {
|
||||
height: 2.35rem;
|
||||
}
|
||||
|
||||
aside {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
padding: 1em;
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
& {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
& {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hide-overflow {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.burger.is-white {
|
||||
color: #fff;
|
||||
::v-deep .splitpanes__splitter {
|
||||
min-width: 4px;
|
||||
background: #666;
|
||||
}
|
||||
</style>
|
||||
|
||||
26
assets/__snapshots__/App.spec.js.snap
Normal file
26
assets/__snapshots__/App.spec.js.snap
Normal file
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<App /> renders correctly 1`] = `
|
||||
<div
|
||||
class="columns is-marginless"
|
||||
>
|
||||
<side-menu-stub />
|
||||
|
||||
<div
|
||||
class="column is-offset-3-tablet is-offset-2-widescreen is-9-tablet is-10-widescreen is-paddingless"
|
||||
>
|
||||
<splitpanes-stub
|
||||
dblclicksplitter="true"
|
||||
pushotherpanes="true"
|
||||
>
|
||||
<pane-stub
|
||||
maxsize="100"
|
||||
minsize="0"
|
||||
>
|
||||
<router-view-stub />
|
||||
</pane-stub>
|
||||
|
||||
</splitpanes-stub>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
158
assets/components/LogEventSource.spec.js
Normal file
158
assets/components/LogEventSource.spec.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import EventSource from "eventsourcemock";
|
||||
import { sources } from "eventsourcemock";
|
||||
import { shallowMount, mount, createLocalVue } from "@vue/test-utils";
|
||||
import Vuex from "vuex";
|
||||
import MockDate from "mockdate";
|
||||
import LogEventSource from "./LogEventSource.vue";
|
||||
import LogViewer from "./LogViewer.vue";
|
||||
|
||||
describe("<LogEventSource />", () => {
|
||||
beforeEach(() => {
|
||||
global.BASE_PATH = "";
|
||||
global.EventSource = EventSource;
|
||||
MockDate.set("6/12/2019", 0);
|
||||
window.scrollTo = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => MockDate.reset());
|
||||
|
||||
function createLogEventSource(searchFilter = null) {
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
localVue.component("log-event-source", LogEventSource);
|
||||
localVue.component("log-viewer", LogViewer);
|
||||
|
||||
const state = { searchFilter };
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state
|
||||
});
|
||||
|
||||
return mount(LogEventSource, {
|
||||
localVue,
|
||||
store,
|
||||
scopedSlots: {
|
||||
default: `
|
||||
<log-viewer :messages="props.messages"></log-viewer>
|
||||
`
|
||||
},
|
||||
propsData: { id: "abc" }
|
||||
});
|
||||
}
|
||||
|
||||
test("is a Vue instance", async () => {
|
||||
const wrapper = shallowMount(LogEventSource);
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("renders correctly", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should connect to EventSource", async () => {
|
||||
shallowMount(LogEventSource);
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
expect(sources["/api/logs/stream?id=abc"].readyState).toBe(1);
|
||||
});
|
||||
|
||||
test("should close EventSource", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
wrapper.destroy();
|
||||
expect(sources["/api/logs/stream?id=abc"].readyState).toBe(2);
|
||||
});
|
||||
|
||||
test("should parse messages", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
|
||||
const [message, _] = wrapper.vm.messages;
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
expect(key).toBeGreaterThanOrEqual(0);
|
||||
|
||||
expect(messageWithoutKey).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"date": 2019-06-12T10:55:42.459Z,
|
||||
"message": " \\"This is a message.\\"",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test("should pass messages to slot", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
|
||||
const [message, _] = wrapper.find(LogViewer).vm.messages;
|
||||
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
expect(key).toBeGreaterThanOrEqual(0);
|
||||
|
||||
expect(messageWithoutKey).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"date": 2019-06-12T10:55:42.459Z,
|
||||
"message": " \\"This is a message.\\"",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test("should render messages", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
|
||||
|
||||
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
|
||||
<ul class="events">
|
||||
<li><span class="date">today at 10:55 AM</span> <span class="text"> "This is a message."</span></li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
test("should render messages with color", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`
|
||||
});
|
||||
|
||||
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
|
||||
<ul class="events">
|
||||
<li><span class="date">today at 10:55 AM</span> <span class="text"> <span style="color:#000">black<span style="color:#AAA">white</span></span></span></li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
test("should render messages with html entities", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`
|
||||
});
|
||||
|
||||
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
|
||||
<ul class="events">
|
||||
<li><span class="date">today at 10:55 AM</span> <span class="text"> <test>foo bar</test></span></li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
test("should render messages with filter", async () => {
|
||||
const wrapper = createLogEventSource("test");
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({
|
||||
data: `2019-06-11T10:55:42.459034602Z Foo bar`
|
||||
});
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`
|
||||
});
|
||||
|
||||
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
|
||||
<ul class="events">
|
||||
<li><span class="date">today at 10:55 AM</span> <span class="text"> This is a <mark>test</mark> <hi></hi></span></li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
});
|
||||
55
assets/components/LogEventSource.vue
Normal file
55
assets/components/LogEventSource.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<slot v-bind:messages="messages"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let nextId = 0;
|
||||
function parseMessage(data) {
|
||||
const date = new Date(data.substring(0, 30));
|
||||
const message = data.substring(30);
|
||||
const key = nextId++;
|
||||
return {
|
||||
key,
|
||||
date,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
props: ["id"],
|
||||
name: "LogEventSource",
|
||||
data() {
|
||||
return {
|
||||
messages: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.es = null;
|
||||
this.loadLogs(this.id);
|
||||
},
|
||||
methods: {
|
||||
loadLogs(id) {
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
this.messages = [];
|
||||
this.es = null;
|
||||
}
|
||||
this.es = new EventSource(`${BASE_PATH}/api/logs/stream?id=${this.id}`);
|
||||
this.es.onmessage = e => this.messages.push(parseMessage(e.data));
|
||||
this.es.onerror = function(e) {
|
||||
console.log("EventSource failed." + e);
|
||||
};
|
||||
this.$once("hook:beforeDestroy", () => this.es.close());
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
id(newValue, oldValue) {
|
||||
if (oldValue !== newValue) {
|
||||
this.loadLogs(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
94
assets/components/LogViewer.vue
Normal file
94
assets/components/LogViewer.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template lang="html">
|
||||
<ul class="events">
|
||||
<li v-for="item in filtered" :key="item.key">
|
||||
<span class="date">{{ item.date | relativeTime }}</span>
|
||||
<span class="text" v-html="colorize(item.message)"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
import { formatRelative } from "date-fns";
|
||||
import AnsiConvertor from "ansi-to-html";
|
||||
|
||||
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
|
||||
|
||||
export default {
|
||||
props: ["messages"],
|
||||
name: "LogViewer",
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
showSearch: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
colorize: function(value) {
|
||||
return ansiConvertor
|
||||
.toHtml(value)
|
||||
.replace("<mark>", "<mark>")
|
||||
.replace("</mark>", "</mark>");
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(["searchFilter"]),
|
||||
filtered() {
|
||||
const { searchFilter, messages } = this;
|
||||
if (searchFilter) {
|
||||
const isSmartCase = searchFilter === searchFilter.toLowerCase();
|
||||
const regex = isSmartCase ? new RegExp(searchFilter, "i") : new RegExp(searchFilter);
|
||||
return messages
|
||||
.filter(d => d.message.match(regex))
|
||||
.map(d => ({
|
||||
...d,
|
||||
message: d.message.replace(regex, "<mark>$&</mark>")
|
||||
}));
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
relativeTime(date) {
|
||||
return formatRelative(date, new Date());
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.events {
|
||||
padding: 10px;
|
||||
font-family: "Roboto Mono", monaco, monospace;
|
||||
|
||||
& > li {
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
background-color: #262626;
|
||||
color: #258ccd;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
>>> mark {
|
||||
border-radius: 2px;
|
||||
background-color: #ffdd57;
|
||||
animation: pops 0.2s ease-out;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes pops {
|
||||
0% {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
assets/components/LogViewerWithSource.vue
Normal file
19
assets/components/LogViewerWithSource.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template lang="html">
|
||||
<log-event-source :id="id" v-slot="eventSource">
|
||||
<log-viewer :messages="eventSource.messages"></log-viewer>
|
||||
</log-event-source>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LogEventSource from "./LogEventSource";
|
||||
import LogViewer from "./LogViewer";
|
||||
|
||||
export default {
|
||||
props: ["id"],
|
||||
name: "LogViewerWithSource",
|
||||
components: {
|
||||
LogEventSource,
|
||||
LogViewer
|
||||
}
|
||||
};
|
||||
</script>
|
||||
89
assets/components/ScrollableView.vue
Normal file
89
assets/components/ScrollableView.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template lang="html">
|
||||
<section>
|
||||
<header v-if="$slots.header">
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<main ref="content" @scroll.passive="onScroll">
|
||||
<slot></slot>
|
||||
</main>
|
||||
<div class="scroll-bar-notification">
|
||||
<transition name="fade">
|
||||
<button
|
||||
class="button"
|
||||
:class="hasMore ? 'is-warning' : 'is-primary'"
|
||||
@click="scrollToBottom('smooth')"
|
||||
v-show="paused"
|
||||
>
|
||||
<span class="icon large"> <i class="fas fa-chevron-down"></i> </span>
|
||||
</button>
|
||||
</transition>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ScrollableView",
|
||||
data() {
|
||||
return {
|
||||
paused: false,
|
||||
hasMore: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const { content } = this.$refs;
|
||||
new MutationObserver(e => {
|
||||
if (!this.paused) {
|
||||
this.scrollToBottom("instant");
|
||||
} else {
|
||||
this.hasMore = true;
|
||||
}
|
||||
}).observe(content, { childList: true, subtree: true });
|
||||
},
|
||||
methods: {
|
||||
scrollToBottom(behavior = "instant") {
|
||||
const { content } = this.$refs;
|
||||
if (typeof content.scroll === "function") {
|
||||
content.scroll({ top: content.scrollHeight, behavior });
|
||||
} else {
|
||||
content.scrollTop = content.scrollHeight;
|
||||
}
|
||||
this.hasMore = false;
|
||||
},
|
||||
onScroll(e) {
|
||||
const { content } = this.$refs;
|
||||
this.paused = content.scrollTop + content.clientHeight + 1 < content.scrollHeight;
|
||||
}
|
||||
},
|
||||
watch: {}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
.scroll-bar-notification {
|
||||
text-align: right;
|
||||
margin-right: 65px;
|
||||
button {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease-in;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template lang="html">
|
||||
<transition name="fade">
|
||||
<button
|
||||
class="button scroll-notification"
|
||||
:class="hasNew ? 'is-warning' : 'is-primary'"
|
||||
@click="scrollToBottom"
|
||||
v-show="visible"
|
||||
>
|
||||
<span class="icon large"> <i class="fas fa-chevron-down"></i> </span>
|
||||
</button>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["messages"],
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
hasNew: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("scroll", this.onScroll, { passive: true });
|
||||
setTimeout(() => this.scrollToBottom(), 500);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("scroll", this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
scrollToBottom() {
|
||||
this.visible = false;
|
||||
window.scrollTo(0, document.documentElement.scrollHeight || document.body.scrollHeight);
|
||||
},
|
||||
onScroll() {
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const scrollBottom =
|
||||
(document.documentElement.scrollHeight || document.body.scrollHeight) - document.documentElement.clientHeight;
|
||||
const diff = Math.abs(scrollTop - scrollBottom);
|
||||
this.visible = diff > 50;
|
||||
if (!this.visible) {
|
||||
this.hasNew = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
messages(newValue, oldValue) {
|
||||
if (this.visible) {
|
||||
this.hasNew = true;
|
||||
} else {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.scroll-notification {
|
||||
position: fixed;
|
||||
right: 40px;
|
||||
bottom: 30px;
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease-in;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
77
assets/components/Search.vue
Normal file
77
assets/components/Search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template lang="html">
|
||||
<div class="search columns is-gapless is-vcentered" v-show="showSearch">
|
||||
<div class="column">
|
||||
<p class="control has-icons-left">
|
||||
<input class="input" type="text" placeholder="Filter" ref="filter" v-model="filter" />
|
||||
<span class="icon is-small is-left"><i class="fas fa-search"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-1 has-text-centered">
|
||||
<button class="delete is-medium" @click="resetSearch()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from "vuex";
|
||||
export default {
|
||||
props: [],
|
||||
name: "Search",
|
||||
data() {
|
||||
return {
|
||||
showSearch: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
updateSearchFilter: "SET_SEARCH"
|
||||
}),
|
||||
onKeyDown(e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "f") {
|
||||
this.showSearch = true;
|
||||
this.$nextTick(() => this.$refs.filter.focus());
|
||||
e.preventDefault();
|
||||
} else if (e.key === "Escape") {
|
||||
this.resetSearch();
|
||||
}
|
||||
},
|
||||
resetSearch() {
|
||||
this.showSearch = false;
|
||||
this.filter = "";
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(["searchFilter"]),
|
||||
filter: {
|
||||
get() {
|
||||
return this.searchFilter;
|
||||
},
|
||||
set(value) {
|
||||
this.updateSearchFilter(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
width: 350px;
|
||||
position: fixed;
|
||||
padding: 10px;
|
||||
background: rgba(50, 50, 50, 0.9);
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 0 0 0 5px;
|
||||
z-index: 10;
|
||||
}
|
||||
.delete {
|
||||
margin-left: 1em;
|
||||
}
|
||||
</style>
|
||||
122
assets/components/SideMenu.vue
Normal file
122
assets/components/SideMenu.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template lang="html">
|
||||
<aside class="column menu is-3-tablet is-2-widescreen">
|
||||
<a
|
||||
role="button"
|
||||
class="navbar-burger burger is-white is-hidden-tablet is-pulled-right"
|
||||
@click="showNav = !showNav"
|
||||
:class="{ 'is-active': showNav }"
|
||||
>
|
||||
<span></span> <span></span> <span></span>
|
||||
</a>
|
||||
<h1 class="title has-text-warning is-marginless">Dozzle</h1>
|
||||
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
|
||||
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
|
||||
<li v-for="item in containers">
|
||||
<router-link :to="{ name: 'container', params: { id: item.id, name: item.name } }" active-class="is-active">
|
||||
<div class="hide-overflow">
|
||||
<span
|
||||
@click.stop.prevent="appendActiveContainer(item)"
|
||||
class="icon is-small will-append-container"
|
||||
:class="{ 'is-active': activeContainersById[item.id] }"
|
||||
>
|
||||
<i class="fas fa-thumbtack"></i>
|
||||
</span>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
props: [],
|
||||
name: "SideMenu",
|
||||
data() {
|
||||
return {
|
||||
showNav: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
colorize: value =>
|
||||
ansiConvertor
|
||||
.toHtml(value)
|
||||
.replace("<mark>", "<mark>")
|
||||
.replace("</mark>", "</mark>")
|
||||
},
|
||||
computed: {
|
||||
...mapState(["containers", "activeContainers"]),
|
||||
activeContainersById() {
|
||||
return this.activeContainers.reduce((map, obj) => {
|
||||
map[obj.id] = obj;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
appendActiveContainer: "APPEND_ACTIVE_CONTAINER"
|
||||
})
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
aside {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
padding: 1em;
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
& {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
& {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
.hide-overflow {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.burger.is-white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.is-hidden-mobile.is-active {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.navbar-burger {
|
||||
height: 2.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
.will-append-container.icon {
|
||||
transition: transform 0.2s ease-out;
|
||||
&.is-active {
|
||||
transform: rotate(25deg);
|
||||
pointer-events: none;
|
||||
color: #00d1b2;
|
||||
}
|
||||
.router-link-exact-active & {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LogEventSource /> renders correctly 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="events"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -7,6 +7,7 @@
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono|Gafata" rel="stylesheet" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link href="styles.scss" rel="stylesheet" />
|
||||
<link rel="icon" href="favicon.ico">
|
||||
<script>
|
||||
window["BASE_PATH"] = "{{ .Base }}";
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import vueHeadful from "vue-headful";
|
||||
import Meta from "vue-meta";
|
||||
import Vuex from "vuex";
|
||||
import store from "./store";
|
||||
import App from "./App.vue";
|
||||
import Container from "./pages/Container.vue";
|
||||
import Index from "./pages/Index.vue";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
Vue.component("vue-headful", vueHeadful);
|
||||
Vue.use(Meta);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -30,5 +32,6 @@ const router = new VueRouter({
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount("#app");
|
||||
|
||||
@@ -1,94 +1,31 @@
|
||||
<template lang="html">
|
||||
<div class="is-fullheight">
|
||||
<ul ref="events" class="events">
|
||||
<li v-for="item in messages" class="event" :key="item.key">
|
||||
<span class="date">{{ item.dateRelative }}</span> <span class="text">{{ item.message }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<scrollbar-notification :messages="messages"></scrollbar-notification>
|
||||
<vue-headful :title="title" />
|
||||
<div>
|
||||
<search></search>
|
||||
<scrollable-view>
|
||||
<log-viewer-with-source :id="id"></log-viewer-with-source>
|
||||
</scrollable-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatRelative } from "date-fns";
|
||||
import ScrollbarNotification from "../components/ScrollbarNotification";
|
||||
|
||||
let es = null;
|
||||
let nextId = 0;
|
||||
const parseMessage = data => {
|
||||
const date = new Date(data.substring(0, 30));
|
||||
const dateRelative = formatRelative(date, new Date());
|
||||
const message = data.substring(30);
|
||||
const key = nextId++;
|
||||
return {
|
||||
key,
|
||||
date,
|
||||
dateRelative,
|
||||
message
|
||||
};
|
||||
};
|
||||
import LogViewerWithSource from "../components/LogViewerWithSource";
|
||||
import Search from "../components/Search";
|
||||
import ScrollableView from "../components/ScrollableView";
|
||||
|
||||
export default {
|
||||
props: ["id", "name"],
|
||||
name: "Container",
|
||||
components: {
|
||||
ScrollbarNotification
|
||||
LogViewerWithSource,
|
||||
ScrollableView,
|
||||
Search
|
||||
},
|
||||
data() {
|
||||
metaInfo() {
|
||||
return {
|
||||
messages: [],
|
||||
title: ""
|
||||
title: this.name,
|
||||
titleTemplate: "%s - Dozzle"
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.loadLogs(this.id);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
id(newValue, oldValue) {
|
||||
if (oldValue !== newValue) {
|
||||
this.loadLogs(newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadLogs(id) {
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
this.messages = [];
|
||||
}
|
||||
es = new EventSource(`${BASE_PATH}/api/logs/stream?id=${id}`);
|
||||
es.onmessage = e => this.messages.push(parseMessage(e.data));
|
||||
this.title = `${this.name} - Dozzle`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.events {
|
||||
padding: 10px;
|
||||
font-family: "Roboto Mono", monaco, monospace;
|
||||
}
|
||||
|
||||
.event {
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.date {
|
||||
background-color: #262626;
|
||||
color: #258ccd;
|
||||
}
|
||||
|
||||
.is-fullheight {
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
14
assets/pages/Index.spec.js
Normal file
14
assets/pages/Index.spec.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import Index from "./Index";
|
||||
|
||||
describe("<Index />", () => {
|
||||
test("is a Vue instance", () => {
|
||||
const wrapper = shallowMount(Index);
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("renders correctly", () => {
|
||||
const wrapper = shallowMount(Index);
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
21
assets/pages/__snapshots__/Index.spec.js.snap
Normal file
21
assets/pages/__snapshots__/Index.spec.js.snap
Normal file
@@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Index /> renders correctly 1`] = `
|
||||
<div
|
||||
class="hero is-fullheight is-dark"
|
||||
>
|
||||
<div
|
||||
class="hero-body"
|
||||
>
|
||||
<div
|
||||
class="container has-text-centered"
|
||||
>
|
||||
<h1
|
||||
class="title"
|
||||
>
|
||||
Please choose a container from the list to view the logs
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
56
assets/store/index.js
Normal file
56
assets/store/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
// import storage from "store/dist/store.modern";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const state = {
|
||||
containers: [],
|
||||
activeContainers: [],
|
||||
searchFilter: null
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
SET_CONTAINERS(state, containers) {
|
||||
state.containers = containers;
|
||||
},
|
||||
ADD_ACTIVE_CONTAINERS(state, container) {
|
||||
state.activeContainers.push(container);
|
||||
},
|
||||
REMOVE_ACTIVE_CONTAINER(state, container) {
|
||||
state.activeContainers.splice(state.activeContainers.indexOf(container), 1);
|
||||
},
|
||||
SET_SEARCH(state, filter) {
|
||||
state.searchFilter = filter;
|
||||
}
|
||||
};
|
||||
|
||||
const actions = {
|
||||
APPEND_ACTIVE_CONTAINER({ commit }, container) {
|
||||
commit("ADD_ACTIVE_CONTAINERS", container);
|
||||
},
|
||||
REMOVE_ACTIVE_CONTAINER({ commit }, container) {
|
||||
commit("REMOVE_ACTIVE_CONTAINER", container);
|
||||
},
|
||||
SET_SEARCH({ commit }, filter) {
|
||||
commit("SET_SEARCH", filter);
|
||||
},
|
||||
async FETCH_CONTAINERS({ commit }) {
|
||||
const containers = await (await fetch(`${BASE_PATH}/api/containers.json`)).json();
|
||||
commit("SET_CONTAINERS", containers);
|
||||
}
|
||||
};
|
||||
const getters = {};
|
||||
|
||||
const es = new EventSource(`${BASE_PATH}/api/events/stream`);
|
||||
es.addEventListener("containers-changed", e => setTimeout(() => store.dispatch("FETCH_CONTAINERS"), 1000), false);
|
||||
|
||||
const store = new Vuex.Store({
|
||||
strict: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
});
|
||||
|
||||
export default store;
|
||||
@@ -4,16 +4,21 @@ $menu-item-active-background-color: hsl(171, 100%, 41%);
|
||||
$menu-item-color: hsl(0, 6%, 87%);
|
||||
|
||||
@import "../node_modules/bulma/bulma.sass";
|
||||
@import "../node_modules/splitpanes/dist/splitpanes.css";
|
||||
|
||||
.is-dark {
|
||||
color: #ddd;
|
||||
background-color: #111;
|
||||
color: #ddd;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
|
||||
h1.title {
|
||||
font-family: "Gafata", sans-serif;
|
||||
font-family: "Gafata", sans-serif;
|
||||
}
|
||||
|
||||
.column {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
149
docker/client.go
149
docker/client.go
@@ -1,46 +1,94 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"strings"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type dockerClient struct {
|
||||
cli dockerProxy
|
||||
cli dockerProxy
|
||||
filters filters.Args
|
||||
}
|
||||
|
||||
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)
|
||||
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
|
||||
}
|
||||
|
||||
// Client is a proxy around the docker client
|
||||
type Client interface {
|
||||
ListContainers() ([]Container, error)
|
||||
ContainerLogs(ctx context.Context, id string) (<-chan string, <-chan error)
|
||||
Events(ctx context.Context) (<-chan events.Message, <-chan error)
|
||||
ListContainers(showAll bool) ([]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 {
|
||||
return NewClientWithFilters(map[string]string{})
|
||||
}
|
||||
|
||||
// NewClientWithFilters creates a new instance of Client with docker filters
|
||||
func NewClientWithFilters(f map[string]string) Client {
|
||||
filterArgs := filters.NewArgs()
|
||||
for k, v := range f {
|
||||
filterArgs.Add(k, v)
|
||||
}
|
||||
|
||||
log.Debugf("filterArgs = %v", filterArgs)
|
||||
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return &dockerClient{cli}
|
||||
|
||||
return &dockerClient{cli, filterArgs}
|
||||
}
|
||||
|
||||
func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
list, err := d.cli.ContainerList(context.Background(), types.ContainerListOptions{})
|
||||
func (d *dockerClient) FindContainer(id string) (Container, error) {
|
||||
var container Container
|
||||
containers, err := d.ListContainers(true)
|
||||
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(showAll bool) ([]Container, error) {
|
||||
containerListOptions := types.ContainerListOptions{
|
||||
Filters: d.filters,
|
||||
All: showAll,
|
||||
}
|
||||
list, err := d.cli.ContainerList(context.Background(), containerListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -73,49 +121,70 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int) (<-chan string, <-chan error) {
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: strconv.Itoa(tailSize), Timestamps: true}
|
||||
reader, err := d.cli.ContainerLogs(ctx, id, options)
|
||||
errChannel := make(chan error, 1)
|
||||
|
||||
if err != nil {
|
||||
tmpErrors := make(chan error, 1)
|
||||
tmpErrors <- err
|
||||
return nil, tmpErrors
|
||||
errChannel <- err
|
||||
close(errChannel)
|
||||
return nil, errChannel
|
||||
}
|
||||
|
||||
messages := make(chan string)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
reader.Close()
|
||||
}()
|
||||
|
||||
messages := make(chan string)
|
||||
errChannel := make(chan error)
|
||||
containerJSON, _ := d.cli.ContainerInspect(ctx, id)
|
||||
|
||||
go func() {
|
||||
hdr := make([]byte, 8)
|
||||
var buffer bytes.Buffer
|
||||
for {
|
||||
_, err := reader.Read(hdr)
|
||||
if err != nil {
|
||||
errChannel <- err
|
||||
break
|
||||
if containerJSON.Config.Tty {
|
||||
go func() {
|
||||
defer close(messages)
|
||||
defer close(errChannel)
|
||||
defer reader.Close()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case messages <- line:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
count := binary.BigEndian.Uint32(hdr[4:])
|
||||
_, err = io.CopyN(&buffer, reader, int64(count))
|
||||
if err != nil {
|
||||
errChannel <- err
|
||||
break
|
||||
}()
|
||||
} else {
|
||||
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
|
||||
}
|
||||
select {
|
||||
case messages <- buffer.String():
|
||||
case <-ctx.Done():
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
messages <- buffer.String()
|
||||
buffer.Reset()
|
||||
}
|
||||
close(messages)
|
||||
close(errChannel)
|
||||
reader.Close()
|
||||
}()
|
||||
}()
|
||||
}
|
||||
|
||||
return messages, errChannel
|
||||
|
||||
}
|
||||
|
||||
func (d *dockerClient) Events(ctx context.Context) (<-chan events.Message, <-chan error) {
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockedProxy struct {
|
||||
@@ -24,12 +31,31 @@ func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions)
|
||||
return containers, args.Error(1)
|
||||
|
||||
}
|
||||
func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
proxy := mockedProxy{}
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
client := &dockerClient{&proxy}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
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 (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
args := m.Called(ctx, containerID)
|
||||
json, ok := args.Get(0).(types.ContainerJSON)
|
||||
if !ok && args.Get(0) != nil {
|
||||
panic("proxies return value is not of type types.ContainerJSON")
|
||||
}
|
||||
|
||||
return json, args.Error(1)
|
||||
}
|
||||
|
||||
func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
list, err := client.ListContainers(true)
|
||||
assert.Empty(t, list, "list should be empty")
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
|
||||
@@ -37,11 +63,11 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_dockerClient_ListContainers_error(t *testing.T) {
|
||||
proxy := mockedProxy{}
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
|
||||
client := &dockerClient{&proxy}
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
list, err := client.ListContainers(true)
|
||||
assert.Nil(t, list, "list should be nil")
|
||||
require.Error(t, err, "test.")
|
||||
|
||||
@@ -60,11 +86,11 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
proxy := mockedProxy{}
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &dockerClient{&proxy}
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
list, err := client.ListContainers(true)
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
|
||||
assert.Equal(t, list, []Container{
|
||||
@@ -82,3 +108,123 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
|
||||
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)...)
|
||||
|
||||
reader := ioutil.NopCloser(bytes.NewReader(b))
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
|
||||
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
messages, _ := client.ContainerLogs(context.Background(), id, 300)
|
||||
|
||||
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_happy_with_tty(t *testing.T) {
|
||||
id := "123456"
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
expected := "INFO Testing logs..."
|
||||
|
||||
reader := ioutil.NopCloser(bytes.NewReader([]byte(expected)))
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
json := types.ContainerJSON{Config: &container.Config{Tty: true}}
|
||||
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
messages, _ := client.ContainerLogs(context.Background(), id, 300)
|
||||
|
||||
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, filters.NewArgs()}
|
||||
|
||||
messages, err := client.ContainerLogs(context.Background(), id, 300)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func Test_dockerClient_FindContainer_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, filters.NewArgs()}
|
||||
|
||||
container, err := client.FindContainer("abcdefghijkl")
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
|
||||
assert.Equal(t, container, Container{
|
||||
ID: "abcdefghijkl",
|
||||
Name: "z_test_container",
|
||||
Names: []string{"/z_test_container"},
|
||||
})
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
func Test_dockerClient_FindContainer_error(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, filters.NewArgs()}
|
||||
|
||||
_, err := client.FindContainer("not_valid")
|
||||
require.Error(t, err, "error should be thrown")
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
|
||||
48
go.mod
Normal file
48
go.mod
Normal file
@@ -0,0 +1,48 @@
|
||||
module github.com/amir20/dozzle
|
||||
|
||||
replace github.com/docker/docker v0.0.0-20190827232753-32688a47f341 => github.com/docker/engine v0.0.0-20190827232753-32688a47f341
|
||||
|
||||
// github.com/docker/engine v19.06.1-ce
|
||||
replace github.com/docker/docker => github.com/docker/engine v0.0.0-20190827232753-32688a47f341
|
||||
|
||||
// github.com/docker/distribution master
|
||||
// a proper tagged release is expected in early fall(September 2018)
|
||||
// see; https://github.com/docker/distribution/issues/2693
|
||||
replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20190711223531-1fb7fffdb266
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/containerd/containerd v1.2.9 // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v0.0.0-20190827232753-32688a47f341
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/gobuffalo/envy v1.7.1 // indirect
|
||||
github.com/gobuffalo/packr v1.30.1
|
||||
github.com/gogo/protobuf v1.3.0 // indirect
|
||||
github.com/google/go-cmp v0.3.1 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/magiconair/properties v1.8.1
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.5.0
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/net v0.0.0-20190918130420-a8b05e9114ab // indirect
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20190916214212-f660b8655731 // indirect
|
||||
google.golang.org/grpc v1.23.1 // indirect
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
)
|
||||
|
||||
go 1.13
|
||||
321
go.sum
Normal file
321
go.sum
Normal file
@@ -0,0 +1,321 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760 h1:FvTM5NSN5HYvfKpgL+8x73U5v063vHsd7AX05eV1DnM=
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ=
|
||||
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/containerd v1.2.9 h1:6tyNjBmAMG47QuFPIT9LgiiexoVxC6qpTGR+eD0R0Z8=
|
||||
github.com/containerd/containerd v1.2.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/distribution v0.0.0-20190711223531-1fb7fffdb266 h1:6BCth6L0iZKTU3F0OxqlkECwdmUDLbHdD9qz6HXlpb4=
|
||||
github.com/docker/distribution v0.0.0-20190711223531-1fb7fffdb266/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
|
||||
github.com/docker/engine v0.0.0-20190827232753-32688a47f341 h1:EZsx4y4IdfCZofMwt/ICb/8P5TgSR69Zrnw21vOHKc0=
|
||||
github.com/docker/engine v0.0.0-20190827232753-32688a47f341/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
|
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
|
||||
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
|
||||
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
||||
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
|
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
||||
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
|
||||
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
|
||||
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE=
|
||||
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.2 h1:XU784Pr0wdahMY2bYcyK6N1KuaRAdLtqD4qd8D18Bfs=
|
||||
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4=
|
||||
github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
|
||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190918130420-a8b05e9114ab h1:h5tBRKZ1aY/bo6GNqe/4zWC8GkaLOFQ5wPKIOQ0i2sA=
|
||||
golang.org/x/net v0.0.0-20190918130420-a8b05e9114ab/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438 h1:khxRGsvPk4n2y8I/mLLjp7e5dMTJmH75wvqS6nMwUtY=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190916214212-f660b8655731 h1:Phvl0+G5t5k/EUFUi0wPdUUeTL2HydMQUXHnunWgSb0=
|
||||
google.golang.org/genproto v0.0.0-20190916214212-f660b8655731/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
130
main.go
130
main.go
@@ -1,37 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = ""
|
||||
base = ""
|
||||
level = ""
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
date = "unknown"
|
||||
addr = ""
|
||||
base = ""
|
||||
level = ""
|
||||
showAll = false
|
||||
tailSize = 300
|
||||
filters map[string]string
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
client docker.Client
|
||||
showAll bool
|
||||
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()
|
||||
pflag.String("addr", ":8080", "http service address")
|
||||
pflag.String("base", "/", "base address of the application to mount")
|
||||
pflag.Bool("showAll", false, "show all containers, even stopped")
|
||||
pflag.String("level", "info", "logging level")
|
||||
pflag.Int("tailSize", 300, "Tail size to use for initial container logs")
|
||||
pflag.StringToStringVar(&filters, "filter", map[string]string{}, "Container filters to use for showing logs")
|
||||
pflag.Parse()
|
||||
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvPrefix("DOZZLE")
|
||||
viper.BindPFlags(pflag.CommandLine)
|
||||
|
||||
addr = viper.GetString("addr")
|
||||
base = viper.GetString("base")
|
||||
level = viper.GetString("level")
|
||||
tailSize = viper.GetInt("tailSize")
|
||||
showAll = viper.GetBool("showAll")
|
||||
|
||||
// Until https://github.com/spf13/viper/issues/608 is fixed. We have to use this hacky way.
|
||||
// filters = viper.GetStringSlice("filter")
|
||||
if value, ok := os.LookupEnv("DOZZLE_FILTER"); ok {
|
||||
log.Infof("Parsing %s", value)
|
||||
urlValues, err := url.ParseQuery(strings.ReplaceAll(value, ",", "&"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
filters = map[string]string{}
|
||||
for k, v := range urlValues {
|
||||
filters[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
l, _ := log.ParseLevel(level)
|
||||
log.SetLevel(l)
|
||||
@@ -42,34 +81,55 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
dockerClient := docker.NewClient()
|
||||
_, err := dockerClient.ListContainers()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to Docker Engine: %v", err)
|
||||
}
|
||||
|
||||
box := packr.NewBox("./static")
|
||||
h := &handler{dockerClient, box}
|
||||
|
||||
func createRoutes(base string, h *handler) *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
|
||||
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)))
|
||||
return r
|
||||
}
|
||||
|
||||
log.Infof("Accepting connections on %s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, r))
|
||||
func main() {
|
||||
log.Infof("Dozzle version %s", version)
|
||||
dockerClient := docker.NewClientWithFilters(filters)
|
||||
_, err := dockerClient.ListContainers(true)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to Docker Engine: %v", err)
|
||||
}
|
||||
|
||||
box := packr.NewBox("./static")
|
||||
r := createRoutes(base, &handler{
|
||||
client: dockerClient,
|
||||
showAll: showAll,
|
||||
box: box,
|
||||
})
|
||||
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 (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -98,7 +158,7 @@ func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func (h *handler) listContainers(w http.ResponseWriter, r *http.Request) {
|
||||
containers, err := h.client.ListContainers()
|
||||
containers, err := h.client.ListContainers(h.showAll)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -123,7 +183,13 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := h.client.ContainerLogs(r.Context(), id)
|
||||
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")
|
||||
@@ -149,6 +215,8 @@ Loop:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
|
||||
log.WithField("NumGoroutine", runtime.NumGoroutine()).Debug("runtime stats")
|
||||
}
|
||||
|
||||
func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
129
main_test.go
129
main_test.go
@@ -3,14 +3,17 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"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"
|
||||
"github.com/gobuffalo/packr"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -20,7 +23,16 @@ type MockedClient struct {
|
||||
docker.Client
|
||||
}
|
||||
|
||||
func (m *MockedClient) ListContainers() ([]docker.Container, error) {
|
||||
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(showAll bool) ([]docker.Container, error) {
|
||||
args := m.Called()
|
||||
containers, ok := args.Get(0).([]docker.Container)
|
||||
if !ok {
|
||||
@@ -29,8 +41,8 @@ func (m *MockedClient) ListContainers() ([]docker.Container, error) {
|
||||
return containers, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerLogs(ctx context.Context, id string) (<-chan string, <-chan error) {
|
||||
args := m.Called(ctx, id)
|
||||
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, tailSize int) (<-chan string, <-chan error) {
|
||||
args := m.Called(ctx, id, tailSize)
|
||||
channel, ok := args.Get(0).(chan string)
|
||||
if !ok {
|
||||
panic("channel is not of type chan string")
|
||||
@@ -89,17 +101,16 @@ 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")
|
||||
q.Add("id", id)
|
||||
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)
|
||||
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)
|
||||
@@ -107,6 +118,26 @@ func Test_handler_streamLogs_happy(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_handler_streamLogs_error_finding_container(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container"))
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -116,15 +147,15 @@ 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")
|
||||
q.Add("id", id)
|
||||
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)
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, id, 300).Return(messages, errChannel)
|
||||
|
||||
go func() {
|
||||
errChannel <- errors.New("test error")
|
||||
@@ -132,6 +163,7 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -140,7 +172,6 @@ func Test_handler_streamLogs_error_reading(t *testing.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)
|
||||
@@ -158,6 +189,7 @@ func Test_handler_streamEvents_happy(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -166,7 +198,6 @@ func Test_handler_streamEvents_happy(t *testing.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)
|
||||
@@ -179,6 +210,7 @@ func Test_handler_streamEvents_error(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -188,8 +220,6 @@ 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)
|
||||
@@ -205,11 +235,80 @@ func Test_handler_streamEvents_error_request(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_createRoutes_index(t *testing.T) {
|
||||
mockedClient := new(MockedClient)
|
||||
box := packr.NewBox("./virtual")
|
||||
require.NoError(t, box.AddString("index.html", "index page"), "AddString should have no error.")
|
||||
|
||||
handler := createRoutes("/", &handler{mockedClient, true, box})
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
}
|
||||
|
||||
func Test_createRoutes_redirect(t *testing.T) {
|
||||
mockedClient := new(MockedClient)
|
||||
box := packr.NewBox("./virtual")
|
||||
|
||||
handler := createRoutes("/foobar", &handler{mockedClient, true,box})
|
||||
req, err := http.NewRequest("GET", "/foobar", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
}
|
||||
|
||||
func Test_createRoutes_foobar(t *testing.T) {
|
||||
mockedClient := new(MockedClient)
|
||||
box := packr.NewBox("./virtual")
|
||||
require.NoError(t, box.AddString("index.html", "foo page"), "AddString should have no error.")
|
||||
|
||||
handler := createRoutes("/foobar", &handler{mockedClient, true, box})
|
||||
req, err := http.NewRequest("GET", "/foobar/", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
}
|
||||
|
||||
func Test_createRoutes_foobar_file(t *testing.T) {
|
||||
mockedClient := new(MockedClient)
|
||||
box := packr.NewBox("./virtual")
|
||||
require.NoError(t, box.AddString("/test", "test page"), "AddString should have no error.")
|
||||
|
||||
handler := createRoutes("/foobar", &handler{mockedClient, true, box})
|
||||
req, err := http.NewRequest("GET", "/foobar/test", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, rr.Body.String(), "test page", "page doesn't match")
|
||||
}
|
||||
|
||||
func Test_createRoutes_version(t *testing.T) {
|
||||
mockedClient := new(MockedClient)
|
||||
box := packr.NewBox("./virtual")
|
||||
|
||||
handler := createRoutes("/", &handler{mockedClient, true, box})
|
||||
req, err := http.NewRequest("GET", "/version", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
exit := m.Run()
|
||||
abide.Cleanup()
|
||||
|
||||
9309
package-lock.json
generated
9309
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
91
package.json
91
package.json
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "1.4.4",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"version": "1.17.0",
|
||||
"description": "Realtime log viewer for docker containers. ",
|
||||
"scripts": {
|
||||
"prestart": "npm run clean",
|
||||
"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 -g '**/*.go' -R '^node_modules/' -R '^static/' -R '^.cache/' -G '*_test.go' -s -- go run main.go --level debug",
|
||||
"watch-assets": "npx 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",
|
||||
"build": "npx parcel build --no-source-maps --public-url '__BASE__' assets/index.html -d static",
|
||||
"clean": "rm -rf static/ a_main-packr.go",
|
||||
"release": "goreleaser --rm-dist"
|
||||
"release": "goreleaser --rm-dist",
|
||||
"test": "jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,24 +24,37 @@
|
||||
},
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"dependencies": {
|
||||
"bulma": "^0.7.2",
|
||||
"date-fns": "^2.0.0-alpha.25",
|
||||
"vue": "^2.5.17",
|
||||
"vue-headful": "^2.0.1",
|
||||
"vue-router": "^3.0.2"
|
||||
"ansi-to-html": "^0.6.13",
|
||||
"bulma": "^0.8.0",
|
||||
"date-fns": "^2.8.1",
|
||||
"splitpanes": "^2.1.1",
|
||||
"store": "^2.0.12",
|
||||
"vue": "^2.6.10",
|
||||
"vue-meta": "^2.3.1",
|
||||
"vue-router": "^3.1.3",
|
||||
"vuex": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.6",
|
||||
"@babel/plugin-transform-runtime": "^7.1.0",
|
||||
"@vue/component-compiler-utils": "^2.3.0",
|
||||
"concurrently": "^4.1.0",
|
||||
"husky": "^1.2.0",
|
||||
"lint-staged": "^8.1.0",
|
||||
"parcel-bundler": "^1.10.3",
|
||||
"prettier": "^1.15.2",
|
||||
"sass": "^1.15.1",
|
||||
"vue-hot-reload-api": "^2.3.1",
|
||||
"vue-template-compiler": "^2.5.17"
|
||||
"@babel/core": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.4",
|
||||
"@vue/component-compiler-utils": "^3.0.2",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
"concurrently": "^5.0.0",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"husky": "^3.1.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"lint-staged": "^9.4.3",
|
||||
"mockdate": "^2.0.5",
|
||||
"node-fetch": "^2.6.0",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^1.19.1",
|
||||
"sass": "^1.23.7",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@@ -57,5 +70,35 @@
|
||||
"browserslist": [
|
||||
">5%",
|
||||
"not ie <= 8"
|
||||
]
|
||||
],
|
||||
"alias": {
|
||||
"vue": "./node_modules/vue/dist/vue.esm.js"
|
||||
},
|
||||
"jest": {
|
||||
"clearMocks": true,
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"vue"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"node_modules"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"node_modules"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules"
|
||||
],
|
||||
"watchPathIgnorePatterns": [
|
||||
"<rootDir>/node_modules/"
|
||||
],
|
||||
"snapshotSerializers": [
|
||||
"jest-serializer-vue"
|
||||
],
|
||||
"transform": {
|
||||
".*\\.vue$": "vue-jest",
|
||||
".+\\.js$": "babel-jest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user