mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-16 15:10:12 +01:00
Merge branch 'main' into all-contributors/add-andriibratanin
This commit is contained in:
commit
678955eefe
47 changed files with 1948 additions and 628 deletions
|
|
@ -846,6 +846,12 @@
|
|||
"name": "Andrii Bratanin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20169213?v=4",
|
||||
"profile": "https://github.com/andriibratanin",
|
||||
},
|
||||
{
|
||||
"login": "IAmTamal",
|
||||
"name": "Tamal Das ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/72851613?v=4",
|
||||
"profile": "https://tamal.vercel.app/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
|
|
|
|||
14
.editorconfig
Normal file
14
.editorconfig
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
|
||||
[*.css]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{go.mod,go.sum,*.go}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,2 +0,0 @@
|
|||
custom: https://www.amazon.com/hz/wishlist/ls/F94JJV822VX6
|
||||
github: simskij
|
||||
71
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
71
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
name: 🐛 Bug report
|
||||
description: Create a report to help us improve
|
||||
labels: ["Priority: Medium, Status: Available, Type: Bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting your issue, please make sure you're using the containrrr/watchtower:latest image. If not, switch to this image prior to posting your report. Other forks, or the old `v2tec` image are **not** supported.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please add screenshots if applicable
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: We would want to know the following things
|
||||
value: |
|
||||
- Platform
|
||||
- Architecture
|
||||
- Docker Version
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Your logs
|
||||
description: Paste the logs from running watchtower with the `--debug` option.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
53
.github/ISSUE_TEMPLATE/bug_report.md
vendored
53
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'Priority: Medium, Status: Available, Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!--
|
||||
Before submitting your issue, please make sure you're using the containrrr/watchtower:latest image.
|
||||
If not, switch to this image prior to posting your report. Other forks, or the old `v2tec` image are **not** supported.
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!--
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
-->
|
||||
**Environment**
|
||||
<!--
|
||||
We want to know:
|
||||
- Platform
|
||||
- Architecture
|
||||
- Docker version
|
||||
-->
|
||||
|
||||
<details>
|
||||
<summary><b> Logs from running watchtower with the <code>--debug</code> option </b></summary>
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**Additional context**
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
-->
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Priority: Low, Status: Available, Type: Enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
name: 💡 Feature request
|
||||
description: Have a new idea/feature ? Please suggest!
|
||||
labels: ["Priority: Low, Status: Available, Type: Enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: extrainfo
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
7
.github/workflows/greetings.yml
vendored
7
.github/workflows/greetings.yml
vendored
|
|
@ -1,10 +1,11 @@
|
|||
name: Greetings
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
# Runs in the context of the target (containrrr/watchtower) repository, and as such has access to GITHUB_TOKEN
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
|
|
|
|||
3
.github/workflows/publish-docs.yml
vendored
3
.github/workflows/publish-docs.yml
vendored
|
|
@ -17,13 +17,14 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mkdocs
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: |
|
||||
docs-requirements.txt
|
||||
- name: Install mkdocs
|
||||
run: |
|
||||
pip install -r docs-requirements.txt
|
||||
- name: Generate docs
|
||||
|
|
|
|||
5
.github/workflows/pull-request.yml
vendored
5
.github/workflows/pull-request.yml
vendored
|
|
@ -19,9 +19,10 @@ jobs:
|
|||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- uses: dominikh/staticcheck-action@v1.2.0
|
||||
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||
with:
|
||||
version: "2022.1.1"
|
||||
install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
|
|
@ -63,7 +64,7 @@ jobs:
|
|||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b #v3
|
||||
with:
|
||||
version: v0.155.0
|
||||
args: --snapshot --skip-publish --debug
|
||||
|
|
|
|||
4
.github/workflows/release-dev.yaml
vendored
4
.github/workflows/release-dev.yaml
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Publish to Docker Hub
|
||||
uses: jerray/publish-docker-action@master
|
||||
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
repository: containrrr/watchtower
|
||||
tags: latest-dev
|
||||
- name: Publish to GHCR
|
||||
uses: jerray/publish-docker-action@master
|
||||
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
|
||||
with:
|
||||
username: ${{ secrets.BOT_USERNAME }}
|
||||
password: ${{ secrets.BOT_GHCR_PAT }}
|
||||
|
|
|
|||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
|
|
@ -22,12 +22,10 @@ jobs:
|
|||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Install linter
|
||||
run: |
|
||||
go get -u golang.org/x/lint/golint
|
||||
- name: Lint files
|
||||
run: |
|
||||
golint -set_exit_status ./...
|
||||
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||
with:
|
||||
version: "2022.1.1"
|
||||
install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
|
||||
|
||||
test:
|
||||
name: Test
|
||||
|
|
@ -72,18 +70,18 @@ jobs:
|
|||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2
|
||||
with:
|
||||
username: ${{ secrets.BOT_USERNAME }}
|
||||
password: ${{ secrets.BOT_GHCR_PAT }}
|
||||
registry: ghcr.io
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b #v3
|
||||
with:
|
||||
version: v0.155.0
|
||||
args: --debug
|
||||
|
|
@ -193,7 +191,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Pull new module version
|
||||
uses: andrewslotin/go-proxy-pull-action@master
|
||||
uses: andrewslotin/go-proxy-pull-action@bfc19ec6536e1638181b2ad6a03e16c7ccfb122f #master@2022-10-14
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
187
README.md
187
README.md
|
|
@ -44,123 +44,130 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><a href="http://codelica.com"><img src="https://avatars3.githubusercontent.com/u/386101?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Codelica" title="Tests">⚠️</a> <a href="#ideas-Codelica" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://kopfkrieg.org"><img src="https://avatars2.githubusercontent.com/u/5047813?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3AKopfKrieg" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=KopfKrieg" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/bdehamer"><img src="https://avatars1.githubusercontent.com/u/398027?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brian DeHamer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bdehamer" title="Code">💻</a> <a href="#maintenance-bdehamer" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/rosscado"><img src="https://avatars1.githubusercontent.com/u/16578183?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ross Cadogan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rosscado" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/stffabi"><img src="https://avatars0.githubusercontent.com/u/9464631?v=4?s=100" width="100px;" alt=""/><br /><sub><b>stffabi</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=stffabi" title="Code">💻</a> <a href="#maintenance-stffabi" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/ATCUSA"><img src="https://avatars3.githubusercontent.com/u/3581228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Austin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ATCUSA" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://labs.ctl.io"><img src="https://avatars2.githubusercontent.com/u/6181487?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Gardner</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Adavidgardner11" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=davidgardner11" title="Documentation">📖</a></td>
|
||||
|
||||
<td align="center"><a href="http://codelica.com"><img src="https://avatars3.githubusercontent.com/u/386101?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Codelica" title="Tests">⚠️</a> <a href="#ideas-Codelica" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://kopfkrieg.org"><img src="https://avatars2.githubusercontent.com/u/5047813?v=4?s=100" width="100px;" alt="Florian"/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3AKopfKrieg" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=KopfKrieg" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/bdehamer"><img src="https://avatars1.githubusercontent.com/u/398027?v=4?s=100" width="100px;" alt="Brian DeHamer"/><br /><sub><b>Brian DeHamer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bdehamer" title="Code">💻</a> <a href="#maintenance-bdehamer" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/rosscado"><img src="https://avatars1.githubusercontent.com/u/16578183?v=4?s=100" width="100px;" alt="Ross Cadogan"/><br /><sub><b>Ross Cadogan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rosscado" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/stffabi"><img src="https://avatars0.githubusercontent.com/u/9464631?v=4?s=100" width="100px;" alt="stffabi"/><br /><sub><b>stffabi</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=stffabi" title="Code">💻</a> <a href="#maintenance-stffabi" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/ATCUSA"><img src="https://avatars3.githubusercontent.com/u/3581228?v=4?s=100" width="100px;" alt="Austin"/><br /><sub><b>Austin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ATCUSA" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://labs.ctl.io"><img src="https://avatars2.githubusercontent.com/u/6181487?v=4?s=100" width="100px;" alt="David Gardner"/><br /><sub><b>David Gardner</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Adavidgardner11" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=davidgardner11" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/dolanor"><img src="https://avatars3.githubusercontent.com/u/928722?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tanguy ⧓ Herrmann</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dolanor" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/rdamazio"><img src="https://avatars3.githubusercontent.com/u/997641?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rodrigo Damazio Bovendorp</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="#infra-thelamer" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/cnrmck"><img src="https://avatars2.githubusercontent.com/u/22061955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cnrmck</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cnrmck" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://harrywalter.co.uk"><img src="https://avatars3.githubusercontent.com/u/338588?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Harry Walter</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=haswalt" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://projectsperanza.com"><img src="https://avatars3.githubusercontent.com/u/74515?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Robotex</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Robotex" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://geraldpape.io"><img src="https://avatars0.githubusercontent.com/u/1494211?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gerald Pape</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ubergesundheit" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/dolanor"><img src="https://avatars3.githubusercontent.com/u/928722?v=4?s=100" width="100px;" alt="Tanguy ⧓ Herrmann"/><br /><sub><b>Tanguy ⧓ Herrmann</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dolanor" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/rdamazio"><img src="https://avatars3.githubusercontent.com/u/997641?v=4?s=100" width="100px;" alt="Rodrigo Damazio Bovendorp"/><br /><sub><b>Rodrigo Damazio Bovendorp</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=100" width="100px;" alt="Ryan Kuba"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="#infra-thelamer" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/cnrmck"><img src="https://avatars2.githubusercontent.com/u/22061955?v=4?s=100" width="100px;" alt="cnrmck"/><br /><sub><b>cnrmck</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cnrmck" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://harrywalter.co.uk"><img src="https://avatars3.githubusercontent.com/u/338588?v=4?s=100" width="100px;" alt="Harry Walter"/><br /><sub><b>Harry Walter</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=haswalt" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://projectsperanza.com"><img src="https://avatars3.githubusercontent.com/u/74515?v=4?s=100" width="100px;" alt="Robotex"/><br /><sub><b>Robotex</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Robotex" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://geraldpape.io"><img src="https://avatars0.githubusercontent.com/u/1494211?v=4?s=100" width="100px;" alt="Gerald Pape"/><br /><sub><b>Gerald Pape</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ubergesundheit" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/fomk"><img src="https://avatars0.githubusercontent.com/u/17636183?v=4?s=100" width="100px;" alt=""/><br /><sub><b>fomk</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=fomk" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/svengo"><img src="https://avatars3.githubusercontent.com/u/2502366?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sven Gottwald</b></sub></a><br /><a href="#infra-svengo" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://liberapay.com/techknowlogick/"><img src="https://avatars1.githubusercontent.com/u/164197?v=4?s=100" width="100px;" alt=""/><br /><sub><b>techknowlogick</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=techknowlogick" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://log.c5t.org/about/"><img src="https://avatars1.githubusercontent.com/u/1449568?v=4?s=100" width="100px;" alt=""/><br /><sub><b>waja</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=waja" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://scottalbertson.com"><img src="https://avatars2.githubusercontent.com/u/154463?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Scott Albertson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=salbertson" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/huddlesj"><img src="https://avatars1.githubusercontent.com/u/11966535?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jason Huddleston</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=huddlesj" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://npstr.space/"><img src="https://avatars3.githubusercontent.com/u/6048348?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Napster</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=napstr" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/fomk"><img src="https://avatars0.githubusercontent.com/u/17636183?v=4?s=100" width="100px;" alt="fomk"/><br /><sub><b>fomk</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=fomk" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/svengo"><img src="https://avatars3.githubusercontent.com/u/2502366?v=4?s=100" width="100px;" alt="Sven Gottwald"/><br /><sub><b>Sven Gottwald</b></sub></a><br /><a href="#infra-svengo" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://liberapay.com/techknowlogick/"><img src="https://avatars1.githubusercontent.com/u/164197?v=4?s=100" width="100px;" alt="techknowlogick"/><br /><sub><b>techknowlogick</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=techknowlogick" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://log.c5t.org/about/"><img src="https://avatars1.githubusercontent.com/u/1449568?v=4?s=100" width="100px;" alt="waja"/><br /><sub><b>waja</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=waja" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://scottalbertson.com"><img src="https://avatars2.githubusercontent.com/u/154463?v=4?s=100" width="100px;" alt="Scott Albertson"/><br /><sub><b>Scott Albertson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=salbertson" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/huddlesj"><img src="https://avatars1.githubusercontent.com/u/11966535?v=4?s=100" width="100px;" alt="Jason Huddleston"/><br /><sub><b>Jason Huddleston</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=huddlesj" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://npstr.space/"><img src="https://avatars3.githubusercontent.com/u/6048348?v=4?s=100" width="100px;" alt="Napster"/><br /><sub><b>Napster</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=napstr" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/darknode"><img src="https://avatars1.githubusercontent.com/u/809429?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maxim</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://schmitt.cat"><img src="https://avatars0.githubusercontent.com/u/17984549?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Max Schmitt</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mxschmitt" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/cron410"><img src="https://avatars1.githubusercontent.com/u/3082899?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cron410</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cron410" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Cardoso222"><img src="https://avatars3.githubusercontent.com/u/7026517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Paulo Henrique</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Cardoso222" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://coded.io"><img src="https://avatars0.githubusercontent.com/u/107097?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kaleb Elwert</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=belak" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/wmbutler"><img src="https://avatars1.githubusercontent.com/u/1254810?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bill Butler</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=wmbutler" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.mariotacke.io"><img src="https://avatars2.githubusercontent.com/u/4942019?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mario Tacke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mariotacke" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/darknode"><img src="https://avatars1.githubusercontent.com/u/809429?v=4?s=100" width="100px;" alt="Maxim"/><br /><sub><b>Maxim</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://schmitt.cat"><img src="https://avatars0.githubusercontent.com/u/17984549?v=4?s=100" width="100px;" alt="Max Schmitt"/><br /><sub><b>Max Schmitt</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mxschmitt" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/cron410"><img src="https://avatars1.githubusercontent.com/u/3082899?v=4?s=100" width="100px;" alt="cron410"/><br /><sub><b>cron410</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cron410" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Cardoso222"><img src="https://avatars3.githubusercontent.com/u/7026517?v=4?s=100" width="100px;" alt="Paulo Henrique"/><br /><sub><b>Paulo Henrique</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Cardoso222" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://coded.io"><img src="https://avatars0.githubusercontent.com/u/107097?v=4?s=100" width="100px;" alt="Kaleb Elwert"/><br /><sub><b>Kaleb Elwert</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=belak" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/wmbutler"><img src="https://avatars1.githubusercontent.com/u/1254810?v=4?s=100" width="100px;" alt="Bill Butler"/><br /><sub><b>Bill Butler</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=wmbutler" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.mariotacke.io"><img src="https://avatars2.githubusercontent.com/u/4942019?v=4?s=100" width="100px;" alt="Mario Tacke"/><br /><sub><b>Mario Tacke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mariotacke" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://markwoodbridge.com"><img src="https://avatars2.githubusercontent.com/u/1101318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mrw34" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://simme.dev"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Code">💻</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Asimskij" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Ansem93"><img src="https://avatars3.githubusercontent.com/u/6626218?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ansem93</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Ansem93" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/zoispag"><img src="https://avatars0.githubusercontent.com/u/21138205?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zoispag" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Azoispag" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-zoispag" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://alexandre.menif.name"><img src="https://avatars0.githubusercontent.com/u/16152103?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexandre Menif</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=alexandremenif" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/chugunov"><img src="https://avatars1.githubusercontent.com/u/4140479?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrey</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chugunov" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://markwoodbridge.com"><img src="https://avatars2.githubusercontent.com/u/1101318?v=4?s=100" width="100px;" alt="Mark Woodbridge"/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mrw34" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://simme.dev"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4?s=100" width="100px;" alt="Simon Aronsson"/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Code">💻</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Asimskij" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Ansem93"><img src="https://avatars3.githubusercontent.com/u/6626218?v=4?s=100" width="100px;" alt="Ansem93"/><br /><sub><b>Ansem93</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Ansem93" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4?s=100" width="100px;" alt="Luka Peschke"/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/zoispag"><img src="https://avatars0.githubusercontent.com/u/21138205?v=4?s=100" width="100px;" alt="Zois Pagoulatos"/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zoispag" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Azoispag" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-zoispag" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://alexandre.menif.name"><img src="https://avatars0.githubusercontent.com/u/16152103?v=4?s=100" width="100px;" alt="Alexandre Menif"/><br /><sub><b>Alexandre Menif</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=alexandremenif" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/chugunov"><img src="https://avatars1.githubusercontent.com/u/4140479?v=4?s=100" width="100px;" alt="Andrey"/><br /><sub><b>Andrey</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chugunov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://noplanman.ch"><img src="https://avatars3.githubusercontent.com/u/9423417?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Armando Lüscher</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=noplanman" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/rjbudke"><img src="https://avatars2.githubusercontent.com/u/273485?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Budke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rjbudke" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://kaloyan.raev.name"><img src="https://avatars2.githubusercontent.com/u/468091?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kaloyan Raev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/sixth"><img src="https://avatars3.githubusercontent.com/u/11591445?v=4?s=100" width="100px;" alt=""/><br /><sub><b>sixth</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixth" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://foosel.net"><img src="https://avatars0.githubusercontent.com/u/83657?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gina Häußge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=foosel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/8ear"><img src="https://avatars0.githubusercontent.com/u/10329648?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Max H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=8ear" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://pjknkda.github.io"><img src="https://avatars0.githubusercontent.com/u/4986524?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jungkook Park</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pjknkda" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://noplanman.ch"><img src="https://avatars3.githubusercontent.com/u/9423417?v=4?s=100" width="100px;" alt="Armando Lüscher"/><br /><sub><b>Armando Lüscher</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=noplanman" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/rjbudke"><img src="https://avatars2.githubusercontent.com/u/273485?v=4?s=100" width="100px;" alt="Ryan Budke"/><br /><sub><b>Ryan Budke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rjbudke" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://kaloyan.raev.name"><img src="https://avatars2.githubusercontent.com/u/468091?v=4?s=100" width="100px;" alt="Kaloyan Raev"/><br /><sub><b>Kaloyan Raev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/sixth"><img src="https://avatars3.githubusercontent.com/u/11591445?v=4?s=100" width="100px;" alt="sixth"/><br /><sub><b>sixth</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixth" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://foosel.net"><img src="https://avatars0.githubusercontent.com/u/83657?v=4?s=100" width="100px;" alt="Gina Häußge"/><br /><sub><b>Gina Häußge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=foosel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/8ear"><img src="https://avatars0.githubusercontent.com/u/10329648?v=4?s=100" width="100px;" alt="Max H."/><br /><sub><b>Max H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=8ear" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://pjknkda.github.io"><img src="https://avatars0.githubusercontent.com/u/4986524?v=4?s=100" width="100px;" alt="Jungkook Park"/><br /><sub><b>Jungkook Park</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pjknkda" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://achfrag.net"><img src="https://avatars1.githubusercontent.com/u/5753622?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jan Kristof Nidzwetzki</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jnidzwetzki" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.lukaselsner.de"><img src="https://avatars0.githubusercontent.com/u/1413542?v=4?s=100" width="100px;" alt=""/><br /><sub><b>lukas</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mindrunner" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://codingcoffee.dev"><img src="https://avatars3.githubusercontent.com/u/13611153?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ameya Shenoy</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=codingCoffee" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/raymondelooff"><img src="https://avatars0.githubusercontent.com/u/9716806?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Raymon de Looff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=raymondelooff" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://codemonkeylabs.com"><img src="https://avatars2.githubusercontent.com/u/704034?v=4?s=100" width="100px;" alt=""/><br /><sub><b>John Clayton</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jsclayton" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Germs2004"><img src="https://avatars2.githubusercontent.com/u/5519340?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Germs2004</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Germs2004" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/lukwil"><img src="https://avatars1.githubusercontent.com/u/30203234?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Lukas Willburger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukwil" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://achfrag.net"><img src="https://avatars1.githubusercontent.com/u/5753622?v=4?s=100" width="100px;" alt="Jan Kristof Nidzwetzki"/><br /><sub><b>Jan Kristof Nidzwetzki</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jnidzwetzki" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.lukaselsner.de"><img src="https://avatars0.githubusercontent.com/u/1413542?v=4?s=100" width="100px;" alt="lukas"/><br /><sub><b>lukas</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mindrunner" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://codingcoffee.dev"><img src="https://avatars3.githubusercontent.com/u/13611153?v=4?s=100" width="100px;" alt="Ameya Shenoy"/><br /><sub><b>Ameya Shenoy</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=codingCoffee" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/raymondelooff"><img src="https://avatars0.githubusercontent.com/u/9716806?v=4?s=100" width="100px;" alt="Raymon de Looff"/><br /><sub><b>Raymon de Looff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=raymondelooff" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://codemonkeylabs.com"><img src="https://avatars2.githubusercontent.com/u/704034?v=4?s=100" width="100px;" alt="John Clayton"/><br /><sub><b>John Clayton</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jsclayton" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Germs2004"><img src="https://avatars2.githubusercontent.com/u/5519340?v=4?s=100" width="100px;" alt="Germs2004"/><br /><sub><b>Germs2004</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Germs2004" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/lukwil"><img src="https://avatars1.githubusercontent.com/u/30203234?v=4?s=100" width="100px;" alt="Lukas Willburger"/><br /><sub><b>Lukas Willburger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukwil" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/auanasgheps"><img src="https://avatars2.githubusercontent.com/u/20586878?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Oliver Cervera</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=auanasgheps" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/victorcmoura"><img src="https://avatars1.githubusercontent.com/u/26290053?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Victor Moura</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/mbrandau"><img src="https://avatars3.githubusercontent.com/u/12972798?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/aneisch"><img src="https://avatars1.githubusercontent.com/u/6991461?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=aneisch" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/sixcorners"><img src="https://avatars0.githubusercontent.com/u/585501?v=4?s=100" width="100px;" alt=""/><br /><sub><b>sixcorners</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixcorners" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4?s=100" width="100px;" alt=""/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=arnested" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center"><a href="https://github.com/auanasgheps"><img src="https://avatars2.githubusercontent.com/u/20586878?v=4?s=100" width="100px;" alt="Oliver Cervera"/><br /><sub><b>Oliver Cervera</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=auanasgheps" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/victorcmoura"><img src="https://avatars1.githubusercontent.com/u/26290053?v=4?s=100" width="100px;" alt="Victor Moura"/><br /><sub><b>Victor Moura</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/mbrandau"><img src="https://avatars3.githubusercontent.com/u/12972798?v=4?s=100" width="100px;" alt="Maximilian Brandau"/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/aneisch"><img src="https://avatars1.githubusercontent.com/u/6991461?v=4?s=100" width="100px;" alt="Andrew"/><br /><sub><b>Andrew</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=aneisch" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/sixcorners"><img src="https://avatars0.githubusercontent.com/u/585501?v=4?s=100" width="100px;" alt="sixcorners"/><br /><sub><b>sixcorners</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixcorners" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4?s=100" width="100px;" alt="nils måsén"/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4?s=100" width="100px;" alt="Arne Jørgensen"/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=arnested" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested" title="Reviewed Pull Requests">👀</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/patski123"><img src="https://avatars1.githubusercontent.com/u/19295295?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PatSki123</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patski123" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://rubyroidlabs.com/"><img src="https://avatars2.githubusercontent.com/u/624999?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Valentine Zavadsky</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/bopoh24"><img src="https://avatars2.githubusercontent.com/u/4086631?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Voronin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bopoh24" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/issues?q=author%3Abopoh24" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://www.teqneers.de"><img src="https://avatars0.githubusercontent.com/u/788989?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Oliver Mueller</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ogmueller" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/tammert"><img src="https://avatars0.githubusercontent.com/u/8885250?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sebastiaan Tammer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tammert" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Miosame"><img src="https://avatars1.githubusercontent.com/u/8201077?v=4?s=100" width="100px;" alt=""/><br /><sub><b>miosame</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=miosame" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://mtz.gr"><img src="https://avatars3.githubusercontent.com/u/590246?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Metzger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/issues?q=author%3Aandrewjmetzger" title="Bug reports">🐛</a> <a href="#example-andrewjmetzger" title="Examples">💡</a></td>
|
||||
<td align="center"><a href="https://github.com/patski123"><img src="https://avatars1.githubusercontent.com/u/19295295?v=4?s=100" width="100px;" alt="PatSki123"/><br /><sub><b>PatSki123</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patski123" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://rubyroidlabs.com/"><img src="https://avatars2.githubusercontent.com/u/624999?v=4?s=100" width="100px;" alt="Valentine Zavadsky"/><br /><sub><b>Valentine Zavadsky</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/bopoh24"><img src="https://avatars2.githubusercontent.com/u/4086631?v=4?s=100" width="100px;" alt="Alexander Voronin"/><br /><sub><b>Alexander Voronin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bopoh24" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/issues?q=author%3Abopoh24" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://www.teqneers.de"><img src="https://avatars0.githubusercontent.com/u/788989?v=4?s=100" width="100px;" alt="Oliver Mueller"/><br /><sub><b>Oliver Mueller</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ogmueller" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/tammert"><img src="https://avatars0.githubusercontent.com/u/8885250?v=4?s=100" width="100px;" alt="Sebastiaan Tammer"/><br /><sub><b>Sebastiaan Tammer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tammert" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Miosame"><img src="https://avatars1.githubusercontent.com/u/8201077?v=4?s=100" width="100px;" alt="miosame"/><br /><sub><b>miosame</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=miosame" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://mtz.gr"><img src="https://avatars3.githubusercontent.com/u/590246?v=4?s=100" width="100px;" alt="Andrew Metzger"/><br /><sub><b>Andrew Metzger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/issues?q=author%3Aandrewjmetzger" title="Bug reports">🐛</a> <a href="#example-andrewjmetzger" title="Examples">💡</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pgrimaud" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/mattdoran"><img src="https://avatars0.githubusercontent.com/u/577779?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matt Doran</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mattdoran" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/MihailITPlace"><img src="https://avatars2.githubusercontent.com/u/28401551?v=4?s=100" width="100px;" alt=""/><br /><sub><b>MihailITPlace</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MihailITPlace" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/bugficks"><img src="https://avatars1.githubusercontent.com/u/2992895?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bugficks</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/MichaelSp"><img src="https://avatars0.githubusercontent.com/u/448282?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MichaelSp" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jokay"><img src="https://avatars0.githubusercontent.com/u/18613935?v=4?s=100" width="100px;" alt=""/><br /><sub><b>D. Domig</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jokay" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://maxwells-daemon.io"><img src="https://avatars1.githubusercontent.com/u/260084?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ben Osheroff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=osheroff" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4?s=100" width="100px;" alt="Pierre Grimaud"/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pgrimaud" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/mattdoran"><img src="https://avatars0.githubusercontent.com/u/577779?v=4?s=100" width="100px;" alt="Matt Doran"/><br /><sub><b>Matt Doran</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mattdoran" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/MihailITPlace"><img src="https://avatars2.githubusercontent.com/u/28401551?v=4?s=100" width="100px;" alt="MihailITPlace"/><br /><sub><b>MihailITPlace</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MihailITPlace" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/bugficks"><img src="https://avatars1.githubusercontent.com/u/2992895?v=4?s=100" width="100px;" alt="bugficks"/><br /><sub><b>bugficks</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/MichaelSp"><img src="https://avatars0.githubusercontent.com/u/448282?v=4?s=100" width="100px;" alt="Michael"/><br /><sub><b>Michael</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MichaelSp" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jokay"><img src="https://avatars0.githubusercontent.com/u/18613935?v=4?s=100" width="100px;" alt="D. Domig"/><br /><sub><b>D. Domig</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jokay" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://maxwells-daemon.io"><img src="https://avatars1.githubusercontent.com/u/260084?v=4?s=100" width="100px;" alt="Ben Osheroff"/><br /><sub><b>Ben Osheroff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=osheroff" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/dhet"><img src="https://avatars3.githubusercontent.com/u/2668621?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dhet" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.gridgeo.com"><img src="https://avatars1.githubusercontent.com/u/671887?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chander Ganesan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chander" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/yrien30"><img src="https://avatars1.githubusercontent.com/u/26816162?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yrien30</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=yrien30" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ksurl"><img src="https://avatars1.githubusercontent.com/u/1371562?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ksurl</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Code">💻</a> <a href="#infra-ksurl" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/rg9400"><img src="https://avatars2.githubusercontent.com/u/39887349?v=4?s=100" width="100px;" alt=""/><br /><sub><b>rg9400</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rg9400" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/tkalus"><img src="https://avatars2.githubusercontent.com/u/287181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Turtle Kalus</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tkalus" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/SrihariThalla"><img src="https://avatars1.githubusercontent.com/u/7479937?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Srihari Thalla</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=SrihariThalla" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/dhet"><img src="https://avatars3.githubusercontent.com/u/2668621?v=4?s=100" width="100px;" alt="David H."/><br /><sub><b>David H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dhet" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.gridgeo.com"><img src="https://avatars1.githubusercontent.com/u/671887?v=4?s=100" width="100px;" alt="Chander Ganesan"/><br /><sub><b>Chander Ganesan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chander" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/yrien30"><img src="https://avatars1.githubusercontent.com/u/26816162?v=4?s=100" width="100px;" alt="yrien30"/><br /><sub><b>yrien30</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=yrien30" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ksurl"><img src="https://avatars1.githubusercontent.com/u/1371562?v=4?s=100" width="100px;" alt="ksurl"/><br /><sub><b>ksurl</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Code">💻</a> <a href="#infra-ksurl" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/rg9400"><img src="https://avatars2.githubusercontent.com/u/39887349?v=4?s=100" width="100px;" alt="rg9400"/><br /><sub><b>rg9400</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rg9400" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/tkalus"><img src="https://avatars2.githubusercontent.com/u/287181?v=4?s=100" width="100px;" alt="Turtle Kalus"/><br /><sub><b>Turtle Kalus</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tkalus" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/SrihariThalla"><img src="https://avatars1.githubusercontent.com/u/7479937?v=4?s=100" width="100px;" alt="Srihari Thalla"/><br /><sub><b>Srihari Thalla</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=SrihariThalla" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://nymous.io"><img src="https://avatars1.githubusercontent.com/u/4216559?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Thomas Gaudin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=nymous" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://indigo.re/"><img src="https://avatars.githubusercontent.com/u/2804645?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hydrargyrum</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hydrargyrum" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://reinout.vanrees.org"><img src="https://avatars.githubusercontent.com/u/121433?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Reinout van Rees</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=reinout" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/DasSkelett"><img src="https://avatars.githubusercontent.com/u/28812678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>DasSkelett</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=DasSkelett" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/zenjabba"><img src="https://avatars.githubusercontent.com/u/679864?v=4?s=100" width="100px;" alt=""/><br /><sub><b>zenjabba</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zenjabba" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://quan.io"><img src="https://avatars.githubusercontent.com/u/3526705?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dan Quan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=djquan" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/modem7"><img src="https://avatars.githubusercontent.com/u/4349962?v=4?s=100" width="100px;" alt=""/><br /><sub><b>modem7</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=modem7" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://nymous.io"><img src="https://avatars1.githubusercontent.com/u/4216559?v=4?s=100" width="100px;" alt="Thomas Gaudin"/><br /><sub><b>Thomas Gaudin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=nymous" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://indigo.re/"><img src="https://avatars.githubusercontent.com/u/2804645?v=4?s=100" width="100px;" alt="hydrargyrum"/><br /><sub><b>hydrargyrum</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hydrargyrum" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://reinout.vanrees.org"><img src="https://avatars.githubusercontent.com/u/121433?v=4?s=100" width="100px;" alt="Reinout van Rees"/><br /><sub><b>Reinout van Rees</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=reinout" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/DasSkelett"><img src="https://avatars.githubusercontent.com/u/28812678?v=4?s=100" width="100px;" alt="DasSkelett"/><br /><sub><b>DasSkelett</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=DasSkelett" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/zenjabba"><img src="https://avatars.githubusercontent.com/u/679864?v=4?s=100" width="100px;" alt="zenjabba"/><br /><sub><b>zenjabba</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zenjabba" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://quan.io"><img src="https://avatars.githubusercontent.com/u/3526705?v=4?s=100" width="100px;" alt="Dan Quan"/><br /><sub><b>Dan Quan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=djquan" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/modem7"><img src="https://avatars.githubusercontent.com/u/4349962?v=4?s=100" width="100px;" alt="modem7"/><br /><sub><b>modem7</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=modem7" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/hypnoglow"><img src="https://avatars.githubusercontent.com/u/4853075?v=4?s=100" width="100px;" alt="Igor Zibarev"/><br /><sub><b>Igor Zibarev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hypnoglow" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/patricegautier"><img src="https://avatars.githubusercontent.com/u/38435239?v=4?s=100" width="100px;" alt="Patrice"/><br /><sub><b>Patrice</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patricegautier" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://jamesw.link/me"><img src="https://avatars.githubusercontent.com/u/8067792?v=4?s=100" width="100px;" alt="James White"/><br /><sub><b>James White</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jamesmacwhite" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ko-fi.com/foxite"><img src="https://avatars.githubusercontent.com/u/20421657?v=4?s=100" width="100px;" alt="Dirk Kok"/><br /><sub><b>Dirk Kok</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Foxite" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/EDIflyer"><img src="https://avatars.githubusercontent.com/u/13610277?v=4?s=100" width="100px;" alt="EDIflyer"/><br /><sub><b>EDIflyer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=EDIflyer" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/jauderho"><img src="https://avatars.githubusercontent.com/u/13562?v=4?s=100" width="100px;" alt="Jauder Ho"/><br /><sub><b>Jauder Ho</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jauderho" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://tamal.vercel.app/"><img src="https://avatars.githubusercontent.com/u/72851613?v=4?s=100" width="100px;" alt="Tamal Das "/><br /><sub><b>Tamal Das </b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=IAmTamal" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/hypnoglow"><img src="https://avatars.githubusercontent.com/u/4853075?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Igor Zibarev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hypnoglow" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/patricegautier"><img src="https://avatars.githubusercontent.com/u/38435239?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Patrice</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patricegautier" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://jamesw.link/me"><img src="https://avatars.githubusercontent.com/u/8067792?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James White</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jamesmacwhite" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ko-fi.com/foxite"><img src="https://avatars.githubusercontent.com/u/20421657?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dirk Kok</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Foxite" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/EDIflyer"><img src="https://avatars.githubusercontent.com/u/13610277?v=4?s=100" width="100px;" alt=""/><br /><sub><b>EDIflyer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=EDIflyer" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/jauderho"><img src="https://avatars.githubusercontent.com/u/13562?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jauder Ho</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jauderho" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/andriibratanin"><img src="https://avatars.githubusercontent.com/u/20169213?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrii Bratanin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=andriibratanin" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
|
|
|
|||
111
cmd/notify-upgrade.go
Normal file
111
cmd/notify-upgrade.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// Package cmd contains the watchtower (sub-)commands
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/flags"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/notifications"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var notifyUpgradeCommand = NewNotifyUpgradeCommand()
|
||||
|
||||
// NewNotifyUpgradeCommand creates the notify upgrade command for watchtower
|
||||
func NewNotifyUpgradeCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "notify-upgrade",
|
||||
Short: "Upgrade legacy notification configuration to shoutrrr URLs",
|
||||
Run: runNotifyUpgrade,
|
||||
}
|
||||
}
|
||||
|
||||
func runNotifyUpgrade(cmd *cobra.Command, args []string) {
|
||||
if err := runNotifyUpgradeE(cmd, args); err != nil {
|
||||
logf("Notification upgrade failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runNotifyUpgradeE(cmd *cobra.Command, _ []string) error {
|
||||
f := cmd.Flags()
|
||||
flags.ProcessFlagAliases(f)
|
||||
|
||||
notifier = notifications.NewNotifier(cmd)
|
||||
urls := notifier.GetURLs()
|
||||
|
||||
logf("Found notification configurations for: %v", strings.Join(notifier.GetNames(), ", "))
|
||||
|
||||
outFile, err := os.CreateTemp("/", "watchtower-notif-urls-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %v", err)
|
||||
}
|
||||
logf("Writing notification URLs to %v", outFile.Name())
|
||||
logf("")
|
||||
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("WATCHTOWER_NOTIFICATION_URL=")
|
||||
|
||||
for i, u := range urls {
|
||||
if i != 0 {
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
sb.WriteString(u)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprint(outFile, sb.String())
|
||||
tryOrLog(err, "Failed to write to output file")
|
||||
|
||||
tryOrLog(outFile.Sync(), "Failed to sync output file")
|
||||
tryOrLog(outFile.Close(), "Failed to close output file")
|
||||
|
||||
containerID := "<CONTAINER>"
|
||||
cid, err := container.GetRunningContainerID()
|
||||
tryOrLog(err, "Failed to get running container ID")
|
||||
if cid != "" {
|
||||
containerID = cid.ShortID()
|
||||
}
|
||||
logf("To get the environment file, use:")
|
||||
logf("cp %v:%v ./watchtower-notifications.env", containerID, outFile.Name())
|
||||
logf("")
|
||||
logf("Note: This file will be removed in 5 minutes or when this container is stopped!")
|
||||
|
||||
signalChannel := make(chan os.Signal, 1)
|
||||
time.AfterFunc(5*time.Minute, func() {
|
||||
signalChannel <- syscall.SIGALRM
|
||||
})
|
||||
|
||||
signal.Notify(signalChannel, os.Interrupt)
|
||||
signal.Notify(signalChannel, syscall.SIGTERM)
|
||||
|
||||
switch <-signalChannel {
|
||||
case syscall.SIGALRM:
|
||||
logf("Timed out!")
|
||||
case os.Interrupt, syscall.SIGTERM:
|
||||
logf("Stopping...")
|
||||
default:
|
||||
}
|
||||
|
||||
if err := os.Remove(outFile.Name()); err != nil {
|
||||
logf("Failed to remove file, it may still be present in the container image! Error: %v", err)
|
||||
} else {
|
||||
logf("Environment file has been removed.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryOrLog(err error, message string) {
|
||||
if err != nil {
|
||||
logf("%v: %v\n", message, err)
|
||||
}
|
||||
}
|
||||
|
||||
func logf(format string, v ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf(format, v...))
|
||||
}
|
||||
18
cmd/root.go
18
cmd/root.go
|
|
@ -54,6 +54,7 @@ func NewRootCommand() *cobra.Command {
|
|||
`,
|
||||
Run: Run,
|
||||
PreRun: PreRun,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ func init() {
|
|||
|
||||
// Execute the root func and exit in case of errors
|
||||
func Execute() {
|
||||
rootCmd.AddCommand(notifyUpgradeCommand)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
@ -87,11 +89,11 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
})
|
||||
}
|
||||
|
||||
if enabled, _ := f.GetBool("debug"); enabled {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
if enabled, _ := f.GetBool("trace"); enabled {
|
||||
log.SetLevel(log.TraceLevel)
|
||||
rawLogLevel, _ := f.GetString(`log-level`)
|
||||
if logLevel, err := log.ParseLevel(rawLogLevel); err != nil {
|
||||
log.Fatalf("Invalid log level: %s", err.Error())
|
||||
} else {
|
||||
log.SetLevel(logLevel)
|
||||
}
|
||||
|
||||
scheduleSpec, _ = f.GetString("schedule")
|
||||
|
|
@ -139,6 +141,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
})
|
||||
|
||||
notifier = notifications.NewNotifier(cmd)
|
||||
notifier.AddLogHook()
|
||||
}
|
||||
|
||||
// Run is the main execution flow of the command
|
||||
|
|
@ -179,7 +182,10 @@ func Run(c *cobra.Command, names []string) {
|
|||
httpAPI := api.New(apiToken)
|
||||
|
||||
if enableUpdateAPI {
|
||||
updateHandler := update.New(func(images []string) { runUpdatesWithNotifications(filters.FilterByImage(images, filter)) }, updateLock)
|
||||
updateHandler := update.New(func(images []string) {
|
||||
metric := runUpdatesWithNotifications(filters.FilterByImage(images, filter))
|
||||
metrics.RegisterScan(metric)
|
||||
}, updateLock)
|
||||
httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
|
||||
// If polling isn't enabled the scheduler is never started and
|
||||
// we need to trigger the startup messages manually.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM --platform=$BUILDPLATFORM alpine:3.16.2 as alpine
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.17.1 as alpine
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
|
|
|
|||
|
|
@ -71,6 +71,10 @@ Environment Variable: WATCHTOWER_REMOVE_VOLUMES
|
|||
## Debug
|
||||
Enable debug mode with verbose logging.
|
||||
|
||||
!!! note "Notes"
|
||||
Alias for `--log-level debug`. See [Maximum log level](#maximum-log-level).
|
||||
Does _not_ take an argument when used as an argument. Using `--debug true` will **not** work.
|
||||
|
||||
```text
|
||||
Argument: --debug, -d
|
||||
Environment Variable: WATCHTOWER_DEBUG
|
||||
|
|
@ -81,6 +85,10 @@ Environment Variable: WATCHTOWER_DEBUG
|
|||
## Trace
|
||||
Enable trace mode with very verbose logging. Caution: exposes credentials!
|
||||
|
||||
!!! note "Notes"
|
||||
Alias for `--log-level trace`. See [Maximum log level](#maximum-log-level).
|
||||
Does _not_ take an argument when used as an argument. Using `--trace true` will **not** work.
|
||||
|
||||
```text
|
||||
Argument: --trace
|
||||
Environment Variable: WATCHTOWER_TRACE
|
||||
|
|
@ -88,6 +96,17 @@ Environment Variable: WATCHTOWER_TRACE
|
|||
Default: false
|
||||
```
|
||||
|
||||
## Maximum log level
|
||||
|
||||
The maximum log level that will be written to STDERR (shown in `docker log` when used in a container).
|
||||
|
||||
```text
|
||||
Argument: --log-level
|
||||
Environment Variable: WATCHTOWER_LOG_LEVEL
|
||||
Possible values: panic, fatal, error, warn, info, debug or trace
|
||||
Default: info
|
||||
```
|
||||
|
||||
## ANSI colors
|
||||
Disable ANSI color escape codes in log output.
|
||||
|
||||
|
|
@ -341,4 +360,4 @@ requests and may rate limit pull requests (mainly docker.io).
|
|||
Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE
|
||||
Possible values: always, auto, never
|
||||
Default: auto
|
||||
```
|
||||
```
|
||||
|
|
@ -1,15 +1,7 @@
|
|||
# Notifications
|
||||
|
||||
Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging
|
||||
system, [logrus](http://github.com/sirupsen/logrus). The types of notifications to send are set by passing a
|
||||
comma-separated list of values to the `--notifications` option
|
||||
(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:
|
||||
|
||||
- `email` to send notifications via e-mail
|
||||
- `slack` to send notifications through a Slack webhook
|
||||
- `msteams` to send notifications via MSTeams webhook
|
||||
- `gotify` to send notifications via Gotify
|
||||
- `shoutrrr` to send notifications via [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr)
|
||||
system, [logrus](http://github.com/sirupsen/logrus).
|
||||
|
||||
!!! note "Using multiple notifications with environment variables"
|
||||
There is currently a bug in Viper (https://github.com/spf13/viper/issues/380), which prevents comma-separated slices to
|
||||
|
|
@ -31,7 +23,221 @@ comma-separated list of values to the `--notifications` option
|
|||
- `notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers.
|
||||
- `notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together.
|
||||
|
||||
## Available services
|
||||
## [shoutrrr](https://github.com/containrrr/shoutrrr) notifications
|
||||
|
||||
To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set:
|
||||
|
||||
- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used.
|
||||
|
||||
|
||||
Go to [containrrr.dev/shoutrrr/v0.7/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to
|
||||
learn more about the different service URLs you can use. You can define multiple services by space separating the
|
||||
URLs. (See example below)
|
||||
|
||||
You can customize the message posted by setting a template.
|
||||
|
||||
- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message.
|
||||
|
||||
The template is a Go [template](https://golang.org/pkg/text/template/) that either format a list
|
||||
of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry) or a `notification.Data` struct.
|
||||
|
||||
Simple templates are used unless the `notification-report` flag is specified:
|
||||
|
||||
- `--notification-report` (env. `WATCHTOWER_NOTIFICATION_REPORT`): Use the session report as the notification template data.
|
||||
|
||||
## Simple templates
|
||||
|
||||
The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also
|
||||
outputs timestamp and log level.
|
||||
|
||||
!!! tip "Custom date format"
|
||||
If you want to adjust the date/time format it must show how the
|
||||
[reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your
|
||||
custom format.
|
||||
i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
|
||||
-e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \
|
||||
containrrr/watchtower
|
||||
```
|
||||
|
||||
## Report templates
|
||||
|
||||
The default template for report notifications are the following:
|
||||
```go
|
||||
{{- if .Report -}}
|
||||
{{- with .Report -}}
|
||||
{{- if ( or .Updated .Failed ) -}}
|
||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||
{{- range .Updated}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||
{{- end -}}
|
||||
{{- range .Fresh}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||
{{- end -}}
|
||||
{{- range .Skipped}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- range .Failed}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
||||
{{- end -}}
|
||||
```
|
||||
|
||||
It will be used to send a summary of every session if there are any containers that were updated or which failed to update.
|
||||
|
||||
!!! note "Skipping notifications"
|
||||
Whenever the result of applying the template results in an empty string, no notifications will
|
||||
be sent. This is by default used to limit the notifications to only be sent when there something noteworthy occurred.
|
||||
|
||||
You can replace `{{- if ( or .Updated .Failed ) -}}` with any logic you want to decide when to send the notifications.
|
||||
|
||||
Example using a custom report template that always sends a session report after each run:
|
||||
|
||||
=== "docker run"
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATION_REPORT="true"
|
||||
-e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
|
||||
-e WATCHTOWER_NOTIFICATION_TEMPLATE="
|
||||
{{- if .Report -}}
|
||||
{{- with .Report -}}
|
||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||
{{- range .Updated}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||
{{- end -}}
|
||||
{{- range .Fresh}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||
{{- end -}}
|
||||
{{- range .Skipped}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- range .Failed}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
||||
{{- end -}}
|
||||
" \
|
||||
containrrr/watchtower
|
||||
```
|
||||
|
||||
=== "docker-compose"
|
||||
|
||||
``` yaml
|
||||
version: "3"
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env:
|
||||
WATCHTOWER_NOTIFICATION_REPORT: "true"
|
||||
WATCHTOWER_NOTIFICATION_URL: >
|
||||
discord://token@channel
|
||||
slack://watchtower@token-a/token-b/token-c
|
||||
WATCHTOWER_NOTIFICATION_TEMPLATE: |
|
||||
{{- if .Report -}}
|
||||
{{- with .Report -}}
|
||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||
{{- range .Updated}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||
{{- end -}}
|
||||
{{- range .Fresh}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||
{{- end -}}
|
||||
{{- range .Skipped}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- range .Failed}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
||||
{{- end -}}
|
||||
```
|
||||
|
||||
## Legacy notifications
|
||||
|
||||
For backwards compatibility, the notifications can also be configured using legacy notification options. These will automatically be converted to shoutrrr URLs when used.
|
||||
The types of notifications to send are set by passing a comma-separated list of values to the `--notifications` option
|
||||
(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:
|
||||
|
||||
- `email` to send notifications via e-mail
|
||||
- `slack` to send notifications through a Slack webhook
|
||||
- `msteams` to send notifications via MSTeams webhook
|
||||
- `gotify` to send notifications via Gotify
|
||||
|
||||
### `notify-upgrade`
|
||||
If watchtower is started with `notify-upgrade` as it's first argument, it will generate a .env file with your current legacy notification options converted to shoutrrr URLs.
|
||||
|
||||
=== "docker run"
|
||||
|
||||
```bash
|
||||
$ docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATIONS=slack \
|
||||
-e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL="https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy" \
|
||||
containrrr/watchtower \
|
||||
notify-upgrade
|
||||
```
|
||||
|
||||
=== "docker-compose.yml"
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env:
|
||||
WATCHTOWER_NOTIFICATIONS: slack
|
||||
WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy
|
||||
command: notify-upgrade
|
||||
```
|
||||
|
||||
|
||||
You can then copy this file from the container (a message with the full command to do so will be logged) and use it with your current setup:
|
||||
|
||||
=== "docker run"
|
||||
|
||||
```bash
|
||||
$ docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--env-file watchtower-notifications.env \
|
||||
containrrr/watchtower
|
||||
```
|
||||
|
||||
=== "docker-compose.yml"
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env_file:
|
||||
- watchtower-notifications.env
|
||||
```
|
||||
|
||||
### Email
|
||||
|
||||
|
|
@ -177,41 +383,3 @@ docker run -d \
|
|||
|
||||
If you want to disable TLS verification for the Gotify instance, you can use either `-e WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY=true` or `--notification-gotify-tls-skip-verify`.
|
||||
|
||||
### [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr)
|
||||
|
||||
To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set:
|
||||
|
||||
- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used.
|
||||
|
||||
|
||||
Go to [containrrr.dev/shoutrrr/v0.6/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to
|
||||
learn more about the different service URLs you can use. You can define multiple services by space separating the
|
||||
URLs. (See example below)
|
||||
|
||||
You can customize the message posted by setting a template.
|
||||
|
||||
- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message.
|
||||
|
||||
The template is a Go [template](https://golang.org/pkg/text/template/) and that format a list
|
||||
of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry).
|
||||
|
||||
The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also
|
||||
outputs timestamp and log level.
|
||||
|
||||
!!! tip "Custom date format"
|
||||
If you want to adjust the date/time format it must show how the
|
||||
[reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your
|
||||
custom format.
|
||||
i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATIONS=shoutrrr \
|
||||
-e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
|
||||
-e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \
|
||||
containrrr/watchtower
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,16 +1,87 @@
|
|||
[data-md-color-scheme="containrrr"] {
|
||||
--md-primary-fg-color: #406170;
|
||||
--md-primary-fg-color--light:#acbfc7;
|
||||
--md-primary-fg-color--dark: #003343;
|
||||
--md-accent-fg-color: #003343;
|
||||
--md-accent-fg-color--transparent: #00334310;
|
||||
/* Primary and accent */
|
||||
--md-primary-fg-color: #406170;
|
||||
--md-primary-fg-color--light:#acbfc7;
|
||||
--md-primary-fg-color--dark: #003343;
|
||||
--md-accent-fg-color: #003343;
|
||||
--md-accent-fg-color--transparent: #00334310;
|
||||
|
||||
/* Typeset overrides */
|
||||
--md-typeset-a-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="containrrr-dark"] {
|
||||
--md-hue: 199;
|
||||
|
||||
/* Primary and accent */
|
||||
--md-primary-fg-color: hsl(199deg 27% 35% / 100%);
|
||||
--md-primary-fg-color--link: hsl(199deg 45% 65% / 100%);
|
||||
--md-primary-fg-color--light: hsl(198deg 19% 73% / 100%);
|
||||
--md-primary-fg-color--dark: hsl(194deg 100% 13% / 100%);
|
||||
--md-accent-fg-color: hsl(194deg 45% 50% / 100%);
|
||||
--md-accent-fg-color--transparent: hsl(194deg 45% 50% / 6.3%);
|
||||
|
||||
/* Default */
|
||||
--md-default-fg-color: hsl(var(--md-hue) 75% 95% / 100%);
|
||||
--md-default-fg-color--light: hsl(var(--md-hue) 75% 90% / 62%);
|
||||
--md-default-fg-color--lighter: hsl(var(--md-hue) 75% 90% / 32%);
|
||||
--md-default-fg-color--lightest: hsl(var(--md-hue) 75% 90% / 12%);
|
||||
--md-default-bg-color: hsl(var(--md-hue) 15% 21% / 100%);
|
||||
--md-default-bg-color--light: hsl(var(--md-hue) 15% 21% / 54%);
|
||||
--md-default-bg-color--lighter: hsl(var(--md-hue) 15% 21% / 26%);
|
||||
--md-default-bg-color--lightest: hsl(var(--md-hue) 15% 21% / 7%);
|
||||
|
||||
/* Code */
|
||||
--md-code-fg-color: hsl(var(--md-hue) 18% 86% / 100%);
|
||||
--md-code-bg-color: hsl(var(--md-hue) 15% 15% / 100%);
|
||||
--md-code-hl-color: hsl(218deg 100% 63% / 15%);
|
||||
--md-code-hl-number-color: hsl(346deg 74% 63% / 100%);
|
||||
--md-code-hl-special-color: hsl(320deg 83% 66% / 100%);
|
||||
--md-code-hl-function-color: hsl(271deg 57% 65% / 100%);
|
||||
--md-code-hl-constant-color: hsl(230deg 62% 70% / 100%);
|
||||
--md-code-hl-keyword-color: hsl(199deg 33% 64% / 100%);
|
||||
--md-code-hl-string-color: hsl( 50deg 34% 74% / 100%);
|
||||
--md-code-hl-name-color: var(--md-code-fg-color);
|
||||
--md-code-hl-operator-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-punctuation-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-comment-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-generic-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-variable-color: hsl(241deg 22% 60% / 100%);
|
||||
|
||||
/* Typeset */
|
||||
--md-typeset-color: var(--md-default-fg-color);
|
||||
--md-typeset-a-color: var(--md-primary-fg-color--link);
|
||||
--md-typeset-mark-color: hsl(218deg 100% 63% / 30%);
|
||||
--md-typeset-kbd-color: hsl(var(--md-hue) 15% 94% / 12%);
|
||||
--md-typeset-kbd-accent-color: hsl(var(--md-hue) 15% 94% / 20%);
|
||||
--md-typeset-kbd-border-color: hsl(var(--md-hue) 15% 14% / 100%);
|
||||
--md-typeset-table-color: hsl(var(--md-hue) 75% 95% / 12%);
|
||||
|
||||
/* Admonition */
|
||||
--md-admonition-fg-color: var(--md-default-fg-color);
|
||||
--md-admonition-bg-color: var(--md-default-bg-color);
|
||||
|
||||
/* Footer */
|
||||
--md-footer-bg-color: hsl(var(--md-hue) 15% 12% / 87%);
|
||||
--md-footer-bg-color--dark: hsl(var(--md-hue) 15% 10% / 100%);
|
||||
|
||||
/* Shadows */
|
||||
--md-shadow-z1:
|
||||
0 0.2rem 0.50rem rgba(0 0 0 20%),
|
||||
0 0 0.05rem rgba(0 0 0 10%);
|
||||
--md-shadow-z2:
|
||||
0 0.2rem 0.50rem rgba(0 0 0 30%),
|
||||
0 0 0.05rem rgba(0 0 0 25%);
|
||||
--md-shadow-z3:
|
||||
0 0.2rem 0.50rem rgba(0 0 0 40%),
|
||||
0 0 0.05rem rgba(0 0 0 35%);
|
||||
}
|
||||
|
||||
.md-header-nav__button.md-logo {
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.md-header-nav__button.md-logo img {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
}
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
}
|
||||
|
|
|
|||
52
go.mod
52
go.mod
|
|
@ -3,22 +3,21 @@ module github.com/containrrr/watchtower
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/containrrr/shoutrrr v0.6.1
|
||||
github.com/docker/cli v20.10.17+incompatible
|
||||
github.com/containrrr/shoutrrr v0.7.1
|
||||
github.com/docker/cli v20.10.23+incompatible
|
||||
github.com/docker/distribution v2.8.1+incompatible
|
||||
github.com/docker/docker v20.10.17+incompatible
|
||||
github.com/docker/docker v20.10.23+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/onsi/gomega v1.20.2
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/onsi/gomega v1.26.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/net v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -30,16 +29,15 @@ require (
|
|||
github.com/docker/docker-credential-helpers v0.6.1 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
|
|
@ -47,25 +45,23 @@ require (
|
|||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||
golang.org/x/text v0.3.7
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0
|
||||
golang.org/x/time v0.1.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -182,6 +182,10 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
|||
viper.GetString("WATCHTOWER_PORCELAIN"),
|
||||
`Write session results to stdout using a stable versioned format. Supported values: "v1"`)
|
||||
|
||||
flags.String(
|
||||
"log-level",
|
||||
viper.GetString("WATCHTOWER_LOG_LEVEL"),
|
||||
"The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace")
|
||||
}
|
||||
|
||||
// RegisterNotificationFlags that are used by watchtower to send notifications
|
||||
|
|
@ -374,6 +378,7 @@ func SetDefaults() {
|
|||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", 25)
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
|
||||
viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info")
|
||||
}
|
||||
|
||||
// EnvConfig translates the command-line options into environment variables
|
||||
|
|
@ -561,6 +566,23 @@ func ProcessFlagAliases(flags *pflag.FlagSet) {
|
|||
interval, _ := flags.GetInt(`interval`)
|
||||
flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
||||
}
|
||||
|
||||
if flagIsEnabled(flags, `debug`) {
|
||||
flags.Set(`log-level`, `debug`)
|
||||
}
|
||||
|
||||
if flagIsEnabled(flags, `trace`) {
|
||||
flags.Set(`log-level`, `trace`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func flagIsEnabled(flags *pflag.FlagSet, name string) bool {
|
||||
value, err := flags.GetBool(name)
|
||||
if err != nil {
|
||||
log.Fatalf(`The flag %q is not defined`, name)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error {
|
||||
|
|
|
|||
|
|
@ -129,11 +129,6 @@ func TestIsFile(t *testing.T) {
|
|||
assert.True(t, isFile(os.Args[0]), "the currently running binary path should always be considered a file")
|
||||
}
|
||||
|
||||
func TestReadFlags(t *testing.T) {
|
||||
logrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() }
|
||||
|
||||
}
|
||||
|
||||
func TestProcessFlagAliases(t *testing.T) {
|
||||
logrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() }
|
||||
cmd := new(cobra.Command)
|
||||
|
|
@ -145,6 +140,7 @@ func TestProcessFlagAliases(t *testing.T) {
|
|||
require.NoError(t, cmd.ParseFlags([]string{
|
||||
`--porcelain`, `v1`,
|
||||
`--interval`, `10`,
|
||||
`--trace`,
|
||||
}))
|
||||
flags := cmd.Flags()
|
||||
ProcessFlagAliases(flags)
|
||||
|
|
@ -163,6 +159,28 @@ func TestProcessFlagAliases(t *testing.T) {
|
|||
|
||||
sched, _ := flags.GetString(`schedule`)
|
||||
assert.Equal(t, `@every 10s`, sched)
|
||||
|
||||
logLevel, _ := flags.GetString(`log-level`)
|
||||
assert.Equal(t, `trace`, logLevel)
|
||||
}
|
||||
|
||||
func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
err := os.Setenv("WATCHTOWER_DEBUG", `true`)
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_DEBUG")
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
|
||||
require.NoError(t, cmd.ParseFlags([]string{}))
|
||||
flags := cmd.Flags()
|
||||
ProcessFlagAliases(flags)
|
||||
|
||||
logLevel, _ := flags.GetString(`log-level`)
|
||||
assert.Equal(t, `debug`, logLevel)
|
||||
}
|
||||
|
||||
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
||||
|
|
|
|||
13
mkdocs.yml
13
mkdocs.yml
|
|
@ -5,7 +5,16 @@ edit_uri: edit/main/docs/
|
|||
theme:
|
||||
name: 'material'
|
||||
palette:
|
||||
scheme: containrrr
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: containrrr
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: containrrr-dark
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
logo: images/logo-450px.png
|
||||
favicon: images/favicon.ico
|
||||
extra_css:
|
||||
|
|
@ -24,7 +33,7 @@ markdown_extensions:
|
|||
repo: watchtower
|
||||
- pymdownx.saneheaders
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
alternate_style: true
|
||||
nav:
|
||||
- 'Home': 'index.md'
|
||||
- 'Introduction': 'introduction.md'
|
||||
|
|
|
|||
29
pkg/container/cgroup_id.go
Normal file
29
pkg/container/cgroup_id.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
var dockerContainerPattern = regexp.MustCompile(`[0-9]+:.*:/docker/([a-f|0-9]{64})`)
|
||||
|
||||
// GetRunningContainerID tries to resolve the current container ID from the current process cgroup information
|
||||
func GetRunningContainerID() (cid types.ContainerID, err error) {
|
||||
file, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", os.Getpid()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return getRunningContainerIDFromString(string(file)), nil
|
||||
}
|
||||
|
||||
func getRunningContainerIDFromString(s string) types.ContainerID {
|
||||
matches := dockerContainerPattern.FindStringSubmatch(s)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
return types.ContainerID(matches[1])
|
||||
}
|
||||
40
pkg/container/cgroup_id_test.go
Normal file
40
pkg/container/cgroup_id_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("GetRunningContainerID", func() {
|
||||
When("a matching container ID is found", func() {
|
||||
It("should return that container ID", func() {
|
||||
cid := getRunningContainerIDFromString(`
|
||||
15:name=systemd:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
14:misc:/
|
||||
13:rdma:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
12:pids:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
11:hugetlb:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
10:net_prio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
9:perf_event:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
8:net_cls:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
7:freezer:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
6:devices:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
5:blkio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
4:cpuacct:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
3:cpu:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
2:cpuset:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
1:memory:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
0::/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
`)
|
||||
Expect(cid).To(BeEquivalentTo(`991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377`))
|
||||
})
|
||||
})
|
||||
When("no matching container ID could be found", func() {
|
||||
It("should return that container ID", func() {
|
||||
cid := getRunningContainerIDFromString(`14:misc:/`)
|
||||
Expect(cid).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
|
|
@ -192,6 +192,10 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
|
|||
log.Debugf("Removing container %s", shortID)
|
||||
|
||||
if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.RemoveVolumes}); err != nil {
|
||||
if sdkClient.IsErrNotFound(err) {
|
||||
log.Debugf("Container %s not found, skipping removal.", shortID)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/filters"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
|
|
@ -33,8 +35,8 @@ var _ = Describe("the client", func() {
|
|||
mockServer.Close()
|
||||
})
|
||||
Describe("WarnOnHeadPullFailed", func() {
|
||||
containerUnknown := *mockContainerWithImageName("unknown.repo/prefix/imagename:latest")
|
||||
containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest")
|
||||
containerUnknown := *MockContainer(WithImageName("unknown.repo/prefix/imagename:latest"))
|
||||
containerKnown := *MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
|
||||
|
||||
When(`warn on head failure is set to "always"`, func() {
|
||||
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}}
|
||||
|
|
@ -64,11 +66,43 @@ var _ = Describe("the client", func() {
|
|||
When("the image consist of a pinned hash", func() {
|
||||
It("should gracefully fail with a useful message", func() {
|
||||
c := dockerClient{}
|
||||
pinnedContainer := *mockContainerWithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b")
|
||||
pinnedContainer := *MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b"))
|
||||
c.PullImage(context.Background(), pinnedContainer)
|
||||
})
|
||||
})
|
||||
})
|
||||
When("removing a running container", func() {
|
||||
When("the container still exist after stopping", func() {
|
||||
It("should attempt to remove the container", func() {
|
||||
container := *MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
||||
containerStopped := *MockContainer(WithContainerState(types.ContainerState{Running: false}))
|
||||
|
||||
cid := container.ContainerInfo().ID
|
||||
mockServer.AppendHandlers(
|
||||
mocks.KillContainerHandler(cid, mocks.Found),
|
||||
mocks.GetContainerHandler(cid, containerStopped.ContainerInfo()),
|
||||
mocks.RemoveContainerHandler(cid, mocks.Found),
|
||||
mocks.GetContainerHandler(cid, nil),
|
||||
)
|
||||
|
||||
Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())
|
||||
})
|
||||
})
|
||||
When("the container does not exist after stopping", func() {
|
||||
It("should not cause an error", func() {
|
||||
container := *MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
||||
|
||||
cid := container.ContainerInfo().ID
|
||||
mockServer.AppendHandlers(
|
||||
mocks.KillContainerHandler(cid, mocks.Found),
|
||||
mocks.GetContainerHandler(cid, nil),
|
||||
mocks.RemoveContainerHandler(cid, mocks.Missing),
|
||||
)
|
||||
|
||||
Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
When("listing containers", func() {
|
||||
When("no filter is provided", func() {
|
||||
It("should return all available containers", func() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// Package container contains code related to dealing with docker containers
|
||||
package container
|
||||
|
||||
import (
|
||||
|
|
|
|||
66
pkg/container/container_mock_test.go
Normal file
66
pkg/container/container_mock_test.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerContainer "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
type MockContainerUpdate func(*types.ContainerJSON, *types.ImageInspect)
|
||||
|
||||
func MockContainer(updates ...MockContainerUpdate) *Container {
|
||||
containerInfo := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: "container_id",
|
||||
Image: "image",
|
||||
Name: "test-containrrr",
|
||||
HostConfig: &dockerContainer.HostConfig{},
|
||||
},
|
||||
Config: &dockerContainer.Config{
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
}
|
||||
image := types.ImageInspect{
|
||||
ID: "image_id",
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
update(&containerInfo, &image)
|
||||
}
|
||||
return NewContainer(&containerInfo, &image)
|
||||
}
|
||||
|
||||
func WithPortBindings(portBindingSources ...string) MockContainerUpdate {
|
||||
return func(c *types.ContainerJSON, i *types.ImageInspect) {
|
||||
portBindings := nat.PortMap{}
|
||||
for _, pbs := range portBindingSources {
|
||||
portBindings[nat.Port(pbs)] = []nat.PortBinding{}
|
||||
}
|
||||
c.HostConfig.PortBindings = portBindings
|
||||
}
|
||||
}
|
||||
|
||||
func WithImageName(name string) MockContainerUpdate {
|
||||
return func(c *types.ContainerJSON, i *types.ImageInspect) {
|
||||
c.Config.Image = name
|
||||
i.RepoTags = append(i.RepoTags, name)
|
||||
}
|
||||
}
|
||||
|
||||
func WithLinks(links []string) MockContainerUpdate {
|
||||
return func(c *types.ContainerJSON, i *types.ImageInspect) {
|
||||
c.HostConfig.Links = links
|
||||
}
|
||||
}
|
||||
|
||||
func WithLabels(labels map[string]string) MockContainerUpdate {
|
||||
return func(c *types.ContainerJSON, i *types.ImageInspect) {
|
||||
c.Config.Labels = labels
|
||||
}
|
||||
}
|
||||
|
||||
func WithContainerState(state types.ContainerState) MockContainerUpdate {
|
||||
return func(cnt *types.ContainerJSON, img *types.ImageInspect) {
|
||||
cnt.State = &state
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
|
@ -12,7 +10,7 @@ var _ = Describe("the container", func() {
|
|||
Describe("VerifyConfiguration", func() {
|
||||
When("verifying a container with no image info", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
c.imageInfo = nil
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).To(Equal(errorNoImageInfo))
|
||||
|
|
@ -20,7 +18,7 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with no container info", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
c.containerInfo = nil
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).To(Equal(errorNoContainerInfo))
|
||||
|
|
@ -28,7 +26,7 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with no config", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
c.containerInfo.Config = nil
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).To(Equal(errorInvalidConfig))
|
||||
|
|
@ -36,7 +34,7 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with no host config", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
c.containerInfo.HostConfig = nil
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).To(Equal(errorInvalidConfig))
|
||||
|
|
@ -44,14 +42,14 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with no port bindings", func() {
|
||||
It("should not return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
When("verifying a container with port bindings, but no exposed ports", func() {
|
||||
It("should make the config compatible with updating", func() {
|
||||
c := mockContainerWithPortBindings("80/tcp")
|
||||
c := MockContainer(WithPortBindings("80/tcp"))
|
||||
c.containerInfo.Config.ExposedPorts = nil
|
||||
Expect(c.VerifyConfiguration()).To(Succeed())
|
||||
|
||||
|
|
@ -61,7 +59,7 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with port bindings and exposed ports is non-nil", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings("80/tcp")
|
||||
c := MockContainer(WithPortBindings("80/tcp"))
|
||||
c.containerInfo.Config.ExposedPorts = map[nat.Port]struct{}{"80/tcp": {}}
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
|
@ -71,10 +69,10 @@ var _ = Describe("the container", func() {
|
|||
When("asked for metadata", func() {
|
||||
var c *Container
|
||||
BeforeEach(func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.enable": "true",
|
||||
"com.centurylinklabs.watchtower": "true",
|
||||
})
|
||||
}))
|
||||
})
|
||||
It("should return its name on calls to .Name()", func() {
|
||||
name := c.Name()
|
||||
|
|
@ -91,36 +89,28 @@ var _ = Describe("the container", func() {
|
|||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeTrue())
|
||||
Expect(enabled).NotTo(BeFalse())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(exists).NotTo(BeFalse())
|
||||
})
|
||||
It("should return false, true if present but not true on calls to .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"})
|
||||
c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"}))
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(exists).NotTo(BeFalse())
|
||||
})
|
||||
It("should return false, false if not present on calls to .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"lol": "false"})
|
||||
c = MockContainer(WithLabels(map[string]string{"lol": "false"}))
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(exists).NotTo(BeTrue())
|
||||
})
|
||||
It("should return false, false if present but not parsable .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"})
|
||||
c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"}))
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(exists).NotTo(BeTrue())
|
||||
})
|
||||
When("checking if its a watchtower instance", func() {
|
||||
It("should return true if the label is set to true", func() {
|
||||
|
|
@ -128,31 +118,31 @@ var _ = Describe("the container", func() {
|
|||
Expect(isWatchtower).To(BeTrue())
|
||||
})
|
||||
It("should return false if the label is present but set to false", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"})
|
||||
c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"}))
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
It("should return false if the label is not present", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"funny.label": "false"})
|
||||
c = MockContainer(WithLabels(map[string]string{"funny.label": "false"}))
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
It("should return false if there are no labels", func() {
|
||||
c = mockContainerWithLabels(map[string]string{})
|
||||
c = MockContainer(WithLabels(map[string]string{}))
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
})
|
||||
When("fetching the custom stop signal", func() {
|
||||
It("should return the signal if its set", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.stop-signal": "SIGKILL",
|
||||
})
|
||||
}))
|
||||
stopSignal := c.StopSignal()
|
||||
Expect(stopSignal).To(Equal("SIGKILL"))
|
||||
})
|
||||
It("should return an empty string if its not set", func() {
|
||||
c = mockContainerWithLabels(map[string]string{})
|
||||
c = MockContainer(WithLabels(map[string]string{}))
|
||||
stopSignal := c.StopSignal()
|
||||
Expect(stopSignal).To(Equal(""))
|
||||
})
|
||||
|
|
@ -160,22 +150,22 @@ var _ = Describe("the container", func() {
|
|||
When("fetching the image name", func() {
|
||||
When("the zodiac label is present", func() {
|
||||
It("should fetch the image name from it", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.zodiac.original-image": "the-original-image",
|
||||
})
|
||||
}))
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(imageName))
|
||||
})
|
||||
})
|
||||
It("should return the image name", func() {
|
||||
name := "image-name:3"
|
||||
c = mockContainerWithImageName(name)
|
||||
c = MockContainer(WithImageName(name))
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(name))
|
||||
})
|
||||
It("should assume latest if no tag is supplied", func() {
|
||||
name := "image-name"
|
||||
c = mockContainerWithImageName(name)
|
||||
c = MockContainer(WithImageName(name))
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(name + ":latest"))
|
||||
})
|
||||
|
|
@ -184,33 +174,33 @@ var _ = Describe("the container", func() {
|
|||
When("fetching container links", func() {
|
||||
When("the depends on label is present", func() {
|
||||
It("should fetch depending containers from it", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "postgres",
|
||||
})
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("postgres"), HaveLen(1)))
|
||||
})
|
||||
It("should fetch depending containers if there are many", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "postgres,redis",
|
||||
})
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("postgres"), ContainElement("redis"), HaveLen(2)))
|
||||
})
|
||||
It("should fetch depending containers if label is blank", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "",
|
||||
})
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
When("the depends on label is not present", func() {
|
||||
It("should fetch depending containers from host config links", func() {
|
||||
c = mockContainerWithLinks([]string{
|
||||
c = MockContainer(WithLinks([]string{
|
||||
"redis:test-containrrr",
|
||||
"postgres:test-containrrr",
|
||||
})
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("redis"), ContainElement("postgres"), HaveLen(2)))
|
||||
})
|
||||
|
|
@ -219,10 +209,10 @@ var _ = Describe("the container", func() {
|
|||
|
||||
When("there is a pre or post update timeout", func() {
|
||||
It("should return minute values", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "3",
|
||||
"com.centurylinklabs.watchtower.lifecycle.post-update-timeout": "5",
|
||||
})
|
||||
}))
|
||||
preTimeout := c.PreUpdateTimeout()
|
||||
Expect(preTimeout).To(Equal(3))
|
||||
postTimeout := c.PostUpdateTimeout()
|
||||
|
|
@ -232,53 +222,3 @@ var _ = Describe("the container", func() {
|
|||
|
||||
})
|
||||
})
|
||||
|
||||
func mockContainerWithPortBindings(portBindingSources ...string) *Container {
|
||||
mockContainer := mockContainerWithLabels(nil)
|
||||
mockContainer.imageInfo = &types.ImageInspect{}
|
||||
hostConfig := &container.HostConfig{
|
||||
PortBindings: nat.PortMap{},
|
||||
}
|
||||
for _, pbs := range portBindingSources {
|
||||
hostConfig.PortBindings[nat.Port(pbs)] = []nat.PortBinding{}
|
||||
}
|
||||
mockContainer.containerInfo.HostConfig = hostConfig
|
||||
return mockContainer
|
||||
}
|
||||
|
||||
func mockContainerWithImageName(name string) *Container {
|
||||
mockContainer := mockContainerWithLabels(nil)
|
||||
mockContainer.containerInfo.Config.Image = name
|
||||
return mockContainer
|
||||
}
|
||||
|
||||
func mockContainerWithLinks(links []string) *Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: "container_id",
|
||||
Image: "image",
|
||||
Name: "test-containrrr",
|
||||
HostConfig: &container.HostConfig{
|
||||
Links: links,
|
||||
},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
}
|
||||
return NewContainer(&content, nil)
|
||||
}
|
||||
|
||||
func mockContainerWithLabels(labels map[string]string) *Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: "container_id",
|
||||
Image: "image",
|
||||
Name: "test-containrrr",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: labels,
|
||||
},
|
||||
}
|
||||
return NewContainer(&content, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ package mocks
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
O "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/ghttp"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
O "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/ghttp"
|
||||
)
|
||||
|
||||
func getMockJSONFile(relPath string) ([]byte, error) {
|
||||
|
|
@ -42,14 +43,14 @@ func respondWithJSONFile(relPath string, statusCode int, optionalHeader ...http.
|
|||
func GetContainerHandlers(containerFiles ...string) []http.HandlerFunc {
|
||||
handlers := make([]http.HandlerFunc, 0, len(containerFiles)*2)
|
||||
for _, file := range containerFiles {
|
||||
handlers = append(handlers, getContainerHandler(file))
|
||||
handlers = append(handlers, getContainerFileHandler(file))
|
||||
|
||||
// Also append the image request since that will be called for every container
|
||||
if file == "running" {
|
||||
// The "running" container is the only one using image02
|
||||
handlers = append(handlers, getImageHandler(1))
|
||||
handlers = append(handlers, getImageFileHandler(1))
|
||||
} else {
|
||||
handlers = append(handlers, getImageHandler(0))
|
||||
handlers = append(handlers, getImageFileHandler(0))
|
||||
}
|
||||
}
|
||||
return handlers
|
||||
|
|
@ -75,15 +76,36 @@ var imageIds = []string{
|
|||
"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
|
||||
}
|
||||
|
||||
func getContainerHandler(file string) http.HandlerFunc {
|
||||
func getContainerFileHandler(file string) http.HandlerFunc {
|
||||
id, ok := containerFileIds[file]
|
||||
failTestUnless(ok)
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", id)),
|
||||
return getContainerHandler(
|
||||
id,
|
||||
RespondWithJSONFile(fmt.Sprintf("./mocks/data/container_%v.json", file), http.StatusOK),
|
||||
)
|
||||
}
|
||||
|
||||
func getContainerHandler(containerId string, responseHandler http.HandlerFunc) http.HandlerFunc {
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", containerId)),
|
||||
responseHandler,
|
||||
)
|
||||
}
|
||||
|
||||
// GetContainerHandler mocks the GET containers/{id}/json endpoint
|
||||
func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) http.HandlerFunc {
|
||||
responseHandler := containerNotFoundResponse(containerID)
|
||||
if containerInfo != nil {
|
||||
responseHandler = ghttp.RespondWithJSONEncoded(http.StatusOK, containerInfo)
|
||||
}
|
||||
return getContainerHandler(containerID, responseHandler)
|
||||
}
|
||||
|
||||
// GetImageHandler mocks the GET images/{id}/json endpoint
|
||||
func GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc {
|
||||
return getImageHandler(imageInfo.ID, ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo))
|
||||
}
|
||||
|
||||
// ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses
|
||||
func ListContainersHandler(statuses ...string) http.HandlerFunc {
|
||||
filterArgs := createFilterArgs(statuses)
|
||||
|
|
@ -116,9 +138,15 @@ func respondWithFilteredContainers(filters filters.Args) http.HandlerFunc {
|
|||
return ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers)
|
||||
}
|
||||
|
||||
func getImageHandler(index int) http.HandlerFunc {
|
||||
func getImageHandler(imageId string, responseHandler http.HandlerFunc) http.HandlerFunc {
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%v/json", imageIds[index])),
|
||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%s/json", imageId)),
|
||||
responseHandler,
|
||||
)
|
||||
}
|
||||
|
||||
func getImageFileHandler(index int) http.HandlerFunc {
|
||||
return getImageHandler(imageIds[index],
|
||||
RespondWithJSONFile(fmt.Sprintf("./mocks/data/image%02d.json", index+1), http.StatusOK),
|
||||
)
|
||||
}
|
||||
|
|
@ -126,3 +154,40 @@ func getImageHandler(index int) http.HandlerFunc {
|
|||
func failTestUnless(ok bool) {
|
||||
O.ExpectWithOffset(2, ok).To(O.BeTrue(), "test setup failed")
|
||||
}
|
||||
|
||||
// KillContainerHandler mocks the POST containers/{id}/kill endpoint
|
||||
func KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
|
||||
responseHandler := noContentStatusResponse
|
||||
if !found {
|
||||
responseHandler = containerNotFoundResponse(containerID)
|
||||
}
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("POST", O.HaveSuffix("containers/%s/kill", containerID)),
|
||||
responseHandler,
|
||||
)
|
||||
}
|
||||
|
||||
// RemoveContainerHandler mocks the DELETE containers/{id} endpoint
|
||||
func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
|
||||
responseHandler := noContentStatusResponse
|
||||
if !found {
|
||||
responseHandler = containerNotFoundResponse(containerID)
|
||||
}
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("DELETE", O.HaveSuffix("containers/%s", containerID)),
|
||||
responseHandler,
|
||||
)
|
||||
}
|
||||
|
||||
func containerNotFoundResponse(containerID string) http.HandlerFunc {
|
||||
return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + containerID})
|
||||
}
|
||||
|
||||
var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)
|
||||
|
||||
type FoundStatus bool
|
||||
|
||||
const (
|
||||
Found FoundStatus = true
|
||||
Missing FoundStatus = false
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,5 +35,6 @@ var commonTemplates = map[string]string{
|
|||
no containers matched filter
|
||||
{{- end -}}
|
||||
{{- end -}}`,
|
||||
}
|
||||
|
||||
`json.v1`: `{{ . | ToJSON }}`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,17 +15,16 @@ const (
|
|||
)
|
||||
|
||||
type emailTypeNotifier struct {
|
||||
From, To string
|
||||
Server, User, Password, SubjectTag string
|
||||
Port int
|
||||
tlsSkipVerify bool
|
||||
entries []*log.Entry
|
||||
logLevels []log.Level
|
||||
delay time.Duration
|
||||
From, To string
|
||||
Server, User, Password string
|
||||
Port int
|
||||
tlsSkipVerify bool
|
||||
entries []*log.Entry
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
func newEmailNotifier(c *cobra.Command) t.ConvertibleNotifier {
|
||||
flags := c.Flags()
|
||||
|
||||
from, _ := flags.GetString("notification-email-from")
|
||||
to, _ := flags.GetString("notification-email-to")
|
||||
|
|
@ -35,7 +34,6 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
|
|||
port, _ := flags.GetInt("notification-email-server-port")
|
||||
tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify")
|
||||
delay, _ := flags.GetInt("notification-email-delay")
|
||||
subjecttag, _ := flags.GetString("notification-email-subjecttag")
|
||||
|
||||
n := &emailTypeNotifier{
|
||||
entries: []*log.Entry{},
|
||||
|
|
@ -46,28 +44,26 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
|
|||
Password: password,
|
||||
Port: port,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
logLevels: acceptedLogLevels,
|
||||
delay: time.Duration(delay) * time.Second,
|
||||
SubjectTag: subjecttag,
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) {
|
||||
conf := &shoutrrrSmtp.Config{
|
||||
FromAddress: e.From,
|
||||
FromName: "Watchtower",
|
||||
ToAddresses: []string{e.To},
|
||||
Port: uint16(e.Port),
|
||||
Host: e.Server,
|
||||
Subject: e.getSubject(c, title),
|
||||
Username: e.User,
|
||||
Password: e.Password,
|
||||
UseStartTLS: !e.tlsSkipVerify,
|
||||
UseHTML: false,
|
||||
Encryption: shoutrrrSmtp.EncMethods.Auto,
|
||||
Auth: shoutrrrSmtp.AuthTypes.None,
|
||||
ClientHost: "localhost",
|
||||
}
|
||||
|
||||
if len(e.User) > 0 {
|
||||
|
|
@ -84,11 +80,3 @@ func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
|
|||
func (e *emailTypeNotifier) GetDelay() time.Duration {
|
||||
return e.delay
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) getSubject(_ *cobra.Command, title string) string {
|
||||
if e.SubjectTag != "" {
|
||||
return e.SubjectTag + " " + title
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,10 @@ type gotifyTypeNotifier struct {
|
|||
gotifyURL string
|
||||
gotifyAppToken string
|
||||
gotifyInsecureSkipVerify bool
|
||||
logLevels []log.Level
|
||||
}
|
||||
|
||||
func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
func newGotifyNotifier(c *cobra.Command) t.ConvertibleNotifier {
|
||||
flags := c.Flags()
|
||||
|
||||
apiURL := getGotifyURL(flags)
|
||||
token := getGotifyToken(flags)
|
||||
|
|
@ -34,7 +33,6 @@ func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifi
|
|||
gotifyURL: apiURL,
|
||||
gotifyAppToken: token,
|
||||
gotifyInsecureSkipVerify: skipVerify,
|
||||
logLevels: levels,
|
||||
}
|
||||
|
||||
return n
|
||||
|
|
@ -62,7 +60,7 @@ func getGotifyURL(flags *pflag.FlagSet) string {
|
|||
return gotifyURL
|
||||
}
|
||||
|
||||
func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
func (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) {
|
||||
apiURL, err := url.Parse(n.gotifyURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -72,7 +70,6 @@ func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, err
|
|||
Host: apiURL.Host,
|
||||
Path: apiURL.Path,
|
||||
DisableTLS: apiURL.Scheme == "http",
|
||||
Title: title,
|
||||
Token: n.gotifyAppToken,
|
||||
}
|
||||
|
||||
|
|
|
|||
71
pkg/notifications/json.go
Normal file
71
pkg/notifications/json.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
type jsonMap = map[string]interface{}
|
||||
|
||||
// MarshalJSON implements json.Marshaler
|
||||
func (d Data) MarshalJSON() ([]byte, error) {
|
||||
var entries = make([]jsonMap, len(d.Entries))
|
||||
for i, entry := range d.Entries {
|
||||
entries[i] = jsonMap{
|
||||
`level`: entry.Level,
|
||||
`message`: entry.Message,
|
||||
`data`: entry.Data,
|
||||
`time`: entry.Time,
|
||||
}
|
||||
}
|
||||
|
||||
var report jsonMap
|
||||
if d.Report != nil {
|
||||
report = jsonMap{
|
||||
`scanned`: marshalReports(d.Report.Scanned()),
|
||||
`updated`: marshalReports(d.Report.Updated()),
|
||||
`failed`: marshalReports(d.Report.Failed()),
|
||||
`skipped`: marshalReports(d.Report.Skipped()),
|
||||
`stale`: marshalReports(d.Report.Stale()),
|
||||
`fresh`: marshalReports(d.Report.Fresh()),
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(jsonMap{
|
||||
`report`: report,
|
||||
`title`: d.Title,
|
||||
`host`: d.Host,
|
||||
`entries`: entries,
|
||||
})
|
||||
}
|
||||
|
||||
func marshalReports(reports []t.ContainerReport) []jsonMap {
|
||||
jsonReports := make([]jsonMap, len(reports))
|
||||
for i, report := range reports {
|
||||
jsonReports[i] = jsonMap{
|
||||
`id`: report.ID().ShortID(),
|
||||
`name`: report.Name(),
|
||||
`currentImageId`: report.CurrentImageID().ShortID(),
|
||||
`latestImageId`: report.LatestImageID().ShortID(),
|
||||
`imageName`: report.ImageName(),
|
||||
`state`: report.State(),
|
||||
}
|
||||
if errorMessage := report.Error(); errorMessage != "" {
|
||||
jsonReports[i][`error`] = errorMessage
|
||||
}
|
||||
}
|
||||
return jsonReports
|
||||
}
|
||||
|
||||
var _ json.Marshaler = &Data{}
|
||||
|
||||
func toJSON(v interface{}) string {
|
||||
var bytes []byte
|
||||
var err error
|
||||
if bytes, err = json.MarshalIndent(v, "", " "); err != nil {
|
||||
LocalLog.Errorf("failed to marshal JSON in notification template: %v", err)
|
||||
return ""
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
118
pkg/notifications/json_test.go
Normal file
118
pkg/notifications/json_test.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
s "github.com/containrrr/watchtower/pkg/session"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("JSON template", func() {
|
||||
When("using report templates", func() {
|
||||
When("JSON template is used", func() {
|
||||
It("should format the messages to the expected format", func() {
|
||||
expected := `{
|
||||
"entries": [
|
||||
{
|
||||
"data": null,
|
||||
"level": "info",
|
||||
"message": "foo Bar",
|
||||
"time": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"host": "Mock",
|
||||
"report": {
|
||||
"failed": [
|
||||
{
|
||||
"currentImageId": "01d210000000",
|
||||
"error": "accidentally the whole container",
|
||||
"id": "c79210000000",
|
||||
"imageName": "mock/fail1:latest",
|
||||
"latestImageId": "d0a210000000",
|
||||
"name": "fail1",
|
||||
"state": "Failed"
|
||||
}
|
||||
],
|
||||
"fresh": [
|
||||
{
|
||||
"currentImageId": "01d310000000",
|
||||
"id": "c79310000000",
|
||||
"imageName": "mock/frsh1:latest",
|
||||
"latestImageId": "01d310000000",
|
||||
"name": "frsh1",
|
||||
"state": "Fresh"
|
||||
}
|
||||
],
|
||||
"scanned": [
|
||||
{
|
||||
"currentImageId": "01d110000000",
|
||||
"id": "c79110000000",
|
||||
"imageName": "mock/updt1:latest",
|
||||
"latestImageId": "d0a110000000",
|
||||
"name": "updt1",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d120000000",
|
||||
"id": "c79120000000",
|
||||
"imageName": "mock/updt2:latest",
|
||||
"latestImageId": "d0a120000000",
|
||||
"name": "updt2",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d210000000",
|
||||
"error": "accidentally the whole container",
|
||||
"id": "c79210000000",
|
||||
"imageName": "mock/fail1:latest",
|
||||
"latestImageId": "d0a210000000",
|
||||
"name": "fail1",
|
||||
"state": "Failed"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d310000000",
|
||||
"id": "c79310000000",
|
||||
"imageName": "mock/frsh1:latest",
|
||||
"latestImageId": "01d310000000",
|
||||
"name": "frsh1",
|
||||
"state": "Fresh"
|
||||
}
|
||||
],
|
||||
"skipped": [
|
||||
{
|
||||
"currentImageId": "01d410000000",
|
||||
"error": "unpossible",
|
||||
"id": "c79410000000",
|
||||
"imageName": "mock/skip1:latest",
|
||||
"latestImageId": "01d410000000",
|
||||
"name": "skip1",
|
||||
"state": "Skipped"
|
||||
}
|
||||
],
|
||||
"stale": [],
|
||||
"updated": [
|
||||
{
|
||||
"currentImageId": "01d110000000",
|
||||
"id": "c79110000000",
|
||||
"imageName": "mock/updt1:latest",
|
||||
"latestImageId": "d0a110000000",
|
||||
"name": "updt1",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d120000000",
|
||||
"id": "c79120000000",
|
||||
"imageName": "mock/updt2:latest",
|
||||
"latestImageId": "d0a120000000",
|
||||
"name": "updt2",
|
||||
"state": "Updated"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "Watchtower updates on Mock"
|
||||
}`
|
||||
data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState)
|
||||
Expect(getTemplatedResult(`json.v1`, false, data)).To(MatchJSON(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
19
pkg/notifications/model.go
Normal file
19
pkg/notifications/model.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StaticData is the part of the notification template data model set upon initialization
|
||||
type StaticData struct {
|
||||
Title string
|
||||
Host string
|
||||
}
|
||||
|
||||
// Data is the notification template data model
|
||||
type Data struct {
|
||||
StaticData
|
||||
Entries []*log.Entry
|
||||
Report t.Report
|
||||
}
|
||||
|
|
@ -15,13 +15,12 @@ const (
|
|||
|
||||
type msTeamsTypeNotifier struct {
|
||||
webHookURL string
|
||||
levels []log.Level
|
||||
data bool
|
||||
}
|
||||
|
||||
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
func newMsTeamsNotifier(cmd *cobra.Command) t.ConvertibleNotifier {
|
||||
|
||||
flags := cmd.PersistentFlags()
|
||||
flags := cmd.Flags()
|
||||
|
||||
webHookURL, _ := flags.GetString("notification-msteams-hook")
|
||||
if len(webHookURL) <= 0 {
|
||||
|
|
@ -30,7 +29,6 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con
|
|||
|
||||
withData, _ := flags.GetBool("notification-msteams-data")
|
||||
n := &msTeamsTypeNotifier{
|
||||
levels: acceptedLogLevels,
|
||||
webHookURL: webHookURL,
|
||||
data: withData,
|
||||
}
|
||||
|
|
@ -38,7 +36,7 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con
|
|||
return n
|
||||
}
|
||||
|
||||
func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) {
|
||||
webhookURL, err := url.Parse(n.webHookURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -50,7 +48,6 @@ func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, er
|
|||
}
|
||||
|
||||
config.Color = ColorHex
|
||||
config.Title = title
|
||||
|
||||
return config.GetURL().String(), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ import (
|
|||
"time"
|
||||
|
||||
ty "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/johntdyer/slackrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewNotifier creates and returns a new Notifier, using global configuration.
|
||||
func NewNotifier(c *cobra.Command) ty.Notifier {
|
||||
f := c.PersistentFlags()
|
||||
f := c.Flags()
|
||||
|
||||
level, _ := f.GetString("notifications-level")
|
||||
logLevel, err := log.ParseLevel(level)
|
||||
|
|
@ -21,25 +20,19 @@ func NewNotifier(c *cobra.Command) ty.Notifier {
|
|||
log.Fatalf("Notifications invalid log level: %s", err.Error())
|
||||
}
|
||||
|
||||
levels := slackrus.LevelThreshold(logLevel)
|
||||
// slackrus does not allow log level TRACE, even though it's an accepted log level for logrus
|
||||
if len(levels) == 0 {
|
||||
log.Fatalf("Unsupported notification log level provided: %s", level)
|
||||
}
|
||||
|
||||
reportTemplate, _ := f.GetBool("notification-report")
|
||||
stdout, _ := f.GetBool("notification-log-stdout")
|
||||
tplString, _ := f.GetString("notification-template")
|
||||
urls, _ := f.GetStringArray("notification-url")
|
||||
|
||||
data := GetTemplateData(c)
|
||||
urls, delay := AppendLegacyUrls(urls, c, data.Title)
|
||||
urls, delay := AppendLegacyUrls(urls, c)
|
||||
|
||||
return newShoutrrrNotifier(tplString, levels, !reportTemplate, data, delay, stdout, urls...)
|
||||
return createNotifier(urls, logLevel, tplString, !reportTemplate, data, stdout, delay)
|
||||
}
|
||||
|
||||
// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags
|
||||
func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string, time.Duration) {
|
||||
func AppendLegacyUrls(urls []string, cmd *cobra.Command) ([]string, time.Duration) {
|
||||
|
||||
// Parse types and create notifiers.
|
||||
types, err := cmd.Flags().GetStringSlice("notifications")
|
||||
|
|
@ -56,13 +49,13 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string
|
|||
|
||||
switch t {
|
||||
case emailType:
|
||||
legacyNotifier = newEmailNotifier(cmd, []log.Level{})
|
||||
legacyNotifier = newEmailNotifier(cmd)
|
||||
case slackType:
|
||||
legacyNotifier = newSlackNotifier(cmd, []log.Level{})
|
||||
legacyNotifier = newSlackNotifier(cmd)
|
||||
case msTeamsType:
|
||||
legacyNotifier = newMsTeamsNotifier(cmd, []log.Level{})
|
||||
legacyNotifier = newMsTeamsNotifier(cmd)
|
||||
case gotifyType:
|
||||
legacyNotifier = newGotifyNotifier(cmd, []log.Level{})
|
||||
legacyNotifier = newGotifyNotifier(cmd)
|
||||
case shoutrrrType:
|
||||
continue
|
||||
default:
|
||||
|
|
@ -71,7 +64,7 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string
|
|||
continue
|
||||
}
|
||||
|
||||
shoutrrrURL, err := legacyNotifier.GetURL(cmd, title)
|
||||
shoutrrrURL, err := legacyNotifier.GetURL(cmd)
|
||||
if err != nil {
|
||||
log.Fatal("failed to create notification config: ", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package notifications_test
|
|||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/cmd"
|
||||
|
|
@ -147,11 +146,9 @@ var _ = Describe("notifications", func() {
|
|||
channel := "123456789"
|
||||
token := "abvsihdbau"
|
||||
color := notifications.ColorInt
|
||||
data := notifications.GetTemplateData(command)
|
||||
title := url.QueryEscape(data.Title)
|
||||
username := "containrrrbot"
|
||||
iconURL := "https://containrrr.dev/watchtower-sq180.png"
|
||||
expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&title=%s&username=watchtower", token, channel, color, title)
|
||||
expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=watchtower", token, channel, color)
|
||||
buildArgs := func(url string) []string {
|
||||
return []string{
|
||||
"--notifications",
|
||||
|
|
@ -172,7 +169,7 @@ var _ = Describe("notifications", func() {
|
|||
When("icon URL and username are specified", func() {
|
||||
It("should return the expected URL", func() {
|
||||
hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token)
|
||||
expectedOutput := fmt.Sprintf("discord://%s@%s?avatar=%s&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&title=%s&username=%s", token, channel, url.QueryEscape(iconURL), color, title, username)
|
||||
expectedOutput := fmt.Sprintf("discord://%s@%s?avatar=%s&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=%s", token, channel, url.QueryEscape(iconURL), color, username)
|
||||
expectedDelay := time.Duration(7) * time.Second
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
|
@ -199,8 +196,6 @@ var _ = Describe("notifications", func() {
|
|||
tokenB := "BBBBBBBBB"
|
||||
tokenC := "123456789123456789123456"
|
||||
color := url.QueryEscape(notifications.ColorHex)
|
||||
data := notifications.GetTemplateData(command)
|
||||
title := url.QueryEscape(data.Title)
|
||||
iconURL := "https://containrrr.dev/watchtower-sq180.png"
|
||||
iconEmoji := "whale"
|
||||
|
||||
|
|
@ -208,7 +203,7 @@ var _ = Describe("notifications", func() {
|
|||
It("should return the expected URL", func() {
|
||||
|
||||
hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC)
|
||||
expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s&title=%s", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL), title)
|
||||
expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL))
|
||||
expectedDelay := time.Duration(7) * time.Second
|
||||
|
||||
args := []string{
|
||||
|
|
@ -231,7 +226,7 @@ var _ = Describe("notifications", func() {
|
|||
When("icon emoji is specified", func() {
|
||||
It("should return the expected URL", func() {
|
||||
hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC)
|
||||
expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s&title=%s", tokenA, tokenB, tokenC, username, color, iconEmoji, title)
|
||||
expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s", tokenA, tokenB, tokenC, username, color, iconEmoji)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
|
@ -258,10 +253,8 @@ var _ = Describe("notifications", func() {
|
|||
|
||||
token := "aaa"
|
||||
host := "shoutrrr.local"
|
||||
data := notifications.GetTemplateData(command)
|
||||
title := url.QueryEscape(data.Title)
|
||||
|
||||
expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title)
|
||||
expectedOutput := fmt.Sprintf("gotify://%s/%s?title=", host, token)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
|
@ -287,11 +280,9 @@ var _ = Describe("notifications", func() {
|
|||
tokenB := "33333333012222222222333333333344"
|
||||
tokenC := "44444444-4444-4444-8444-cccccccccccc"
|
||||
color := url.QueryEscape(notifications.ColorHex)
|
||||
data := notifications.GetTemplateData(command)
|
||||
title := url.QueryEscape(data.Title)
|
||||
|
||||
hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC)
|
||||
expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title)
|
||||
expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s", tokenA, tokenB, tokenC, color)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
|
@ -362,18 +353,12 @@ var _ = Describe("notifications", func() {
|
|||
})
|
||||
|
||||
func buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string {
|
||||
hostname, err := os.Hostname()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
subject := fmt.Sprintf("Watchtower updates on %s", hostname)
|
||||
|
||||
var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=%s&toaddresses=%s"
|
||||
var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=&toaddresses=%s"
|
||||
return fmt.Sprintf(template,
|
||||
url.QueryEscape(username),
|
||||
url.QueryEscape(password),
|
||||
host, port, auth,
|
||||
url.QueryEscape(from),
|
||||
url.QueryEscape(subject),
|
||||
url.QueryEscape(to))
|
||||
}
|
||||
|
||||
|
|
@ -385,8 +370,7 @@ func testURL(args []string, expectedURL string, expectedDelay time.Duration) {
|
|||
|
||||
Expect(command.ParseFlags(args)).To(Succeed())
|
||||
|
||||
data := notifications.GetTemplateData(command)
|
||||
urls, delay := notifications.AppendLegacyUrls([]string{}, command, data.Title)
|
||||
urls, delay := notifications.AppendLegacyUrls([]string{}, command)
|
||||
|
||||
Expect(urls).To(ContainElement(expectedURL))
|
||||
Expect(delay).To(Equal(expectedDelay))
|
||||
|
|
|
|||
|
|
@ -32,13 +32,15 @@ type shoutrrrTypeNotifier struct {
|
|||
Urls []string
|
||||
Router router
|
||||
entries []*log.Entry
|
||||
logLevels []log.Level
|
||||
logLevel log.Level
|
||||
template *template.Template
|
||||
messages chan string
|
||||
done chan bool
|
||||
legacyTemplate bool
|
||||
params *types.Params
|
||||
data StaticData
|
||||
receiving bool
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// GetScheme returns the scheme part of a Shoutrrr URL
|
||||
|
|
@ -59,18 +61,24 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
|
|||
return names
|
||||
}
|
||||
|
||||
func newShoutrrrNotifier(tplString string, levels []log.Level, legacy bool, data StaticData, delay time.Duration, stdout bool, urls ...string) t.Notifier {
|
||||
|
||||
notifier := createNotifier(urls, levels, tplString, legacy, data, stdout)
|
||||
log.AddHook(notifier)
|
||||
|
||||
// Do the sending in a separate goroutine so we don't block the main process.
|
||||
go sendNotifications(notifier, delay)
|
||||
|
||||
return notifier
|
||||
// GetNames returns a list of URLs for notification services that has been added
|
||||
func (n *shoutrrrTypeNotifier) GetURLs() []string {
|
||||
return n.Urls
|
||||
}
|
||||
|
||||
func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool, data StaticData, stdout bool) *shoutrrrTypeNotifier {
|
||||
// AddLogHook adds the notifier as a receiver of log messages and starts a go func for processing them
|
||||
func (n *shoutrrrTypeNotifier) AddLogHook() {
|
||||
if n.receiving {
|
||||
return
|
||||
}
|
||||
n.receiving = true
|
||||
log.AddHook(n)
|
||||
|
||||
// Do the sending in a separate goroutine so we don't block the main process.
|
||||
go sendNotifications(n)
|
||||
}
|
||||
|
||||
func createNotifier(urls []string, level log.Level, tplString string, legacy bool, data StaticData, stdout bool, delay time.Duration) *shoutrrrTypeNotifier {
|
||||
tpl, err := getShoutrrrTemplate(tplString, legacy)
|
||||
if err != nil {
|
||||
log.Errorf("Could not use configured notification template: %s. Using default template", err)
|
||||
|
|
@ -97,7 +105,7 @@ func createNotifier(urls []string, levels []log.Level, tplString string, legacy
|
|||
Router: r,
|
||||
messages: make(chan string, 1),
|
||||
done: make(chan bool),
|
||||
logLevels: levels,
|
||||
logLevel: level,
|
||||
template: tpl,
|
||||
legacyTemplate: legacy,
|
||||
data: data,
|
||||
|
|
@ -105,9 +113,9 @@ func createNotifier(urls []string, levels []log.Level, tplString string, legacy
|
|||
}
|
||||
}
|
||||
|
||||
func sendNotifications(n *shoutrrrTypeNotifier, delay time.Duration) {
|
||||
func sendNotifications(n *shoutrrrTypeNotifier) {
|
||||
for msg := range n.messages {
|
||||
time.Sleep(delay)
|
||||
time.Sleep(n.delay)
|
||||
errs := n.Router.Send(msg, n.params)
|
||||
|
||||
for i, err := range errs {
|
||||
|
|
@ -180,7 +188,7 @@ func (n *shoutrrrTypeNotifier) Close() {
|
|||
|
||||
// Levels return what log levels trigger notifications
|
||||
func (n *shoutrrrTypeNotifier) Levels() []log.Level {
|
||||
return n.logLevels
|
||||
return log.AllLevels[:n.logLevel+1]
|
||||
}
|
||||
|
||||
// Fire is the hook that logrus calls on a new log message
|
||||
|
|
@ -202,6 +210,7 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
|
|||
funcs := template.FuncMap{
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"ToJSON": toJSON,
|
||||
"Title": cases.Title(language.AmericanEnglish).String,
|
||||
}
|
||||
tplBase := template.New("").Funcs(funcs)
|
||||
|
|
@ -232,16 +241,3 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// StaticData is the part of the notification template data model set upon initialization
|
||||
type StaticData struct {
|
||||
Title string
|
||||
Host string
|
||||
}
|
||||
|
||||
// Data is the notification template data model
|
||||
type Data struct {
|
||||
StaticData
|
||||
Entries []*log.Entry
|
||||
Report t.Report
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var allButTrace = logrus.AllLevels[0:logrus.TraceLevel]
|
||||
var allButTrace = logrus.DebugLevel
|
||||
|
||||
var legacyMockData = Data{
|
||||
Entries: []*logrus.Entry{
|
||||
|
|
@ -83,6 +83,30 @@ updt1 (mock/updt1:latest): Updated
|
|||
})
|
||||
})
|
||||
|
||||
When("adding a log hook", func() {
|
||||
When("it has not been added before", func() {
|
||||
It("should be added to the logrus hooks", func() {
|
||||
level := logrus.TraceLevel
|
||||
hooksBefore := len(logrus.StandardLogger().Hooks[level])
|
||||
shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second)
|
||||
shoutrrr.AddLogHook()
|
||||
hooksAfter := len(logrus.StandardLogger().Hooks[level])
|
||||
Expect(hooksAfter).To(BeNumerically(">", hooksBefore))
|
||||
})
|
||||
})
|
||||
When("it is being added a second time", func() {
|
||||
It("should not be added to the logrus hooks", func() {
|
||||
level := logrus.TraceLevel
|
||||
shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second)
|
||||
shoutrrr.AddLogHook()
|
||||
hooksBefore := len(logrus.StandardLogger().Hooks[level])
|
||||
shoutrrr.AddLogHook()
|
||||
hooksAfter := len(logrus.StandardLogger().Hooks[level])
|
||||
Expect(hooksAfter).To(Equal(hooksBefore))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("using legacy templates", func() {
|
||||
|
||||
When("no custom template is provided", func() {
|
||||
|
|
@ -90,7 +114,7 @@ updt1 (mock/updt1:latest): Updated
|
|||
cmd := new(cobra.Command)
|
||||
flags.RegisterNotificationFlags(cmd)
|
||||
|
||||
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{}, false)
|
||||
shoutrrr := createNotifier([]string{}, logrus.TraceLevel, "", true, StaticData{}, false, time.Second)
|
||||
|
||||
entries := []*logrus.Entry{
|
||||
{
|
||||
|
|
@ -245,7 +269,7 @@ Turns out everything is on fire
|
|||
When("batching notifications", func() {
|
||||
When("no messages are queued", func() {
|
||||
It("should not send any notification", func() {
|
||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), false, "logger://")
|
||||
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0))
|
||||
shoutrrr.StartNotification()
|
||||
shoutrrr.SendNotification(nil)
|
||||
Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`))
|
||||
|
|
@ -253,7 +277,8 @@ Turns out everything is on fire
|
|||
})
|
||||
When("at least one message is queued", func() {
|
||||
It("should send a notification", func() {
|
||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), false, "logger://")
|
||||
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0))
|
||||
shoutrrr.AddLogHook()
|
||||
shoutrrr.StartNotification()
|
||||
logrus.Info("This log message is sponsored by ContainrrrVPN")
|
||||
shoutrrr.SendNotification(nil)
|
||||
|
|
@ -267,7 +292,7 @@ Turns out everything is on fire
|
|||
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{
|
||||
Host: "test.host",
|
||||
Title: "",
|
||||
}, false)
|
||||
}, false, time.Second)
|
||||
_, found := shoutrrr.params.Title()
|
||||
Expect(found).ToNot(BeTrue())
|
||||
})
|
||||
|
|
@ -321,13 +346,14 @@ func sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *b
|
|||
Router: router,
|
||||
legacyTemplate: legacy,
|
||||
params: &types.Params{},
|
||||
delay: time.Duration(0),
|
||||
}
|
||||
|
||||
entry := &logrus.Entry{
|
||||
Message: "foo bar",
|
||||
}
|
||||
|
||||
go sendNotifications(shoutrrr, time.Duration(0))
|
||||
go sendNotifications(shoutrrr)
|
||||
|
||||
shoutrrr.StartNotification()
|
||||
_ = shoutrrr.Fire(entry)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
shoutrrrDisco "github.com/containrrr/shoutrrr/pkg/services/discord"
|
||||
shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/johntdyer/slackrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -16,11 +15,15 @@ const (
|
|||
)
|
||||
|
||||
type slackTypeNotifier struct {
|
||||
slackrus.SlackrusHook
|
||||
HookURL string
|
||||
Username string
|
||||
Channel string
|
||||
IconEmoji string
|
||||
IconURL string
|
||||
}
|
||||
|
||||
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
func newSlackNotifier(c *cobra.Command) t.ConvertibleNotifier {
|
||||
flags := c.Flags()
|
||||
|
||||
hookURL, _ := flags.GetString("notification-slack-hook-url")
|
||||
userName, _ := flags.GetString("notification-slack-identifier")
|
||||
|
|
@ -29,19 +32,16 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
|
|||
iconURL, _ := flags.GetString("notification-slack-icon-url")
|
||||
|
||||
n := &slackTypeNotifier{
|
||||
SlackrusHook: slackrus.SlackrusHook{
|
||||
HookURL: hookURL,
|
||||
Username: userName,
|
||||
Channel: channel,
|
||||
IconEmoji: emoji,
|
||||
IconURL: iconURL,
|
||||
AcceptedLevels: acceptedLogLevels,
|
||||
},
|
||||
HookURL: hookURL,
|
||||
Username: userName,
|
||||
Channel: channel,
|
||||
IconEmoji: emoji,
|
||||
IconURL: iconURL,
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) {
|
||||
trimmedURL := strings.TrimRight(s.HookURL, "/")
|
||||
trimmedURL = strings.TrimPrefix(trimmedURL, "https://")
|
||||
parts := strings.Split(trimmedURL, "/")
|
||||
|
|
@ -52,7 +52,6 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
|
|||
WebhookID: parts[len(parts)-3],
|
||||
Token: parts[len(parts)-2],
|
||||
Color: ColorInt,
|
||||
Title: title,
|
||||
SplitLines: true,
|
||||
Username: s.Username,
|
||||
}
|
||||
|
|
@ -70,7 +69,6 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
|
|||
BotName: s.Username,
|
||||
Color: ColorHex,
|
||||
Channel: "webhook",
|
||||
Title: title,
|
||||
}
|
||||
|
||||
if s.IconURL != "" {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ func GetDigest(url string, token string) (string, error) {
|
|||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v1+json")
|
||||
req.Header.Add("Accept", "application/vnd.oci.image.index.v1+json")
|
||||
|
||||
logrus.WithField("url", url).Debug("Doing a HEAD request to fetch a digest")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ConvertibleNotifier is a notifier capable of creating a shoutrrr URL
|
||||
type ConvertibleNotifier interface {
|
||||
GetURL(c *cobra.Command, title string) (string, error)
|
||||
GetURL(c *cobra.Command) (string, error)
|
||||
}
|
||||
|
||||
// DelayNotifier is a notifier that might need to be delayed before sending notifications
|
||||
type DelayNotifier interface {
|
||||
GetDelay() time.Duration
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ package types
|
|||
type Notifier interface {
|
||||
StartNotification()
|
||||
SendNotification(Report)
|
||||
AddLogHook()
|
||||
GetNames() []string
|
||||
GetURLs() []string
|
||||
Close()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue